init commit
|
|
@ -0,0 +1,3 @@
|
|||
.idea/
|
||||
venv/
|
||||
**/__pycache__/**
|
||||
|
|
@ -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/).
|
||||

|
||||

|
||||
|
||||
## 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
|
||||
3. Text
|
||||
4. Photos
|
||||
5. Elements
|
||||
6. Upload
|
||||
7. Background
|
||||
8. Layers
|
||||
|
||||
## 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/).
|
||||
|
After Width: | Height: | Size: 603 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 905 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 604 KiB |
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
});
|
||||
})();
|
||||
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
||||
})();
|
||||
|
|
@ -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)
|
||||