upload a cn

This commit is contained in:
lllyasviel
2024-01-27 10:34:31 -08:00
parent ef02d8fa39
commit ccea2f3305
940 changed files with 150956 additions and 0 deletions

View File

@@ -0,0 +1,311 @@
/**
* Give a badge on ControlNet Accordion indicating total number of active
* units.
* Make active unit's tab name green.
* Append control type to tab name.
* Disable resize mode selection when A1111 img2img input is used.
*/
(function () {
const cnetAllAccordions = new Set();
onUiUpdate(() => {
const ImgChangeType = {
NO_CHANGE: 0,
REMOVE: 1,
ADD: 2,
SRC_CHANGE: 3,
};
function imgChangeObserved(mutationsList) {
// Iterate over all mutations that just occured
for (let mutation of mutationsList) {
// Check if the mutation is an addition or removal of a node
if (mutation.type === 'childList') {
// Check if nodes were added
if (mutation.addedNodes.length > 0) {
for (const node of mutation.addedNodes) {
if (node.tagName === 'IMG') {
return ImgChangeType.ADD;
}
}
}
// Check if nodes were removed
if (mutation.removedNodes.length > 0) {
for (const node of mutation.removedNodes) {
if (node.tagName === 'IMG') {
return ImgChangeType.REMOVE;
}
}
}
}
// Check if the mutation is a change of an attribute
else if (mutation.type === 'attributes') {
if (mutation.target.tagName === 'IMG' && mutation.attributeName === 'src') {
return ImgChangeType.SRC_CHANGE;
}
}
}
return ImgChangeType.NO_CHANGE;
}
function childIndex(element) {
// Get all child nodes of the parent
let children = Array.from(element.parentNode.childNodes);
// Filter out non-element nodes (like text nodes and comments)
children = children.filter(child => child.nodeType === Node.ELEMENT_NODE);
return children.indexOf(element);
}
function imageInputDisabledAlert() {
alert('Inpaint control type must use a1111 input in img2img mode.');
}
class ControlNetUnitTab {
constructor(tab, accordion) {
this.tab = tab;
this.accordion = accordion;
this.isImg2Img = tab.querySelector('.cnet-unit-enabled').id.includes('img2img');
this.enabledCheckbox = tab.querySelector('.cnet-unit-enabled input');
this.inputImage = tab.querySelector('.cnet-input-image-group .cnet-image input[type="file"]');
this.inputImageContainer = tab.querySelector('.cnet-input-image-group .cnet-image');
this.controlTypeRadios = tab.querySelectorAll('.controlnet_control_type_filter_group input[type="radio"]');
this.resizeModeRadios = tab.querySelectorAll('.controlnet_resize_mode_radio input[type="radio"]');
this.runPreprocessorButton = tab.querySelector('.cnet-run-preprocessor');
const tabs = tab.parentNode;
this.tabNav = tabs.querySelector('.tab-nav');
this.tabIndex = childIndex(tab) - 1; // -1 because tab-nav is also at the same level.
this.attachEnabledButtonListener();
this.attachControlTypeRadioListener();
this.attachTabNavChangeObserver();
this.attachImageUploadListener();
this.attachImageStateChangeObserver();
this.attachA1111SendInfoObserver();
this.attachPresetDropdownObserver();
}
getTabNavButton() {
return this.tabNav.querySelector(`:nth-child(${this.tabIndex + 1})`);
}
getActiveControlType() {
for (let radio of this.controlTypeRadios) {
if (radio.checked) {
return radio.value;
}
}
return undefined;
}
updateActiveState() {
const tabNavButton = this.getTabNavButton();
if (!tabNavButton) return;
if (this.enabledCheckbox.checked) {
tabNavButton.classList.add('cnet-unit-active');
} else {
tabNavButton.classList.remove('cnet-unit-active');
}
}
updateActiveUnitCount() {
function getActiveUnitCount(checkboxes) {
let activeUnitCount = 0;
for (const checkbox of checkboxes) {
if (checkbox.checked)
activeUnitCount++;
}
return activeUnitCount;
}
const checkboxes = this.accordion.querySelectorAll('.cnet-unit-enabled input');
const span = this.accordion.querySelector('.label-wrap span');
// Remove existing badge.
if (span.childNodes.length !== 1) {
span.removeChild(span.lastChild);
}
// Add new badge if necessary.
const activeUnitCount = getActiveUnitCount(checkboxes);
if (activeUnitCount > 0) {
const div = document.createElement('div');
div.classList.add('cnet-badge');
div.classList.add('primary');
div.innerHTML = `${activeUnitCount} unit${activeUnitCount > 1 ? 's' : ''}`;
span.appendChild(div);
}
}
/**
* Add the active control type to tab displayed text.
*/
updateActiveControlType() {
const tabNavButton = this.getTabNavButton();
if (!tabNavButton) return;
// Remove the control if exists
const controlTypeSuffix = tabNavButton.querySelector('.control-type-suffix');
if (controlTypeSuffix) controlTypeSuffix.remove();
// Add new suffix.
const controlType = this.getActiveControlType();
if (controlType === 'All') return;
const span = document.createElement('span');
span.innerHTML = `[${controlType}]`;
span.classList.add('control-type-suffix');
tabNavButton.appendChild(span);
}
/**
* When 'Inpaint' control type is selected in img2img:
* - Make image input disabled
* - Clear existing image input
*/
updateImageInputState() {
if (!this.isImg2Img) return;
const tabNavButton = this.getTabNavButton();
if (!tabNavButton) return;
const controlType = this.getActiveControlType();
if (controlType.toLowerCase() === 'inpaint') {
this.inputImage.disabled = true;
this.inputImage.parentNode.addEventListener('click', imageInputDisabledAlert);
const removeButton = this.tab.querySelector(
'.cnet-input-image-group .cnet-image button[aria-label="Remove Image"]');
if (removeButton) removeButton.click();
} else {
this.inputImage.disabled = false;
this.inputImage.parentNode.removeEventListener('click', imageInputDisabledAlert);
}
}
attachEnabledButtonListener() {
this.enabledCheckbox.addEventListener('change', () => {
this.updateActiveState();
this.updateActiveUnitCount();
});
}
attachControlTypeRadioListener() {
for (const radio of this.controlTypeRadios) {
radio.addEventListener('change', () => {
this.updateActiveControlType();
});
}
}
/**
* Each time the active tab change, all tab nav buttons are cleared and
* regenerated by gradio. So we need to reapply the active states on
* them.
*/
attachTabNavChangeObserver() {
new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
this.updateActiveState();
this.updateActiveControlType();
}
}
}).observe(this.tabNav, { childList: true });
}
attachImageUploadListener() {
// Automatically check `enable` checkbox when image is uploaded.
this.inputImage.addEventListener('change', (event) => {
if (!event.target.files) return;
if (!this.enabledCheckbox.checked)
this.enabledCheckbox.click();
});
// Automatically check `enable` checkbox when JSON pose file is uploaded.
this.tab.querySelector('.cnet-upload-pose input').addEventListener('change', (event) => {
if (!event.target.files) return;
if (!this.enabledCheckbox.checked)
this.enabledCheckbox.click();
});
}
attachImageStateChangeObserver() {
new MutationObserver((mutationsList) => {
const changeObserved = imgChangeObserved(mutationsList);
if (changeObserved === ImgChangeType.ADD) {
// enabling the run preprocessor button
this.runPreprocessorButton.removeAttribute("disabled");
this.runPreprocessorButton.title = 'Run preprocessor';
}
if (changeObserved === ImgChangeType.REMOVE) {
// disabling the run preprocessor button
this.runPreprocessorButton.setAttribute("disabled", true);
this.runPreprocessorButton.title = "No ControlNet input image available";
}
}).observe(this.inputImageContainer, {
childList: true,
subtree: true,
});
}
/**
* Observe send PNG info buttons in A1111, as they can also directly
* set states of ControlNetUnit.
*/
attachA1111SendInfoObserver() {
const pasteButtons = gradioApp().querySelectorAll('#paste');
const pngButtons = gradioApp().querySelectorAll(
this.isImg2Img ?
'#img2img_tab, #inpaint_tab' :
'#txt2img_tab'
);
for (const button of [...pasteButtons, ...pngButtons]) {
button.addEventListener('click', () => {
// The paste/send img generation info feature goes
// though gradio, which is pretty slow. Ideally we should
// observe the event when gradio has done the job, but
// that is not an easy task.
// Here we just do a 2 second delay until the refresh.
setTimeout(() => {
this.updateActiveState();
this.updateActiveUnitCount();
}, 2000);
});
}
}
attachPresetDropdownObserver() {
const presetDropDown = this.tab.querySelector('.cnet-preset-dropdown');
new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.removedNodes.length > 0) {
setTimeout(() => {
this.updateActiveState();
this.updateActiveUnitCount();
this.updateActiveControlType();
}, 1000);
return;
}
}
}).observe(presetDropDown, {
childList: true,
subtree: true,
});
}
}
gradioApp().querySelectorAll('#controlnet').forEach(accordion => {
if (cnetAllAccordions.has(accordion)) return;
accordion.querySelectorAll('.cnet-unit-tab')
.forEach(tab => new ControlNetUnitTab(tab, accordion));
cnetAllAccordions.add(accordion);
});
});
})();

