├── .github.ignore └── workflows │ └── pylint.yml ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── .gitignore ├── .gitmodules ├── .prettierrc.json ├── LICENSE ├── README.MD ├── install.py ├── javascript ├── openoutpaint-ext.js └── openoutpaint-imagehistory.js ├── preload.py ├── scripts ├── api.py ├── interface.py └── main.py └── style.css /.github.ignore/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Pylint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.8", "3.9", "3.10"] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v3 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install pylint 21 | - name: Analysing the code with pylint 22 | run: | 23 | pylint $(git ls-files '*.py') 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: You think somethings is broken in the UI 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | assignees: zero01101, seijihariki 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | *Please complete this form with as much detailed information as possible.* 12 | - type: checkboxes 13 | attributes: 14 | label: Is ths issue about the extension? 15 | description: Most issues should likely be submitted to the main [openOutpaint issue tracker](https://github.com/zero01101/openOutpaint/issues) unless they are truly an issue with Graido or other A1111 extension functionality; e.g. the send-to-* interface buttons. 16 | options: 17 | - label: Yup, this is for sure about the extension 18 | required: true 19 | - type: textarea 20 | id: what-did 21 | attributes: 22 | label: What happened? 23 | description: What happened that you weren't expecting, or what happened incorrectly? 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: steps 28 | attributes: 29 | label: Steps to reproduce the problem 30 | description: Please provide us with precise step-by-step information on how to reproduce the issue 31 | value: | 32 | 1. Go to .... 33 | 2. Press .... 34 | 3. ... [etc] 35 | validations: 36 | required: true 37 | - type: textarea 38 | id: what-should 39 | attributes: 40 | label: What should have happened? 41 | description: Describe what you believe should have ocurred instead of what actually happened. 42 | validations: 43 | required: true 44 | - type: input 45 | id: commit 46 | attributes: 47 | label: Commit where the problem happens 48 | description: Which commit are you running? (i.e. https://github.com/zero01101/openOutpaint/commit/bf21c19ae352800d9e1b37bb490e817b6848e533, bf21c19) 49 | validations: 50 | required: true 51 | - type: dropdown 52 | id: platforms 53 | attributes: 54 | label: What platforms do you use to access openOutpaint? 55 | multiple: true 56 | options: 57 | - Windows 58 | - Linux 59 | - MacOS 60 | - iOS 61 | - Android 62 | - Other/Cloud 63 | validations: 64 | required: true 65 | - type: dropdown 66 | id: browsers 67 | attributes: 68 | label: What browsers do you use to access the UI ? 69 | multiple: true 70 | options: 71 | - Mozilla Firefox 72 | - Google Chrome 73 | - Brave 74 | - Apple Safari 75 | - Microsoft Edge 76 | - Opera 77 | - Other (please list in additional information) 78 | validations: 79 | required: true 80 | - type: textarea 81 | id: browser-extensions 82 | attributes: 83 | label: Browser Extensions/Addons 84 | description: Please list all browser extensions/addons you have running. Some have been known to cause issues with openOutpaint. 85 | validations: 86 | required: true 87 | - type: textarea 88 | id: webui-commandline 89 | attributes: 90 | label: AUTOMATIC1111 webUI Commandline Arguments 91 | description: Please list all used commandline arguments passed to A1111 webUI (i.e. `--api`). 92 | validations: 93 | required: true 94 | - type: textarea 95 | id: misc 96 | attributes: 97 | label: Additional information 98 | description: Please provide us with any relevant additional information, context, screenshots, etc. 99 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | title: "[Feature Request]: " 4 | labels: ["enhancement"] 5 | assignees: zero01101, seijihariki 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | *Please complete this form with as much detailed information as possible.* 12 | - type: textarea 13 | id: related 14 | attributes: 15 | label: Is your feature request related to a problem? Please describe. 16 | description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: feature 21 | attributes: 22 | label: Describe the solution you'd like 23 | description: A clear and concise description of what you want to happen, preferably with example use-case scenario. 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: workflow 28 | attributes: 29 | label: Proposed workflow 30 | description: Please provide us with step by step information on how you'd like the feature to be accessed and used 31 | value: | 32 | 1. Go to .... 33 | 2. Press .... 34 | 3. ... 35 | validations: 36 | required: true 37 | - type: textarea 38 | id: alternatives 39 | attributes: 40 | label: Describe alternatives you've considered 41 | description: A clear and concise description of any alternative solutions or features you've considered. 42 | validations: 43 | required: true 44 | - type: textarea 45 | id: misc 46 | attributes: 47 | label: Additional context 48 | description: Add any other context or screenshots about the feature request here. 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # IDEs 10 | .vscode/ 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 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 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "app"] 2 | path = app 3 | url = https://github.com/zero01101/openOutpaint.git 4 | branch = main 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSameLine": true, 4 | "bracketSpacing": false, 5 | "embeddedLanguageFormatting": "auto", 6 | "htmlWhitespaceSensitivity": "ignore", 7 | "insertPragma": false, 8 | "jsxSingleQuote": false, 9 | "printWidth": 80, 10 | "proseWrap": "preserve", 11 | "quoteProps": "as-needed", 12 | "requirePragma": false, 13 | "semi": true, 14 | "singleQuote": false, 15 | "tabWidth": 2, 16 | "trailingComma": "es5", 17 | "useTabs": true, 18 | "vueIndentScriptAndStyle": false 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 tim h 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | In this repo lives a mighty handy little wrapper for adding [openOutpaint](https://github.com/zero01101/openOutpaint) to [AUTOMATIC1111 webUI](https://github.com/AUTOMATIC1111/stable-diffusion-webui) directly as a native extension. 2 | 3 | Please see the respective READMEs and wikis for each of the above projects for a more comprehensive understanding of their feature sets. 4 | 5 | This extension also adds buttons to send output from webUI txt2img and img2img tools directly to openOutpaint which will also include the prompts used for convenience. 6 | 7 | **_2023-01-23: new `--lock-oo-submodule` commandline argument if you want to roll back to a previous version of openOutpaint and keep it there - be sure to install/run openOutpaint extension at least once before enabling this flag_** 8 | 9 | **Note: Requires `--api` flag enabled in your webui-user launch script!** 10 | 11 | **_FURTHER NOTE: the commandline flag `--gradio-debug` disables custom API routes and completely breaks openOutpaint. please remove it from your COMMANDLINE_ARGS before running openOutpaint._** 12 | 13 | **_EVEN FURTHER NOTE: [PLEASE SEE DOCUMENTATION REGARDING NEW HRfix FEATURES](https://github.com/zero01101/openOutpaint/wiki/Manual#hrfix) IMPLEMENTED AS OF webUI COMMIT [ef27a18](https://github.com/AUTOMATIC1111/stable-diffusion-webui/commit/ef27a18b6b7cb1a8eebdc9b2e88d25baf2c2414d)_** 14 | 15 | ![image](https://user-images.githubusercontent.com/1649724/209033089-fb908d92-0c52-4165-a6a3-e6e9f28b032d.png) 16 | 17 | ### surprising incompatibilities 18 | 19 | **_COLAB USERS: you may experience issues installing openOutpaint (and other webUI extensions) - there is a workaround that has been discovered and tested against [TheLastBen's fast-stable-diffusion](https://github.com/TheLastBen/fast-stable-diffusion). Please see [this discussion](https://github.com/TheLastBen/fast-stable-diffusion/discussions/1161) containing the workaround, which requires adding a command into the final cell of the colab, as well as setting `Enable_API` to `True`._** 20 | 21 | - [microsoft editor extension for chrome/edge seems to disable the overmask slider](https://github.com/zero01101/openOutpaint/discussions/88#discussioncomment-4498341) 22 | - ~~[duckduckgo privacy extension for firefox breaks outpainting, resulting in pure black output](https://github.com/zero01101/openOutpaint-webUI-extension/issues/3#issuecomment-1367694000) - add an exception for your openOutpaint host (likely localhost or 127.0.0.1)~~ should be fixed as of [b128943](https://github.com/zero01101/openOutpaint/commit/b128943f0c94970600fdc1c98bfec22de619866f) 23 | -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from modules import scripts 4 | 5 | git = os.environ.get('GIT', "git") 6 | usefulDirs = sys.argv[0].split(os.sep)[-3:] 7 | 8 | installDir = os.path.join(scripts.basedir(), usefulDirs[0], usefulDirs[1]) 9 | 10 | # Attempt to use launch module from webui 11 | command = f'"{git}" -C "' + installDir +\ 12 | '" submodule update --init --recursive --remote' 13 | if not os.path.isfile(os.path.join(installDir, "app", "index.html")): 14 | try: 15 | from launch import run 16 | stdout = run(command) 17 | if stdout is not None: 18 | print(stdout) 19 | except ImportError: 20 | print("[openoutpaint-extension] We failed to import the 'launch' module. Using 'os'") 21 | try: 22 | os.system(command) 23 | except: 24 | # TODO: find exception type 25 | print(f"[openOutpaint-extension-submodule] failed to download update, check network") 26 | -------------------------------------------------------------------------------- /javascript/openoutpaint-ext.js: -------------------------------------------------------------------------------- 1 | // Txt2Img Send to Resource 2 | const openoutpaint = { 3 | frame: null, 4 | key: null, 5 | }; 6 | 7 | /** 8 | * Converts a Data URL string to a file object 9 | * 10 | * Based on https://stackoverflow.com/questions/28041840/convert-dataurl-to-file-using-javascript 11 | * 12 | * @param {string} dataurl Data URL to load into a file 13 | * @returns 14 | */ 15 | function openoutpaint_dataURLtoFile(dataurl) { 16 | var arr = dataurl.split(","), 17 | mime = arr[0].match(/:(.*?);/)[1], 18 | bstr = atob(arr[1]), 19 | n = bstr.length, 20 | u8arr = new Uint8Array(n); 21 | while (n--) { 22 | u8arr[n] = bstr.charCodeAt(n); 23 | } 24 | return new File([u8arr], "openOutpaint-file", {type: mime}); 25 | } 26 | 27 | async function openoutpaint_get_image_from_gallery() { 28 | var buttons = gradioApp().querySelectorAll( 29 | '[style="display: block;"].tabitem div[id$=_gallery] .thumbnail-item.thumbnail-small.selected' 30 | ); 31 | var buttonsArray = Array.from(buttons); 32 | var hiddenButtons = gradioApp().querySelectorAll( 33 | '[style="display: none;"].tabitem div[id$=_gallery] .thumbnail-item.thumbnail-small.selected' 34 | ); 35 | var hiddenButtonsArray = Array.from(hiddenButtons); 36 | var selectedButton = buttonsArray.filter( 37 | (value) => !hiddenButtonsArray.includes(value) 38 | ); 39 | var button = selectedButton[0]; 40 | 41 | if (!button) button = buttons[0]; 42 | 43 | if (!button) 44 | throw new Error("[openoutpaint] No image available in the gallery"); 45 | 46 | const canvas = document.createElement("canvas"); 47 | const image = document.createElement("img"); 48 | image.src = button.querySelector("img").src; 49 | 50 | await image.decode(); 51 | 52 | canvas.width = image.width; 53 | canvas.height = image.height; 54 | 55 | canvas.getContext("2d").drawImage(image, 0, 0); 56 | 57 | return canvas.toDataURL(); 58 | } 59 | 60 | function openoutpaint_send_image(dataURL, name = "Embed Resource") { 61 | openoutpaint.frame.contentWindow.postMessage({ 62 | key: openoutpaint.key, 63 | type: "openoutpaint/add-resource", 64 | image: { 65 | dataURL, 66 | resourceName: name, 67 | }, 68 | }); 69 | } 70 | 71 | function openoutpaint_gototab(tabname = "openOutpaint", tabsId = "tabs") { 72 | Array.from( 73 | gradioApp().querySelectorAll(`#${tabsId} > div:first-child button`) 74 | ).forEach((button) => { 75 | if (button.textContent.trim() === tabname) { 76 | button.click(); 77 | } 78 | }); 79 | } 80 | 81 | function openoutpaint_send_gallery(name = "Embed Resource") { 82 | openoutpaint_get_image_from_gallery() 83 | .then((dataURL) => { 84 | // Send to openOutpaint 85 | openoutpaint_send_image(dataURL, name); 86 | 87 | // Send prompt to openOutpaint 88 | const tab = get_uiCurrentTabContent().id; 89 | 90 | if (["tab_txt2img", "tab_img2img"].includes(tab)) { 91 | const prompt = 92 | tab === "tab_txt2img" 93 | ? gradioApp().querySelector("#txt2img_prompt textarea").value 94 | : gradioApp().querySelector("#img2img_prompt textarea").value; 95 | const negPrompt = 96 | tab === "tab_txt2img" 97 | ? gradioApp().querySelector("#txt2img_neg_prompt textarea").value 98 | : gradioApp().querySelector("#img2img_neg_prompt textarea").value; 99 | openoutpaint.frame.contentWindow.postMessage({ 100 | key: openoutpaint.key, 101 | type: "openoutpaint/set-prompt", 102 | prompt, 103 | negPrompt, 104 | }); 105 | } 106 | 107 | // Change Tab 108 | openoutpaint_gototab(); 109 | }) 110 | .catch((error) => { 111 | console.warn("[openoutpaint] No image selected to send to openOutpaint"); 112 | }); 113 | } 114 | 115 | const openoutpaintjs = async () => { 116 | const frame = gradioApp().getElementById("openoutpaint-iframe"); 117 | const key = gradioApp().getElementById("openoutpaint-key").value; 118 | 119 | openoutpaint.frame = frame; 120 | openoutpaint.key = key; 121 | 122 | // Listens for messages from the frame 123 | console.info("[openoutpaint] Add message listener"); 124 | window.addEventListener("message", ({data, origin, source}) => { 125 | if (source === frame.contentWindow) { 126 | switch (data.type) { 127 | case "openoutpaint/ack": 128 | if (data.message.type === "openoutpaint/init") { 129 | console.info("[openoutpaint] Received Init Ack"); 130 | clearTimeout(initLoop); 131 | initLoop = null; 132 | } 133 | break; 134 | case "openoutpaint/sendto": 135 | console.info( 136 | `[openoutpaint] Exported image to '${data.message.destination}'` 137 | ); 138 | const container = new DataTransfer(); 139 | const file = openoutpaint_dataURLtoFile(data.message.image); 140 | container.items.add(file); 141 | 142 | const setImageInput = (selector) => { 143 | const inputel = gradioApp().querySelector(selector); 144 | inputel.files = container.files; 145 | inputel.dispatchEvent(new Event("change")); 146 | }; 147 | 148 | switch (data.message.destination) { 149 | case "img2img": 150 | openoutpaint_gototab("img2img"); 151 | openoutpaint_gototab("img2img", "mode_img2img"); 152 | setImageInput("#img2img_img2img_tab input[type=file]"); 153 | break; 154 | case "img2img_sketch": 155 | openoutpaint_gototab("img2img"); 156 | openoutpaint_gototab("Sketch", "mode_img2img"); 157 | setImageInput("#img2img_img2img_sketch_tab input[type=file]"); 158 | break; 159 | case "img2img_inpaint": 160 | openoutpaint_gototab("img2img"); 161 | openoutpaint_gototab("Inpaint", "mode_img2img"); 162 | setImageInput("#img2img_inpaint_tab input[type=file]"); 163 | break; 164 | case "img2img_sketch_inpaint": 165 | openoutpaint_gototab("img2img"); 166 | openoutpaint_gototab("Inpaint sketch", "mode_img2img"); 167 | setImageInput("#img2img_inpaint_sketch_tab input[type=file]"); 168 | break; 169 | case "extras": 170 | openoutpaint_gototab("Extras"); 171 | setImageInput("#extras_single_tab input[type=file]"); 172 | break; 173 | case "pnginfo": 174 | openoutpaint_gototab("PNG Info"); 175 | setImageInput("#tab_pnginfo input[type=file]"); 176 | break; 177 | default: 178 | console.warn( 179 | `[openoutpaint] Unknown destination ${data.message.destination}` 180 | ); 181 | } 182 | break; 183 | } 184 | } 185 | }); 186 | 187 | // Initializes communication channel 188 | let initLoop = null; 189 | const sendInit = () => { 190 | console.info("[openoutpaint] Sending init message"); 191 | const pathname = window.location.pathname; 192 | const host = `${window.location.origin}${ 193 | pathname.endsWith("/") 194 | ? pathname.substring(0, pathname.length - 1) 195 | : pathname 196 | }`; 197 | frame.contentWindow.postMessage({ 198 | type: "openoutpaint/init", 199 | key, 200 | host, 201 | destinations: [ 202 | { 203 | name: "Image to Image", 204 | id: "img2img", 205 | }, 206 | { 207 | name: "Sketch", 208 | id: "img2img_sketch", 209 | }, 210 | { 211 | name: "Inpaint", 212 | id: "img2img_inpaint", 213 | }, 214 | { 215 | name: "Sketch & Inpaint", 216 | id: "img2img_sketch_inpaint", 217 | }, 218 | { 219 | name: "Extras", 220 | id: "extras", 221 | }, 222 | { 223 | name: "PNG Info", 224 | id: "pnginfo", 225 | }, 226 | ], 227 | }); 228 | initLoop = setTimeout(sendInit, 1000); 229 | }; 230 | 231 | frame.addEventListener("load", () => { 232 | sendInit(); 233 | }); 234 | 235 | // Setup openOutpaint tab scaling 236 | const tabEl = gradioApp().getElementById("tab_openOutpaint"); 237 | frame.style.left = "0px"; 238 | 239 | const refreshBtn = document.createElement("button"); 240 | refreshBtn.id = "openoutpaint-refresh"; 241 | refreshBtn.textContent = "🔄"; 242 | refreshBtn.title = "Refresh openOutpaint"; 243 | refreshBtn.style.width = "fit-content"; 244 | refreshBtn.classList.add("gr-button", "gr-button-lg", "gr-button-secondary"); 245 | refreshBtn.addEventListener("click", async () => { 246 | if (confirm("Are you sure you want to refresh openOutpaint?")) { 247 | frame.contentWindow.location.reload(); 248 | } 249 | }); 250 | tabEl.appendChild(refreshBtn); 251 | 252 | const recalculate = () => { 253 | // If we are on the openoutpaint tab, recalculate 254 | if (tabEl.style.display !== "none") { 255 | frame.style.height = window.innerHeight + "px"; 256 | const current = document.body.scrollHeight; 257 | const bb = frame.getBoundingClientRect(); 258 | const iframeh = bb.height; 259 | const innerh = window.innerHeight; 260 | frame.style.height = `${Math.floor(iframeh + (innerh - current)) - 1}px`; 261 | frame.style.width = `${Math.floor(window.innerWidth) - 1}px`; 262 | frame.style.left = `${Math.floor( 263 | parseInt(frame.style.left, 10) - bb.x 264 | )}px`; 265 | } 266 | }; 267 | 268 | window.addEventListener("resize", () => { 269 | recalculate(); 270 | }); 271 | 272 | new MutationObserver((e) => { 273 | recalculate(); 274 | }).observe(tabEl, { 275 | attributes: true, 276 | }); 277 | 278 | // Add button to other tabs 279 | const createButton = (tabname = "default", tool = true) => { 280 | const button = document.createElement("button"); 281 | button.id = tabname + "_openOutpaint_button"; 282 | button.classList.add("lg", "secondary", "gradio-button", "svelte-cmf5ev"); 283 | button.title = "Send image to openOutpaint."; 284 | if (tool) { 285 | button.classList.add("tool"); 286 | button.textContent = "🐠"; 287 | } else { 288 | button.textContent = "Send to openOutpaint"; 289 | } 290 | return button; 291 | }; 292 | 293 | const extrasBtn = createButton("extras"); 294 | extrasBtn.addEventListener("click", () => 295 | openoutpaint_send_gallery("WebUI Extras Resource") 296 | ); 297 | gradioApp() 298 | .querySelector("#tab_extras button#extras_send_to_extras") 299 | .after(extrasBtn); 300 | 301 | const pnginfoBtn = createButton("pnginfo", false); 302 | pnginfoBtn.addEventListener("click", () => { 303 | const image = gradioApp().querySelector("#pnginfo_image img"); 304 | if (image && image.src) { 305 | openoutpaint_send_image(image.src, "WebUI PNGInfo Resource"); 306 | openoutpaint_gototab(); 307 | } 308 | }); 309 | gradioApp().querySelector("#tab_pnginfo button#extras_tab").after(pnginfoBtn); 310 | 311 | // Initial calculations 312 | sendInit(); 313 | recalculate(); 314 | 315 | new MutationObserver((mutations) => { 316 | if ( 317 | mutations.some( 318 | (mutation) => 319 | mutation.attributeName === "style" && 320 | mutation.target.style.display !== "none" 321 | ) 322 | ) 323 | frame.contentWindow.focus(); 324 | }).observe(tabEl, { 325 | attributes: true, 326 | }); 327 | 328 | if (tabEl.style.display !== "none") frame.contentWindow.focus(); 329 | }; 330 | document.addEventListener("DOMContentLoaded", () => { 331 | const onload = () => { 332 | if (gradioApp().getElementById("openoutpaint-iframe")) { 333 | openoutpaintjs(); 334 | } else { 335 | setTimeout(onload, 10); 336 | } 337 | }; 338 | onload(); 339 | }); 340 | -------------------------------------------------------------------------------- /javascript/openoutpaint-imagehistory.js: -------------------------------------------------------------------------------- 1 | async function openoutpaint_get_image_from_history() { 2 | return new Promise(function (resolve, reject) { 3 | var buttons = gradioApp().querySelectorAll( 4 | '#tab_images_history [style="display: block;"].tabitem div[id$=_gallery] .gallery-item' 5 | ); 6 | var button = gradioApp().querySelector( 7 | '#tab_images_history [style="display: block;"].tabitem div[id$=_gallery] .gallery-item.\\!ring-2' 8 | ); 9 | 10 | if (!button) button = buttons[0]; 11 | 12 | if (!button) 13 | reject(new Error("[openoutpaint] No image available in the gallery")); 14 | 15 | const canvas = document.createElement("canvas"); 16 | const image = document.createElement("img"); 17 | image.onload = () => { 18 | canvas.width = image.width; 19 | canvas.height = image.height; 20 | 21 | canvas.getContext("2d").drawImage(image, 0, 0); 22 | 23 | resolve(canvas.toDataURL()); 24 | }; 25 | image.src = button.querySelector("img").src; 26 | }); 27 | } 28 | 29 | function openoutpaint_send_history_gallery( 30 | name = "Image Browser Resource", 31 | tab 32 | ) { 33 | openoutpaint_get_image_from_history() 34 | .then((dataURL) => { 35 | // Send to openOutpaint 36 | openoutpaint.frame.contentWindow.postMessage({ 37 | key: openoutpaint.key, 38 | type: "openoutpaint/add-resource", 39 | image: { 40 | dataURL, 41 | resourceName: name, 42 | }, 43 | }); 44 | 45 | // Send prompt to openOutpaint 46 | const tab = get_uiCurrentTabContent().id; 47 | const prompt = 48 | tab === "tab_txt2img" 49 | ? gradioApp().querySelector("#txt2img_prompt textarea").value 50 | : gradioApp().querySelector("#img2img_prompt textarea").value; 51 | const negPrompt = 52 | tab === "tab_txt2img" 53 | ? gradioApp().querySelector("#txt2img_neg_prompt textarea").value 54 | : gradioApp().querySelector("#img2img_neg_prompt textarea").value; 55 | openoutpaint.frame.contentWindow.postMessage({ 56 | key: openoutpaint.key, 57 | type: "openoutpaint/set-prompt", 58 | prompt, 59 | negPrompt, 60 | }); 61 | 62 | // Change Tab 63 | Array.from( 64 | gradioApp().querySelectorAll("#tabs > div:first-child button") 65 | ).forEach((button) => { 66 | if (button.textContent.trim() === "openOutpaint") { 67 | button.click(); 68 | } 69 | }); 70 | }) 71 | .catch((error) => { 72 | console.warn("[openoutpaint] No image selected to send to openOutpaint"); 73 | }); 74 | } 75 | 76 | document.addEventListener("DOMContentLoaded", () => { 77 | let tries = 3; 78 | const onload = () => { 79 | const element = gradioApp().getElementById("tab_images_history"); 80 | const tabsEl = gradioApp().getElementById("images_history_tab"); 81 | if (element) { 82 | console.debug(`[oo-ext] Detected image history extension`); 83 | 84 | // Gets tab buttons 85 | const tabs = Array.from(tabsEl.firstChild.querySelectorAll("button")).map( 86 | (button) => button.textContent.trim() 87 | ); 88 | 89 | tabs.forEach((tab) => { 90 | const buttonPanel = gradioApp().getElementById( 91 | `${tab}_images_history_button_panel` 92 | ); 93 | 94 | if (!buttonPanel) return; 95 | 96 | const button = document.createElement("button"); 97 | button.textContent = "Send to openOutpaint"; 98 | button.classList.add( 99 | "gr-button", 100 | "gr-button-lg", 101 | "gr-button-secondary" 102 | ); 103 | button.addEventListener("click", () => { 104 | openoutpaint_send_history_gallery(`Image Browser (${tab}) Resource`); 105 | }); 106 | 107 | buttonPanel.appendChild(button); 108 | }); 109 | } else if (tries-- > 0) { 110 | // Tries n times every 1 second before giving up 111 | setTimeout(onload, 1000); 112 | } 113 | }; 114 | onload(); 115 | }); 116 | -------------------------------------------------------------------------------- /preload.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | def preload(parser: argparse.ArgumentParser): 4 | parser.add_argument("--lock-oo-submodule", action='store_true', 5 | help="(openOutpaint-webUI-extension) Prevent checking for main openOutpaint submodule updates.") 6 | -------------------------------------------------------------------------------- /scripts/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Response, Form 2 | from fastapi.responses import JSONResponse 3 | from modules import sd_models, shared, scripts 4 | import asyncio 5 | import gradio as gr 6 | 7 | 8 | def test_api(_: gr.Blocks, app: FastAPI): 9 | """ 10 | it kept yelling at me without the stupid gradio import there, i'm sure i did something wrong 11 | -------------------------- 12 | Error executing callback app_started_callback for E:\storage\stable-diffusion-webui\extensions\apitest\scripts\api.py 13 | Traceback (most recent call last): 14 | File "E:\storage\stable-diffusion-webui\modules\script_callbacks.py", line 88, in app_started_callback 15 | c.callback(demo, app) 16 | TypeError: test_api() takes 1 positional argument but 2 were given 17 | """ 18 | @app.post("/openOutpaint/unet-count") 19 | async def return_model_unet_channel_count( 20 | model_name: str = Form(description="the model to be inspected") 21 | ): 22 | err_msg = "" 23 | try: 24 | model = sd_models.checkpoints_list[model_name] 25 | except: 26 | err_msg = "submitted model failed loading, falling back to loaded model" 27 | model = sd_models.checkpoints_list[get_current_model()] 28 | theta_0 = sd_models.read_state_dict(model.filename, map_location='cpu') 29 | channelCount = theta_0["model.diffusion_model.input_blocks.0.0.weight"].shape[1] 30 | return { 31 | "unet_channels": channelCount, 32 | "estimated_type": switchAssumption(channelCount), 33 | "tested_model": model, 34 | "additional_data": err_msg 35 | } 36 | 37 | def switchAssumption(channelCount): 38 | return { 39 | 4: "traditional", 40 | 5: "sdv2 depth2img", 41 | 7: "sdv2 upscale 4x", 42 | 8: "instruct-pix2pix", 43 | 9: "inpainting" 44 | }.get(channelCount, "¯\_(ツ)_/¯") 45 | 46 | def get_current_model(): 47 | options = {} 48 | for key in shared.opts.data.keys(): 49 | metadata = shared.opts.data_labels.get(key) 50 | if(metadata is not None): 51 | options.update({key: shared.opts.data.get(key, shared.opts.data_labels.get(key).default)}) 52 | else: 53 | options.update({key: shared.opts.data.get(key, None)}) 54 | 55 | return options["sd_model_checkpoint"] # super inefficient but i'm a moron 56 | 57 | 58 | try: 59 | import modules.script_callbacks as script_callbacks 60 | script_callbacks.on_app_started(test_api) 61 | except: 62 | print("[openOutpaint-webui-extension] UNET API failed to initialize") 63 | 64 | 65 | -------------------------------------------------------------------------------- /scripts/interface.py: -------------------------------------------------------------------------------- 1 | import gradio as gr 2 | from modules import scripts 3 | from modules.ui_components import ToolButton 4 | 5 | 6 | class Script(scripts.Script): 7 | def title(self): 8 | return "OpenOutpaint" 9 | 10 | def show(self, is_img2img): 11 | return scripts.AlwaysVisible 12 | 13 | def after_component(self, component, **kwargs): 14 | # Add button to both txt2img and img2img tabs 15 | if kwargs.get("elem_id") == "txt2img_send_to_extras" or kwargs.get("elem_id") == "img2img_send_to_extras": 16 | tabname = kwargs.get("elem_id").replace("_send_to_extras", "") 17 | new_send_button = ToolButton('🐠', elem_id=f'{tabname}_openOutpaint_button', tooltip="Send image and prompt parameters to openOutpaint.") 18 | new_send_button.click(None, [], None, 19 | _js="() => openoutpaint_send_gallery('WebUI " + tabname + " Resource')") 20 | 21 | def ui(self, is_img2img): 22 | return [] 23 | -------------------------------------------------------------------------------- /scripts/main.py: -------------------------------------------------------------------------------- 1 | from modules import script_callbacks, scripts, shared 2 | import gradio as gr 3 | from fastapi import FastAPI 4 | import os 5 | from launch import run 6 | import pathlib 7 | import inspect 8 | 9 | import string 10 | import random as rd 11 | 12 | 13 | extension_dir = pathlib.Path(inspect.getfile(lambda: None)).parent.parent 14 | key_characters = (string.ascii_letters + string.digits) 15 | 16 | 17 | def random_string(length=20): 18 | return ''.join([rd.choice(key_characters) for _ in range(length)]) 19 | 20 | 21 | key = random_string() 22 | 23 | 24 | def get_files(path): 25 | # Gets all files 26 | directories = set() 27 | for root, _, files in os.walk(path.resolve()): 28 | for file in files: 29 | directories.add(root + '/' + file) 30 | 31 | return directories 32 | 33 | 34 | def started(demo, app: FastAPI): 35 | try: 36 | # Force allow paths for fixing symlinked extension directory references 37 | force_allow = get_files(extension_dir / "app") 38 | 39 | # Add to allowed files list 40 | app.blocks.temp_file_sets.append(force_allow) 41 | 42 | # Force allow paths for fixing symlinked extension directory references (base javascript files now) 43 | force_allow = get_files(extension_dir / "javascript") 44 | 45 | # Add to allowed files list 46 | app.blocks.temp_file_sets.append(force_allow) 47 | except Exception: 48 | print(f"[openOutpaint] Could not force allowed files. Skipping...") 49 | pass 50 | 51 | 52 | def update_app(): 53 | try: 54 | git = os.environ.get('GIT', "git") 55 | # print(scripts.basedir) 56 | run(f'"{git}" -C "' + os.path.join(scripts.basedir(), usefulDirs[0], usefulDirs[1]) + 57 | '" submodule update --init --recursive --remote', live=True) 58 | except: 59 | # TODO: find exception type 60 | print(f"[openOutpaint-extension-submodule] failed to download update, check network") 61 | 62 | 63 | def add_tab(): 64 | try: 65 | if shared.cmd_opts.lock_oo_submodule: 66 | print(f"[openOutpaint] Submodule locked. Will skip submodule update.") 67 | else: 68 | update_app() 69 | except Exception: 70 | update_app() 71 | 72 | with gr.Blocks(analytics_enabled=False) as ui: 73 | #refresh = gr.Button(value="refresh", variant="primary") 74 | canvas = gr.HTML( 75 | f"") 76 | keyinput = gr.HTML( 77 | f"") 78 | 79 | return [(ui, "openOutpaint", "openOutpaint")] 80 | 81 | 82 | usefulDirs = scripts.basedir().split(os.sep)[-2:] 83 | 84 | with open(f"{scripts.basedir()}/app/key.json", "w") as keyfile: 85 | keyfile.write('{\n') 86 | keyfile.write(f" \"key\": \"{key}\"\n") 87 | keyfile.write('}\n') 88 | keyfile.close() 89 | 90 | script_callbacks.on_ui_tabs(add_tab) 91 | script_callbacks.on_app_started(started) 92 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | #tab_openOutpaint { 2 | position: relative; 3 | 4 | padding: 0; 5 | border: 0; 6 | } 7 | 8 | #openoutpaint-iframe { 9 | position: absolute; 10 | 11 | top: -2px; 12 | 13 | box-sizing: border-box; 14 | 15 | z-index: 1; 16 | } 17 | 18 | #openoutpaint-refresh { 19 | position: absolute; 20 | 21 | top: 10px; 22 | left: 0; 23 | right: 0; 24 | 25 | margin: auto; 26 | 27 | z-index: 9999; /* little guy gets bashful and hides behind the canvas in firefox */ 28 | } 29 | --------------------------------------------------------------------------------