| | (function () { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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 |
| |
|
| | |
| | for (var i = 0; i < mainLength; i = i + 3) { |
| | |
| | chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2] |
| |
|
| | |
| | a = (chunk & 16515072) >> 18 |
| | b = (chunk & 258048) >> 12 |
| | c = (chunk & 4032) >> 6 |
| | d = chunk & 63 |
| |
|
| | |
| | base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d] |
| | } |
| |
|
| | |
| | if (byteRemainder == 1) { |
| | chunk = bytes[mainLength] |
| |
|
| | a = (chunk & 252) >> 2 |
| |
|
| | |
| | b = (chunk & 3) << 4 |
| |
|
| | base64 += encodings[a] + encodings[b] + '==' |
| | } else if (byteRemainder == 2) { |
| | chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1] |
| |
|
| | a = (chunk & 64512) >> 10 |
| | b = (chunk & 1008) >> 4 |
| |
|
| | |
| | c = (chunk & 15) << 2 |
| |
|
| | base64 += encodings[a] + encodings[b] + encodings[c] + '=' |
| | } |
| |
|
| | return base64 |
| | } |
| |
|
| | |
| | |
| | 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) { |
| | |
| | var canvas = document.createElement('canvas'); |
| | canvas.width = width; |
| | canvas.height = height; |
| |
|
| | |
| | var ctx = canvas.getContext('2d'); |
| |
|
| | |
| | ctx.fillStyle = 'black'; |
| | ctx.fillRect(0, 0, width, height); |
| |
|
| | |
| | var base64Image = canvas.toDataURL('image/png'); |
| |
|
| | return base64Image; |
| | } |
| |
|
| | |
| | |
| | function pasteImage(base64image) { |
| | app.open(base64image, null, 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)); |
| | } |
| |
|
| | |
| | |
| | function exportSelectedLayerOnly(format, layerName) { |
| | |
| | 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); |
| | |
| | |
| | const layerStates = []; |
| | for (let i = 0; i < allLayers.length; i++) { |
| | const layer = allLayers[i]; |
| | layerStates.push(layer.visible); |
| | } |
| | |
| | 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"); |
| | } |
| | |
| |
|
| | 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; |
| | |
| | setTimeout(resolve, 10000); |
| |
|
| | |
| | while (true) { |
| | try { |
| | await this.invoke(hasActiveDocument); |
| | break; |
| | } catch (e) { |
| | console.log("Keep waiting for photopea to accept message."); |
| | } |
| | } |
| | this.timeout = 5000; |
| | } |
| | resolve(); |
| | }); |
| | } |
| |
|
| | |
| | postMessageToPhotopea(message) { |
| | return new Promise((resolve, reject) => { |
| | const responseDataPieces = []; |
| | let hasError = false; |
| | const photopeaMessageHandle = (event) => { |
| | if (event.source !== this.photopeaIframe.contentWindow) { |
| | return; |
| | } |
| | |
| | if (typeof event.data === 'string' && event.data.includes('MSFAPI#')) { |
| | return; |
| | } |
| | |
| | |
| | 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, "*"); |
| | }); |
| | } |
| |
|
| | |
| | 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}.`; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | async fetchFromControlNet(tabs) { |
| | if (tabs.length === 0) return; |
| | const isImg2Img = tabs[0].querySelector('.cnet-mask-upload').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); |
| | |
| | |
| | await new Promise(r => setTimeout(r, 200)); |
| | layerNames.push(`unit-${i}`); |
| | } |
| | await this.invoke(removeLayersWithNames, layerNames); |
| | await this.invoke(setLayerNames, layerNames.reverse()); |
| | } |
| |
|
| | |
| | |
| | |
| | async sendToControlNet(tabs) { |
| | |
| | |
| | function setImageOnInput(imageInput, file) { |
| | |
| | const dt = new DataTransfer(); |
| | dt.items.add(file); |
| | const list = dt.files; |
| |
|
| | |
| | imageInput.files = list; |
| |
|
| | |
| | const event = new Event('change', { |
| | 'bubbles': true, |
| | "composed": true |
| | }); |
| | imageInput.dispatchEvent(event); |
| | } |
| |
|
| | function sendToControlNetUnit(b64Image, index) { |
| | const tab = tabs[index]; |
| | |
| | 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")); |
| |
|
| | |
| | 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'); |
| | |
| | if (!photopeaMainTrigger) { |
| | console.log("ControlNet photopea edit disabled."); |
| | return; |
| | } |
| |
|
| | const closeModalButton = accordion.querySelector('.cnet-photopea-edit .cnet-modal-close'); |
| | const tabs = accordion.querySelectorAll('.controlnet .input-accordion'); |
| | 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); |
| | })(); |