init commit

pull/15/head
jtydhr88 2023-04-12 20:42:36 -04:00
commit 52557c9352
19 changed files with 16911 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea/
venv/
**/__pycache__/**

35
README.md Normal file
View File

@ -0,0 +1,35 @@
# Stable Diffusion WebUI Canvas Editor
A custom extension for [AUTOMATIC1111/stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui) that integrated a full capability canvas editor which you can use layer, text, image, elements and so on, then send to ControlNet, basing on [Polotno](https://polotno.com/).
![1.png](doc/images/overall.png)
![controlnet.png](doc/images/controlnet.png)
## Installation
Just like you install other extension of webui:
1. go to Extensions -> Install from URL
2. paste this repo link
3. install
4. go to Installed, apply and restart UI
## Key Feature
1. Full capability image editor, such as Effects, Crop, Position, Lock, etc
2. Templates![templates.png](doc/images/templates.png)
3. Text![text.png](doc/images/text.png)
4. Photos![photos.png](doc/images/photos.png)
5. Elements![elements.png](doc/images/elements.png)
6. Upload![upload.png](doc/images/upload.png)
7. Background![background.png](doc/images/background.png)
8. Layers![layers.png](doc/images/layers.png)
## Further Plan
1. rebuild Polotno
2. Send image to img2img, Sketch, Inpaint, etc
3. Pen and eraser support
4. connect to Segment Anything to segment image
5. any suggestions or requirements are welcome
## Polotno API Key
I included my Polotno api key, notice it only supports local environment (localhost or 127.0.0.1) for non-commercial purpose.
It is easy to create your own free api key from [Polotno API](https://polotno.com/cabinet), then you can replace mine from webui Settings -> Canvas Editor
## Credits
Created by [jtydhr88](https://github.com/jtydhr88) basing on [Polotno](https://polotno.com/).

BIN
doc/images/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

BIN
doc/images/controlnet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

BIN
doc/images/elements.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
doc/images/layers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
doc/images/overall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
doc/images/photos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
doc/images/templates.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 KiB

BIN
doc/images/text.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

BIN
doc/images/upload.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

22
index.html Normal file
View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script async src="js/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"polotno": "./js/polotno.bundle.js"
}
}
</script>
<script type="esms-options">
{
"noLoadEventRetriggers": true
}
</script>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,42 @@
(function () {
if (!globalThis.canvasEditor) globalThis.canvasEditor = {};
const canvasEditor = globalThis.canvasEditor;
function load(cont) {
const scripts = cont.textContent.trim().split('\n');
const base_path = `/file=${scripts.shift()}/js`;
cont.textContent = '';
const df = document.createDocumentFragment();
for (let src of scripts) {
const script = document.createElement('script');
script.async = true;
script.type = 'module';
script.src = `file=${src}`;
df.appendChild(script);
}
globalThis.canvasEditor.import = async () => {
const polotno = await import(`${base_path}/polotno.bundle.js`);
return { polotno };
};
if (!globalThis.canvasEditor.imports) {
globalThis.canvasEditor.imports = {};
}
if (!globalThis.canvasEditor.imports.polotno) {
globalThis.canvasEditor.imports.polotno = async () => await import(`${base_path}/polotno.bundle.js`);
}
cont.appendChild(df);
}
onUiLoaded(function () {
canvasEditorImport = gradioApp().querySelector('#canvas-editor-import');
load(canvasEditorImport);
});
})();

View File

@ -0,0 +1,136 @@
console.log('[3D Model Loader] loading...');
async function _import() {
if (!globalThis.canvasEditor || !globalThis.canvasEditor.import) {
return await import('polotno');
} else {
return await globalThis.canvasEditor.imports.polotno();
}
}
await _import();
(async function () {
const container = gradioApp().querySelector('#canvas-editor-container');
const apiKey = gradioApp().querySelector('#canvas-editor-polotno-api-key');
const apiKeyValue = apiKey.value;
const { store } = createPolotnoApp({
// this is a demo key just for that project
// (!) please don't use it in your projects
// to create your own API key please go here: https://polotno.com/cabinet
key: apiKeyValue,
// you can hide back-link on a paid license
// but it will be good if you can keep it for Polotno project support
showCredit: true,
container: container,
});
function dataURLtoFile(dataurl, filename) {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while(n--){
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, {type:mime});
}
window.sendImageCanvasEditor = async function (type, index) {
const imageDataURL = await store.toDataURL();
var file = dataURLtoFile(imageDataURL, 'my-image-file.jpg');
const dt = new DataTransfer();
dt.items.add(file);
const selector = type === "txt2img" ? "#txt2img_script_container" : "#img2img_script_container";
if (type === "txt2img") {
switch_to_txt2img();
} else if (type === "img2img") {
switch_to_img2img();
}
let container = gradioApp().querySelector(selector);
let element = container.querySelector('#controlnet');
if (!element) {
for (const spans of container.querySelectorAll < HTMLSpanElement > (
'.cursor-pointer > span'
)) {
if (!spans.textContent?.includes('ControlNet')) {
continue
}
if (spans.textContent?.includes('M2M')) {
continue
}
element = spans.parentElement?.parentElement
}
if (!element) {
console.error('ControlNet element not found')
return
}
}
const imageElems = element.querySelectorAll('div[data-testid="image"]')
if (!imageElems[Number(index)]) {
let accordion = element.querySelector('.icon');
if (accordion) {
accordion.click();
let controlNetAppeared = false;
let observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
for (let i = 0; i < mutation.addedNodes.length; i++) {
if (mutation.addedNodes[i].tagName === "INPUT") {
controlNetAppeared = true;
const imageElems2 = element.querySelectorAll('div[data-testid="image"]');
updateGradioImage(imageElems2[Number(index)], dt);
observer.disconnect();
return;
}
}
}
});
});
observer.observe(element, {childList: true, subtree: true});
}
} else {
updateGradioImage(imageElems[Number(index)], dt);
}
};
function updateGradioImage (element, dt) {
let clearButton = element.querySelector("button[aria-label='Clear']");
if (clearButton) {
clearButton.click();
}
const input = element.querySelector("input[type='file']");
input.value = ''
input.files = dt.files
input.dispatchEvent(
new Event('change', {
bubbles: true,
composed: true,
})
)
}
})();

789
js/es-module-shims.js Normal file

File diff suppressed because one or more lines are too long

564
js/polotno.bundle.js Normal file

File diff suppressed because one or more lines are too long

0
js/polotno.bundle.js.map Normal file
View File

147
scripts/main.py Normal file
View File

@ -0,0 +1,147 @@
import gradio as gr
import modules.scripts as scripts
from modules import script_callbacks
from modules import extensions
import os
from typing import Callable
from modules.shared import opts
from modules import shared
class Script(scripts.Script):
def __init__(self) -> None:
super().__init__()
def title(self):
return "Canvas Editor"
def show(self, is_img2img):
return scripts.AlwaysVisible
def ui(self, is_img2img):
return ()
def wrap_api(fn):
_r = 0
def f(*args, **kwargs):
nonlocal _r
_r += 1
v = fn(*args, **kwargs)
return v, str(_r)
return f
def js2py(
name: str,
id: Callable[[str], str],
js: Callable[[str], str],
sink: gr.components.IOComponent,
) -> gr.Textbox:
v_set = gr.Button(elem_id=id(f'{name}_set'))
v = gr.Textbox(elem_id=id(name))
v_sink = gr.Textbox()
v_set.click(fn=None, _js=js(name), outputs=[v, v_sink])
v_sink.change(fn=None, _js=js(f'{name}_after'), outputs=[sink])
return v
def py2js(
name: str,
fn: Callable[[], str],
id: Callable[[str], str],
js: Callable[[str], str],
sink: gr.components.IOComponent,
) -> None:
v_fire = gr.Button(elem_id=id(f'{name}_get'))
v_sink = gr.Textbox()
v_sink2 = gr.Textbox()
v_fire.click(fn=wrap_api(fn), outputs=[v_sink, v_sink2])
v_sink2.change(fn=None, _js=js(name), inputs=[v_sink], outputs=[sink])
def jscall(
name: str,
fn: Callable[[str], str],
id: Callable[[str], str],
js: Callable[[str], str],
sink: gr.components.IOComponent,
) -> None:
v_args_set = gr.Button(elem_id=id(f'{name}_args_set'))
v_args = gr.Textbox(elem_id=id(f'{name}_args'))
v_args_sink = gr.Textbox()
v_args_set.click(fn=None, _js=js(f'{name}_args'), outputs=[v_args, v_args_sink])
v_args_sink.change(fn=None, _js=js(f'{name}_args_after'), outputs=[sink])
v_fire = gr.Button(elem_id=id(f'{name}_get'))
v_sink = gr.Textbox()
v_sink2 = gr.Textbox()
v_fire.click(fn=wrap_api(fn), inputs=[v_args], outputs=[v_sink, v_sink2])
v_sink2.change(fn=None, _js=js(name), inputs=[v_sink], outputs=[sink])
def on_ui_tabs():
id = lambda s: f'canvas-editor-{s}'
js = lambda s: f'globalThis["{id(s)}"]'
ext = get_self_extension()
if ext is None:
return []
js_ = [f'{x.path}?{os.path.getmtime(x.path)}' for x in ext.list_files('javascript/lazyload', '.js')]
js_.insert(0, ext.path)
with gr.Blocks(analytics_enabled=False) as canvas_editor:
try:
polotno_api_key = opts.polotno_api_key
except:
polotno_api_key = "bHEpG9Rp0Nq9XrLcwFNu"
gr.HTML(f'<input type="hidden" id="canvas-editor-polotno-api-key" value="{polotno_api_key}" />', visible=False)
import_id = 'canvas-editor-import'
gr.HTML(value='\n'.join(js_), elem_id=import_id, visible=False)
with gr.Row():
gr.HTML('<div id="canvas-editor-container"></div>')
with gr.Row():
send_t2t = gr.Button(value="Send to txt2img")
send_i2i = gr.Button(value="Send to img2img")
try:
control_net_num = opts.control_net_max_models_num
except:
control_net_num = 1
select_target_index = gr.Dropdown([str(i) for i in range(control_net_num)],
label="Send to", value="0", interactive=True,
visible=(control_net_num > 1))
send_t2t.click(None, select_target_index, None, _js="(i) => {sendImageCanvasEditor('txt2img', i)}")
send_i2i.click(None, select_target_index, None, _js="(i) => {sendImageCanvasEditor('img2img', i)}")
return [(canvas_editor, "Canvas Editor", "canvas_editor")]
def get_self_extension():
if '__file__' in globals():
filepath = __file__
else:
import inspect
filepath = inspect.getfile(lambda: None)
for ext in extensions.active():
if ext.path in filepath:
return ext
def on_ui_settings():
section = ('canvas-editor', "Canvas Editor")
shared.opts.add_option("polotno_api_key", shared.OptionInfo(
"bHEpG9Rp0Nq9XrLcwFNu", "Polotno API Key", section=section))
script_callbacks.on_ui_tabs(on_ui_tabs)
script_callbacks.on_ui_settings(on_ui_settings)

15173
style.css Normal file

File diff suppressed because it is too large Load Diff