|
(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); |
|
})(); |