├── .gitignore ├── README.md ├── install.py ├── javascript ├── qrcode.js └── tooltips.js ├── scripts └── qrcode.py └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webui-qrcode-generator 2 | 3 | Adds a tab to generate QR Codes. Supports the following data types. 4 | - Text 5 | - SMS 6 | - Email 7 | - WiFi Network 8 | - vCard 9 | - MeCard 10 | - Location 11 | 12 | ## Installation 13 | Install from `Available` on the `Extensions` tab. 14 | -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | import launch 2 | import importlib.metadata 3 | import packaging.version 4 | 5 | def compare_versions(package, version_required): 6 | try: 7 | version_installed = importlib.metadata.version(package) 8 | except Exception: 9 | return False 10 | 11 | if packaging.version.parse(version_installed) < packaging.version.parse(version_required): 12 | return False 13 | 14 | return True 15 | 16 | if not compare_versions("segno", "1.5.3"): 17 | launch.run_pip('install "segno>=1.5.3"', "requirements for webui-qrcode-generator") -------------------------------------------------------------------------------- /javascript/qrcode.js: -------------------------------------------------------------------------------- 1 | onUiLoaded(function() { 2 | gradioApp().querySelector("#qrcode_geo_latitude input").addEventListener("input", (event) => { 3 | let target = event.originalTarget || event.composedPath()[0]; 4 | let value = target.value; 5 | if (value < -90 || value > 90) { 6 | target.value = clamp(value, -90, 90) 7 | updateInput(target); 8 | } 9 | }); 10 | 11 | gradioApp().querySelector("#qrcode_geo_longitude input").addEventListener("input", (event) => { 12 | let target = event.originalTarget || event.composedPath()[0]; 13 | let value = target.value; 14 | if (value < -180 || value > 180) { 15 | target.value = clamp(value, -180, 180) 16 | updateInput(target); 17 | } 18 | }); 19 | 20 | gradioApp().querySelector("#qrcode_geo_longitude input, #qrcode_geo_latitude input").addEventListener("focusout", (event) => { 21 | let target = event.originalTarget || event.composedPath()[0]; 22 | if (target.value == "") { 23 | target.value = 0; 24 | updateInput(target); 25 | } 26 | }); 27 | }); 28 | 29 | function clamp(num, min, max) { 30 | return Math.min(Math.max(num, min), max); 31 | }; 32 | 33 | async function sendToControlnet(img, tab, index) { 34 | const response = await fetch(img); 35 | const blob = await response.blob(); 36 | const file = new File([blob], "image.png", { type: "image/png" }); 37 | const dt = new DataTransfer(); 38 | dt.items.add(file); 39 | const list = dt.files; 40 | 41 | await window["switch_to_" + tab](); 42 | 43 | const controlnet = gradioApp().querySelector(`#${tab}_script_container #controlnet`); 44 | const accordion = controlnet.querySelector(":scope > .label-wrap") 45 | 46 | if (!accordion.classList.contains("open")) { 47 | await accordion.click(); 48 | } 49 | 50 | const tabs = controlnet.querySelectorAll("div.tab-nav > button"); 51 | 52 | if (tabs !== null && tabs.length > 1) { 53 | tabs[index].click(); 54 | } 55 | 56 | const input = controlnet.querySelectorAll("input[type='file']")[index * 2]; 57 | 58 | if (input == null) { 59 | const callback = (observer) => { 60 | input = controlnet.querySelector("input[type='file']"); 61 | if (input == null) { 62 | return; 63 | } else { 64 | setImage(input, list); 65 | observer.disconnect(); 66 | } 67 | } 68 | const observer = new MutationObserver(callback); 69 | observer.observe(controlnet, { childList: true }); 70 | } else { 71 | setImage(input, list); 72 | } 73 | 74 | controlnet.scrollIntoView(); 75 | } 76 | 77 | function setImage(input, list) { 78 | try { 79 | input.previousElementSibling?.previousElementSibling?.querySelector("button[aria-label='Clear']")?.click(); 80 | } catch (e) { 81 | console.error(e); 82 | } 83 | input.value = ""; 84 | input.files = list; 85 | const event = new Event("change", { "bubbles": true, "composed": true }); 86 | input.dispatchEvent(event); 87 | } 88 | -------------------------------------------------------------------------------- /javascript/tooltips.js: -------------------------------------------------------------------------------- 1 | const qrcode_tooltips = { 2 | "#qrcode_scale": "Size of each module, in pixels", 3 | "#qrcode_border": "Border width, in modules", 4 | "#qrcode_error_correction label:nth-of-type(1)": "Low: 7% correctable", 5 | "#qrcode_error_correction label:nth-of-type(2)": "Medium: 15% correctable", 6 | "#qrcode_error_correction label:nth-of-type(3)": "Quartile: 25% correctable", 7 | "#qrcode_error_correction label:nth-of-type(4)": "High: 30% correctable", 8 | } 9 | 10 | onUiLoaded(function(){ 11 | for (let [key, value] of Object.entries(qrcode_tooltips)) { 12 | e = gradioApp().querySelector(key) 13 | if (e) gradioApp().querySelector(key).title = value; 14 | } 15 | }) -------------------------------------------------------------------------------- /scripts/qrcode.py: -------------------------------------------------------------------------------- 1 | import modules.scripts as scripts 2 | import gradio as gr 3 | import io 4 | import os 5 | 6 | from modules import script_callbacks, generation_parameters_copypaste, extensions 7 | from modules.shared import opts 8 | from PIL import Image 9 | import segno 10 | from segno import helpers 11 | 12 | def generate(keys, *values): 13 | args = dict(zip(keys, values)) 14 | try: 15 | if args["selected_tab"] == "tab_wifi": 16 | if args["wifi_security"] == "None": 17 | args["wifi_password"] = args["wifi_security"] = None 18 | data = helpers.make_wifi_data(ssid=args["wifi_ssid"], password=args["wifi_password"], security=args["wifi_security"], hidden=args["wifi_hidden"]) 19 | elif args["selected_tab"] == "tab_vcard": 20 | name = f'{args["vcard_name_last"]};{args["vcard_name_first"]};{args["vcard_name_middle"]}' 21 | data = helpers.make_vcard_data(name, displayname=args["vcard_displayname"], nickname=args["vcard_nickname"], street=args["vcard_address"], city=args["vcard_city"], region=args["vcard_state"], zipcode=args["vcard_zipcode"], country=args["vcard_country"], birthday=args["vcard_birthday"], email=args["vcard_email"], phone=args["vcard_phone"], fax=args["vcard_fax"], memo=args["vcard_memo"], org=args["vcard_organization"], title=args["vcard_title"], cellphone=args["vcard_phone_mobile"], url=args["vcard_url"]) 22 | elif args["selected_tab"] == "tab_mecard": 23 | data = helpers.make_mecard_data(name=args["mecard_name"], reading=args["mecard_kananame"], nickname=args["mecard_nickname"], houseno=args["mecard_address"], city=args["mecard_city"], prefecture=args["mecard_state"], zipcode=args["mecard_zipcode"], country=args["mecard_country"], birthday=args["mecard_birthday"], email=args["mecard_email"], phone=args["mecard_phone"], memo=args["mecard_memo"]) 24 | elif args["selected_tab"] == "tab_sms": 25 | data = f'smsto:{args["sms_number"]}:{args["sms_message"]}' 26 | elif args["selected_tab"] == "tab_email": 27 | data = helpers.make_make_email_data(to=args["email_address"], subject=args["email_subject"], body=args["email_body"]) 28 | elif args["selected_tab"] == "tab_geo": 29 | data = helpers.make_geo_data(args["geo_latitude"], args["geo_longitude"]) 30 | else: 31 | data = args["text"] 32 | 33 | out = io.BytesIO() 34 | qrcode = segno.make(data, micro=False, error=args["setting_error_correction"], boost_error=False) 35 | scale = 1 if args["size_mode"] == "size" else args["setting_scale"] 36 | qrcode.save(out, kind='png', scale=scale, border=args["setting_border"], dark=args["setting_dark"], light=args["setting_light"]) 37 | except Exception as e: 38 | return None, f'❌{getattr(e, "message", str(e))}' 39 | 40 | img = Image.open(out) 41 | 42 | if args["size_mode"] == "size": 43 | img = img.resize((args["setting_size"], args["setting_size"])) 44 | 45 | return img, None 46 | 47 | def on_ui_tabs(): 48 | with gr.Blocks() as ui_component: 49 | inputs = {} 50 | with gr.Row(): 51 | with gr.Column(): 52 | with gr.Tab("Text") as tab_text: 53 | inputs["text"] = gr.Textbox(show_label=False, lines=3, placeholder="Plain text / URL / Custom format") 54 | 55 | with gr.Tab("WiFi") as tab_wifi: 56 | inputs["wifi_ssid"] = gr.Text(label="SSID") 57 | inputs["wifi_hidden"] = gr.Checkbox(False, label="Hidden SSID") 58 | inputs["wifi_password"] = gr.Text(label="Password") 59 | inputs["wifi_security"] = gr.Radio(value="None", label="Security", choices=["None", "WEP", "WPA"]) 60 | 61 | with gr.Tab("vCard") as tab_vcard: 62 | with gr.Row(): 63 | inputs["vcard_name_first"] = gr.Text(label="First Name") 64 | inputs["vcard_name_middle"] = gr.Text(label="Middle Name") 65 | inputs["vcard_name_last"] = gr.Text(label="Last Name") 66 | inputs["vcard_displayname"] = gr.Text(label="Display Name") 67 | inputs["vcard_nickname"] = gr.Text(label="Nickname") 68 | with gr.Row(): 69 | inputs["vcard_email"] = gr.Text(label="Email") 70 | inputs["vcard_url"] = gr.Text(label="URL") 71 | with gr.Row(): 72 | inputs["vcard_phone"] = gr.Text(label="Phone") 73 | inputs["vcard_phone_mobile"] = gr.Text(label="Mobile Phone") 74 | inputs["vcard_organization"] = gr.Text(label="Organization") 75 | inputs["vcard_title"] = gr.Text(label="Title") 76 | inputs["vcard_address"] = gr.Text(label="Address") 77 | with gr.Row(): 78 | inputs["vcard_city"] = gr.Text(label="City") 79 | inputs["vcard_state"] = gr.Text(label="State") 80 | with gr.Row(): 81 | inputs["vcard_zipcode"] = gr.Text(label="ZIP Code") 82 | inputs["vcard_country"] = gr.Text(label="Country") 83 | inputs["vcard_birthday"] = gr.Text(label="Birthday", placeholder="YYYY-MM-DD") 84 | inputs["vcard_fax"] = gr.Text(label="Fax") 85 | inputs["vcard_memo"] = gr.Text(label="Memo") 86 | 87 | with gr.Tab("MeCard") as tab_mecard: 88 | inputs["mecard_name"] = gr.Text(label="Name") 89 | inputs["mecard_kananame"] = gr.Text(label="Kana Name") 90 | inputs["mecard_nickname"] = gr.Text(label="Nickname") 91 | inputs["mecard_email"] = gr.Text(label="Email") 92 | inputs["mecard_phone"] = gr.Text(label="Phone") 93 | inputs["mecard_address"] = gr.Text(label="Address") 94 | with gr.Row(): 95 | inputs["mecard_city"] = gr.Text(label="City") 96 | inputs["mecard_state"] = gr.Text(label="State") 97 | with gr.Row(): 98 | inputs["mecard_zipcode"] = gr.Text(label="ZIP Code") 99 | inputs["mecard_country"] = gr.Text(label="Country") 100 | inputs["mecard_birthday"] = gr.Text(label="Birthday", placeholder="YYYYMMDD") 101 | inputs["mecard_memo"] = gr.Text(label="Memo") 102 | 103 | with gr.Tab("SMS") as tab_sms: 104 | inputs["sms_number"] = gr.Text(label="Number") 105 | inputs["sms_message"] = gr.Textbox(label="Message", lines=3) 106 | 107 | with gr.Tab("Email") as tab_email: 108 | inputs["email_address"] = gr.Text(label="Address") 109 | inputs["email_subject"] = gr.Text(label="Subject") 110 | inputs["email_body"] = gr.Textbox(label="Message", lines=3) 111 | 112 | with gr.Tab("Location") as tab_geo: 113 | with gr.Row(): 114 | inputs["geo_latitude"] = gr.Number(0, label="Latitude", elem_id="qrcode_geo_latitude") 115 | inputs["geo_longitude"] = gr.Number(0, label="Longitude", elem_id="qrcode_geo_longitude") 116 | 117 | inputs["size_mode"] = gr.State("size") 118 | 119 | with gr.Tab("Scale to") as tab_size: 120 | tab_size.select(lambda: "size", None, inputs["size_mode"]) 121 | inputs["setting_size"] = gr.Slider(label="Size", minimum=64, maximum=2048, value=512, step=8) 122 | 123 | with gr.Tab("Scale by") as tab_scale: 124 | tab_scale.select(lambda: "scale", None, inputs["size_mode"]) 125 | inputs["setting_scale"] = gr.Slider(label="Scale", minimum=1, maximum=50, value=10, step=1, elem_id="qrcode_scale") 126 | 127 | inputs["setting_border"] = gr.Slider(label="Border", minimum=0, maximum=10, value=4, step=1, elem_id="qrcode_border") 128 | 129 | with gr.Row(): 130 | inputs["setting_dark"] = gr.ColorPicker("#000000", label="Module Color") 131 | inputs["setting_light"] = gr.ColorPicker("#ffffff", label="Background Color") 132 | 133 | inputs["setting_error_correction"] = gr.Radio(value="H", label="Error Correction Level", choices=["L", "M", "Q", "H"], elem_id="qrcode_error_correction") 134 | 135 | button_generate = gr.Button("Generate", variant="primary") 136 | status = gr.HTML() 137 | 138 | with gr.Column(): 139 | output = gr.Image(interactive=False, show_label=False, type="pil", format="png", elem_id="qrcode_output", height=480) 140 | 141 | with gr.Row(): 142 | send_to_buttons = generation_parameters_copypaste.create_buttons(["img2img", "inpaint", "extras"]) 143 | for tabname, button in send_to_buttons.items(): 144 | generation_parameters_copypaste.register_paste_params_button(generation_parameters_copypaste.ParamBinding(paste_button=button, tabname=tabname, source_image_component=output)) 145 | 146 | with gr.Row(visible="sd-webui-controlnet" in [x.name for x in extensions.active()]): 147 | sendto_controlnet_txt2img = gr.Button("Send to ControlNet (txt2img)") 148 | sendto_controlnet_img2img = gr.Button("Send to ControlNet (img2img)") 149 | control_net_max_models_num = opts.data.get('control_net_max_models_num', 1) 150 | sendto_controlnet_num = gr.Dropdown([str(i) for i in range(control_net_max_models_num)], label="ControlNet Unit", value="0", interactive=True, visible=(control_net_max_models_num > 1)) 151 | sendto_controlnet_txt2img.click(None, [output, sendto_controlnet_num], None, _js="(i, n) => {sendToControlnet(i, 'txt2img', n)}", show_progress=False) 152 | sendto_controlnet_img2img.click(None, [output, sendto_controlnet_num], None, _js="(i, n) => {sendToControlnet(i, 'img2img', n)}", show_progress=False) 153 | 154 | inputs["selected_tab"] = gr.State("tab_text") 155 | input_keys = gr.State(list(inputs.keys())) 156 | button_generate.click(generate, [input_keys, *inputs.values()], [output, status], show_progress=False) 157 | 158 | tab_text.select(lambda: "tab_text", None, inputs["selected_tab"]) 159 | tab_wifi.select(lambda: "tab_wifi", None, inputs["selected_tab"]) 160 | tab_vcard.select(lambda: "tab_vcard", None, inputs["selected_tab"]) 161 | tab_mecard.select(lambda: "tab_mecard", None, inputs["selected_tab"]) 162 | tab_sms.select(lambda: "tab_sms", None, inputs["selected_tab"]) 163 | tab_email.select(lambda: "tab_email", None, inputs["selected_tab"]) 164 | tab_geo.select(lambda: "tab_geo", None, inputs["selected_tab"]) 165 | 166 | return [(ui_component, "QR Code", "qrcode_tab")] 167 | 168 | script_callbacks.on_ui_tabs(on_ui_tabs) -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | #qrcode_output img { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | width: unset; 8 | height: unset; 9 | max-width: 100%; 10 | max-height: 100%; 11 | margin: auto; 12 | } --------------------------------------------------------------------------------