View File

@@ -0,0 +1,17 @@
(function () {
var hasApplied = false;
onUiUpdate(function () {
if (!hasApplied) {
if (typeof window.applyZoomAndPanIntegration === "function") {
hasApplied = true;
window.applyZoomAndPanIntegration("#txt2img_controlnet", Array.from({ length: 20 }, (_, i) => `#txt2img_controlnet_ControlNet-${i}_input_image`));
window.applyZoomAndPanIntegration("#img2img_controlnet", Array.from({ length: 20 }, (_, i) => `#img2img_controlnet_ControlNet-${i}_input_image`));
window.applyZoomAndPanIntegration("#txt2img_controlnet", ["#txt2img_controlnet_ControlNet_input_image"]);
window.applyZoomAndPanIntegration("#img2img_controlnet", ["#img2img_controlnet_ControlNet_input_image"]);
//console.log("window.applyZoomAndPanIntegration applied.");
} else {
//console.log("window.applyZoomAndPanIntegration is not available.");
}
}
});
})();

View File

@@ -0,0 +1,33 @@
(function () {
const cnetModalRegisteredElements = new Set();
onUiUpdate(() => {
// Get all the buttons that open a modal
const btns = gradioApp().querySelectorAll(".cnet-modal-open");
// Get all the <span> elements that close a modal
const spans = document.querySelectorAll(".cnet-modal-close");
// For each button, add a click event listener that opens the corresponding modal
btns.forEach((btn) => {
if (cnetModalRegisteredElements.has(btn)) return;
cnetModalRegisteredElements.add(btn);
const modalId = btn.id.replace('cnet-modal-open-', '');
const modal = document.getElementById("cnet-modal-" + modalId);
btn.addEventListener('click', () => {
modal.style.display = "block";
});
});
// For each <span> element, add a click event listener that closes the corresponding modal
spans.forEach((span) => {
if (cnetModalRegisteredElements.has(span)) return;
cnetModalRegisteredElements.add(span);
const modal = span.parentNode;
span.addEventListener('click', () => {
modal.style.display = "none";
});
});
});
})();

View File

@@ -0,0 +1,152 @@
(function () {
async function checkEditorAvailable() {
const LOCAL_EDITOR_PATH = '/openpose_editor_index';
const REMOTE_EDITOR_PATH = 'https://huchenlei.github.io/sd-webui-openpose-editor/';
async function testEditorPath(path) {
const res = await fetch(path);
return res.status === 200 ? path : null;
}
// Use local editor if the user has the extension installed. Fallback
// onto remote editor if the local editor is not ready yet.
// See https://github.com/huchenlei/sd-webui-openpose-editor/issues/53
// for more details.
return await testEditorPath(LOCAL_EDITOR_PATH) || await testEditorPath(REMOTE_EDITOR_PATH);
}
const cnetOpenposeEditorRegisteredElements = new Set();
let editorURL = null;
function loadOpenposeEditor() {
// Simulate an `input` DOM event for Gradio Textbox component. Needed after you edit its contents in javascript, otherwise your edits
// will only visible on web page and not sent to python.
function updateInput(target) {
let e = new Event("input", { bubbles: true })
Object.defineProperty(e, "target", { value: target })
target.dispatchEvent(e);
}
function navigateIframe(iframe, editorURL) {
function getPathname(rawURL) {
try {
return new URL(rawURL).pathname;
} catch (e) {
return rawURL;
}
}
return new Promise((resolve) => {
const darkThemeParam = document.body.classList.contains('dark') ?
new URLSearchParams({ theme: 'dark' }).toString() :
'';
window.addEventListener('message', (event) => {
const message = event.data;
if (message['ready']) resolve();
}, { once: true });
if ((editorURL.startsWith("http") ? iframe.src : getPathname(iframe.src)) !== editorURL) {
iframe.src = `${editorURL}?${darkThemeParam}`;
// By default assume 5 second is enough for the openpose editor
// to load.
setTimeout(resolve, 5000);
} else {
// If no navigation is required, immediately return.
resolve();
}
});
}
const tabs = gradioApp().querySelectorAll('.cnet-unit-tab');
tabs.forEach(tab => {
if (cnetOpenposeEditorRegisteredElements.has(tab)) return;
cnetOpenposeEditorRegisteredElements.add(tab);
const generatedImageGroup = tab.querySelector('.cnet-generated-image-group');
const editButton = generatedImageGroup.querySelector('.cnet-edit-pose');
editButton.addEventListener('click', async () => {
const inputImageGroup = tab.querySelector('.cnet-input-image-group');
const inputImage = inputImageGroup.querySelector('.cnet-image img');
const downloadLink = generatedImageGroup.querySelector('.cnet-download-pose a');
const modalId = editButton.id.replace('cnet-modal-open-', '');
const modalIframe = generatedImageGroup.querySelector('.cnet-modal iframe');
if (!editorURL) {
editorURL = await checkEditorAvailable();
if (!editorURL) {
alert("No openpose editor available.")
}
}
await navigateIframe(modalIframe, editorURL);
modalIframe.contentWindow.postMessage({
modalId,
imageURL: inputImage ? inputImage.src : undefined,
poseURL: downloadLink.href,
}, '*');
// Focus the iframe so that the focus is no longer on the `Edit` button.
// Pressing space when the focus is on `Edit` button will trigger
// the click again to resend the frame message.
modalIframe.contentWindow.focus();
});
/*
* Writes the pose data URL to an link element on input image group.
* Click a hidden button to trigger a backend rendering of the pose JSON.
*
* The backend should:
* - Set the rendered pose image as preprocessor generated image.
*/
function updatePreviewPose(poseURL) {
const downloadLink = generatedImageGroup.querySelector('.cnet-download-pose a');
const renderButton = generatedImageGroup.querySelector('.cnet-render-pose');
const poseTextbox = generatedImageGroup.querySelector('.cnet-pose-json textarea');
const allowPreviewCheckbox = tab.querySelector('.cnet-allow-preview input');
if (!allowPreviewCheckbox.checked)
allowPreviewCheckbox.click();
// Only set href when download link exists and needs an update. `downloadLink`
// can be null when user closes preview and click `Upload JSON` button again.
// https://github.com/Mikubill/sd-webui-controlnet/issues/2308
if (downloadLink !== null)
downloadLink.href = poseURL;
poseTextbox.value = poseURL;
updateInput(poseTextbox);
renderButton.click();
}
// Updates preview image when edit is done.
window.addEventListener('message', (event) => {
const message = event.data;
const modalId = editButton.id.replace('cnet-modal-open-', '');
if (message.modalId !== modalId) return;
updatePreviewPose(message.poseURL);
const closeModalButton = generatedImageGroup.querySelector('.cnet-modal .cnet-modal-close');
closeModalButton.click();
});
const inputImageGroup = tab.querySelector('.cnet-input-image-group');
const uploadButton = inputImageGroup.querySelector('.cnet-upload-pose input');
// Updates preview image when JSON file is uploaded.
uploadButton.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file)
return;
const reader = new FileReader();
reader.onload = function (e) {
const contents = e.target.result;
const poseURL = `data:application/json;base64,${btoa(contents)}`;
updatePreviewPose(poseURL);
};
reader.readAsText(file);
// Reset the file input value so that uploading the same file still triggers callback.
event.target.value = '';
});
});
}
onUiUpdate(loadOpenposeEditor);
})();

View File

@@ -0,0 +1,435 @@
(function () {
/*
MIT LICENSE
Copyright 2011 Jon Leighton
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// From: https://gist.github.com/jonleighton/958841
function base64ArrayBuffer(arrayBuffer) {
var base64 = ''
var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
var bytes = new Uint8Array(arrayBuffer)
var byteLength = bytes.byteLength
var byteRemainder = byteLength % 3
var mainLength = byteLength - byteRemainder
var a, b, c, d
var chunk
// Main loop deals with bytes in chunks of 3
for (var i = 0; i < mainLength; i = i + 3) {
// Combine the three bytes into a single integer
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]
// Use bitmasks to extract 6-bit segments from the triplet
a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6
d = chunk & 63 // 63 = 2^6 - 1
// Convert the raw binary segments to the appropriate ASCII encoding
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]
}
// Deal with the remaining bytes and padding
if (byteRemainder == 1) {
chunk = bytes[mainLength]
a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2
// Set the 4 least significant bits to zero
b = (chunk & 3) << 4 // 3 = 2^2 - 1
base64 += encodings[a] + encodings[b] + '=='
} else if (byteRemainder == 2) {
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]
a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4
// Set the 2 least significant bits to zero
c = (chunk & 15) << 2 // 15 = 2^4 - 1
base64 += encodings[a] + encodings[b] + encodings[c] + '='
}
return base64
}
// Turn a base64 string into a blob.
// From https://gist.github.com/gauravmehla/7a7dfd87dd7d1b13697b6e894426615f
function b64toBlob(b64Data, contentType, sliceSize) {
var contentType = contentType || '';
var sliceSize = sliceSize || 512;
var byteCharacters = atob(b64Data);
var byteArrays = [];
for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
var slice = byteCharacters.slice(offset, offset + sliceSize);
var byteNumbers = new Array(slice.length);
for (var i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
var byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, { type: contentType });
}
function createBlackImageBase64(width, height) {
// Create a canvas element
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
// Get the context of the canvas
var ctx = canvas.getContext('2d');
// Fill the canvas with black color
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, width, height);
// Get the base64 encoded string
var base64Image = canvas.toDataURL('image/png');
return base64Image;
}
// Functions to be called within photopea context.
// Start of photopea functions
function pasteImage(base64image) {
app.open(base64image, null, /* asSmart */ true);
app.echoToOE("success");
}
function setLayerNames(names) {
const layers = app.activeDocument.layers;
if (layers.length !== names.length) {
console.error("layer length does not match names length");
echoToOE("error");
return;
}
for (let i = 0; i < names.length; i++) {
const layer = layers[i];
layer.name = names[i];
}
app.echoToOE("success");
}
function removeLayersWithNames(names) {
const layers = app.activeDocument.layers;
for (let i = 0; i < layers.length; i++) {
const layer = layers[i];
if (names.includes(layer.name)) {
layer.remove();
}
}
app.echoToOE("success");
}
function getAllLayerNames() {
const layers = app.activeDocument.layers;
const names = [];
for (let i = 0; i < layers.length; i++) {
const layer = layers[i];
names.push(layer.name);
}
app.echoToOE(JSON.stringify(names));
}
// Hides all layers except the current one, outputs the whole image, then restores the previous
// layers state.
function exportSelectedLayerOnly(format, layerName) {
// Gets all layers recursively, including the ones inside folders.
function getAllArtLayers(document) {
let allArtLayers = [];
for (let i = 0; i < document.layers.length; i++) {
const currentLayer = document.layers[i];
allArtLayers.push(currentLayer);
if (currentLayer.typename === "LayerSet") {
allArtLayers = allArtLayers.concat(getAllArtLayers(currentLayer));
}
}
return allArtLayers;
}
function makeLayerVisible(layer) {
let currentLayer = layer;
while (currentLayer != app.activeDocument) {
currentLayer.visible = true;
if (currentLayer.parent.typename != 'Document') {
currentLayer = currentLayer.parent;
} else {
break;
}
}
}
const allLayers = getAllArtLayers(app.activeDocument);
// Make all layers except the currently selected one invisible, and store
// their initial state.
const layerStates = [];
for (let i = 0; i < allLayers.length; i++) {
const layer = allLayers[i];
layerStates.push(layer.visible);
}
// Hide all layers to begin with
for (let i = 0; i < allLayers.length; i++) {
const layer = allLayers[i];
layer.visible = false;
}
for (let i = 0; i < allLayers.length; i++) {
const layer = allLayers[i];
const selected = layer.name === layerName;
if (selected) {
makeLayerVisible(layer);
}
}
app.activeDocument.saveToOE(format);
for (let i = 0; i < allLayers.length; i++) {
const layer = allLayers[i];
layer.visible = layerStates[i];
}
}
function hasActiveDocument() {
app.echoToOE(app.documents.length > 0 ? "true" : "false");
}
// End of photopea functions
const MESSAGE_END_ACK = "done";
const MESSAGE_ERROR = "error";
const PHOTOPEA_URL = "https://www.photopea.com/";
class PhotopeaContext {
constructor(photopeaIframe) {
this.photopeaIframe = photopeaIframe;
this.timeout = 1000;
}
navigateIframe() {
const iframe = this.photopeaIframe;
const editorURL = PHOTOPEA_URL;
return new Promise(async (resolve) => {
if (iframe.src !== editorURL) {
iframe.src = editorURL;
// Stop waiting after 10s.
setTimeout(resolve, 10000);
// Testing whether photopea is able to accept message.
while (true) {
try {
await this.invoke(hasActiveDocument);
break;
} catch (e) {
console.log("Keep waiting for photopea to accept message.");
}
}
this.timeout = 5000; // Restore to a longer timeout in normal messaging.
}
resolve();
});
}
// From https://github.com/huchenlei/stable-diffusion-ps-pea/blob/main/src/Photopea.ts
postMessageToPhotopea(message) {
return new Promise((resolve, reject) => {
const responseDataPieces = [];
let hasError = false;
const photopeaMessageHandle = (event) => {
if (event.source !== this.photopeaIframe.contentWindow) {
return;
}
// Filter out the ping messages
if (typeof event.data === 'string' && event.data.includes('MSFAPI#')) {
return;
}
// Ignore "done" when no data has been received. The "done" can come from
// MSFAPI ping.
if (event.data === MESSAGE_END_ACK && responseDataPieces.length === 0) {
return;
}
if (event.data === MESSAGE_END_ACK) {
window.removeEventListener("message", photopeaMessageHandle);
if (hasError) {
reject('Photopea Error.');
} else {
resolve(responseDataPieces.length === 1 ? responseDataPieces[0] : responseDataPieces);
}
} else if (event.data === MESSAGE_ERROR) {
responseDataPieces.push(event.data);
hasError = true;
} else {
responseDataPieces.push(event.data);
}
};
window.addEventListener("message", photopeaMessageHandle);
setTimeout(() => reject("Photopea message timeout"), this.timeout);
this.photopeaIframe.contentWindow.postMessage(message, "*");
});
}
// From https://github.com/huchenlei/stable-diffusion-ps-pea/blob/main/src/Photopea.ts
async invoke(func, ...args) {
await this.navigateIframe();
const message = `${func.toString()} ${func.name}(${args.map(arg => JSON.stringify(arg)).join(',')});`;
try {
return await this.postMessageToPhotopea(message);
} catch (e) {
throw `Failed to invoke ${func.name}. ${e}.`;
}
}
/**
* Fetch detected maps from each ControlNet units.
* Create a new photopea document.
* Add those detected maps to the created document.
*/
async fetchFromControlNet(tabs) {
if (tabs.length === 0) return;
const isImg2Img = tabs[0].querySelector('.cnet-unit-enabled').id.includes('img2img');
const generationType = isImg2Img ? 'img2img' : 'txt2img';
const width = gradioApp().querySelector(`#${generationType}_width input[type=number]`).value;
const height = gradioApp().querySelector(`#${generationType}_height input[type=number]`).value;
const layerNames = ["background"];
await this.invoke(pasteImage, createBlackImageBase64(width, height));
await new Promise(r => setTimeout(r, 200));
for (const [i, tab] of tabs.entries()) {
const generatedImage = tab.querySelector('.cnet-generated-image-group .cnet-image img');
if (!generatedImage) continue;
await this.invoke(pasteImage, generatedImage.src);
// Wait 200ms for pasting to fully complete so that we do not ended up with 2 separate
// documents.
await new Promise(r => setTimeout(r, 200));
layerNames.push(`unit-${i}`);
}
await this.invoke(removeLayersWithNames, layerNames);
await this.invoke(setLayerNames, layerNames.reverse());
}
/**
* Send the images in the active photopea document back to each ControlNet units.
*/
async sendToControlNet(tabs) {
// Gradio's image widgets are inputs. To set the image in one, we set the image on the input and
// force it to refresh.
function setImageOnInput(imageInput, file) {
// Createa a data transfer element to set as the data in the input.
const dt = new DataTransfer();
dt.items.add(file);
const list = dt.files;
// Actually set the image in the image widget.
imageInput.files = list;
// Foce the image widget to update with the new image, after setting its source files.
const event = new Event('change', {
'bubbles': true,
"composed": true
});
imageInput.dispatchEvent(event);
}
function sendToControlNetUnit(b64Image, index) {
const tab = tabs[index];
// Upload image to output image element.
const outputImage = tab.querySelector('.cnet-photopea-output');
const outputImageUpload = outputImage.querySelector('input[type="file"]');
setImageOnInput(outputImageUpload, new File([b64toBlob(b64Image, "image/png")], "photopea_output.png"));
// Make sure `UsePreviewAsInput` checkbox is checked.
const checkbox = tab.querySelector('.cnet-preview-as-input input[type="checkbox"]');
if (!checkbox.checked) {
checkbox.click();
}
}
const layerNames =
JSON.parse(await this.invoke(getAllLayerNames))
.filter(name => /unit-\d+/.test(name));
for (const layerName of layerNames) {
const arrayBuffer = await this.invoke(exportSelectedLayerOnly, 'PNG', layerName);
const b64Image = base64ArrayBuffer(arrayBuffer);
const layerIndex = Number.parseInt(layerName.split('-')[1]);
sendToControlNetUnit(b64Image, layerIndex);
}
}
}
let photopeaWarningShown = false;
function firstTimeUserPrompt() {
if (opts.controlnet_photopea_warning){
const photopeaPopupMsg = "you are about to connect to https://photopea.com\n" +
"- Click OK: proceed.\n" +
"- Click Cancel: abort.\n" +
"Photopea integration can be disabled in Settings > ControlNet > Disable photopea edit.\n" +
"This popup can be disabled in Settings > ControlNet > Photopea popup warning.";
if (photopeaWarningShown || confirm(photopeaPopupMsg)) photopeaWarningShown = true;
else return false;
}
return true;
}
const cnetRegisteredAccordions = new Set();
function loadPhotopea() {
function registerCallbacks(accordion) {
const photopeaMainTrigger = accordion.querySelector('.cnet-photopea-main-trigger');
// Photopea edit feature disabled.
if (!photopeaMainTrigger) {
console.log("ControlNet photopea edit disabled.");
return;
}
const closeModalButton = accordion.querySelector('.cnet-photopea-edit .cnet-modal-close');
const tabs = accordion.querySelectorAll('.cnet-unit-tab');
const photopeaIframe = accordion.querySelector('.photopea-iframe');
const photopeaContext = new PhotopeaContext(photopeaIframe, tabs);
tabs.forEach(tab => {
const photopeaChildTrigger = tab.querySelector('.cnet-photopea-child-trigger');
photopeaChildTrigger.addEventListener('click', async () => {
if (!firstTimeUserPrompt()) return;
photopeaMainTrigger.click();
if (await photopeaContext.invoke(hasActiveDocument) === "false") {
await photopeaContext.fetchFromControlNet(tabs);
}
});
});
accordion.querySelector('.photopea-fetch').addEventListener('click', () => photopeaContext.fetchFromControlNet(tabs));
accordion.querySelector('.photopea-send').addEventListener('click', () => {
photopeaContext.sendToControlNet(tabs)
closeModalButton.click();
});
}
const accordions = gradioApp().querySelectorAll('#controlnet');
accordions.forEach(accordion => {
if (cnetRegisteredAccordions.has(accordion)) return;
registerCallbacks(accordion);
cnetRegisteredAccordions.add(accordion);
});
}
onUiUpdate(loadPhotopea);
})();