ehristoforu's picture
Upload folder using huggingface_hub
0163a2c verified
/* Setup and navigation */
var photopeaWindow = null;
var photopeaIframe = null;
// Called by the iframe set up on photopea-tab.py.
function onPhotopeaLoaded(iframe) {
console.log("Photopea iFrame loaded");
photopeaWindow = iframe.contentWindow;
photopeaIframe = iframe;
// Clone some buttons to send the contents of galleries in txt2img, img2img and extras tabs
// to Photopea. You can also just copy-paste the images directly but these are the ones I
// use the most.
createSendToPhotopeaButton("image_buttons_txt2img", window.txt2img_gallery);
createSendToPhotopeaButton("image_buttons_img2img", window.img2img_gallery);
createSendToPhotopeaButton("image_buttons_extras", window.extras_gallery);
// Listen to the size slider changes.
gradioApp().getElementById("photopeaIframeSlider").addEventListener('input', (event) => {
// Get the value of the slider and parse it as an integer
const newHeight = parseInt(event.target.value);
// Update the height of the iframe
photopeaIframe.style.height = newHeight + 'px';
});
}
// Creates a button in one of the WebUI galleries that will get the currently selected image in the
// gallery.
// `queryId`: the id for the querySelector to search for the specific gallery list of buttons.
// `gallery`: the gallery div itself (cached by WebUI).
function createSendToPhotopeaButton(queryId, gallery) {
const existingButton = gradioApp().querySelector(`#${queryId} button`);
const newButton = existingButton.cloneNode(true);
newButton.style.display = "flex";
newButton.id = `${queryId}_open_in_photopea`;
newButton.title = "Send to Photopea"
newButton.textContent = "\u{1F99C}";
newButton.addEventListener("click", () => openImageInPhotopea(gallery));
existingButton.parentNode.appendChild(newButton);
}
// Switches to the "Photopea" tab by finding and clicking on the DOM button.
function goToPhotopeaTab() {
// Find Photopea tab button, as we don't know which order it might appear in.
const allButtons = gradioApp().querySelector('#tabs').querySelectorAll('button');
// The space after the name seems to be added automatically for some reason, so this is likely
// flaky across versions. We can't use "contains" because there's also "Send to Photopea"
// buttons.
photopeaTabButton = Array.from(allButtons).find(button => button.textContent === 'Photopea ');
photopeaTabButton.click();
}
// Navigates the UI to the "Inpaint Upload" tab under the img2img tab.
// Gradio will destroy and recreate parts of the UI when swapping tabs, so we wait for the page to
// be refreshed before trying to find the relevant bits.
function goToImg2ImgInpaintUpload(onFinished) {
// Start by swapping to the img2img tab.
switch_to_img2img();
const img2imgdiv = gradioApp().getElementById("mode_img2img");
waitForWebUiUpdate(img2imgdiv).then(() => {
const allButtons = img2imgdiv.querySelectorAll("div.tab-nav > button");
const inpaintButton =
Array.from(allButtons).find(button => button.textContent === 'Inpaint upload ');
inpaintButton.click();
onFinished();
});
}
/* Image transfer functions */
// Returns true if the "Active Layer Only" checkbox is ticked, false otherwise.
function activeLayerOnly() {
return gradioApp()
.getElementById("photopea-use-active-layer-only")
.querySelector("input[type=checkbox]").checked;
}
// Gets the currently selected image in a WebUI gallery and opens it in Photopea.
function openImageInPhotopea(originGallery) {
var imageSizeMatches = true;
const outgoingImg = originGallery.querySelectorAll("img")[0];
goToPhotopeaTab();
// First, check the image size to see if we have matching sizes. If it's bigger, we open it
// as a new document. Otherwise, we just append it to the current document as a new layer.
postMessageToPhotopea(getPhotopeaScriptString(getActiveDocumentSize)).then((response) => {
const activeDocSize = response[0].split(",");
if (outgoingImg.naturalWidth > activeDocSize[0] ||
outgoingImg.naturalHeight > activeDocSize[1]) {
imageSizeMatches = false;
}
blobTob64(outgoingImg.src, (imageData) => {
// Actually open the image, passing `imageSizeMatches` into Photopea's "open as new document" parameter.
postMessageToPhotopea(`app.open("${imageData}", null, ${imageSizeMatches});`, "*")
.then(() => {
if (imageSizeMatches) {
postMessageToPhotopea(`app.activeDocument.activeLayer.rasterize();`, "*");
} else {
postMessageToPhotopea(
`alert("New document created as the image sent is bigger than the active document");`,
"*");
}
});
});
});
}
// Requests the image from Photopea, converts the array result into a base64 png, then a blob, then
// actually send it to the WebUI.
function getAndSendImageToWebUITab(webUiTab, sendToControlnet, imageWidgetIndex) {
// Photopea only allows exporting the whole image, so in case "Active layer only" is selected in
// the UI, instead of just requesting the image to be saved, we also make all non-selected
// layers invisible.
const saveMessage = activeLayerOnly()
? getPhotopeaScriptString(exportSelectedLayerOnly)
: 'app.activeDocument.saveToOE("png");';
postMessageToPhotopea(saveMessage)
.then((resultArray) => {
// The first index of the payload is an ArrayBuffer of the image. We convert that to
// base64 string, then to blob, so it can be sent to a specific image widget in WebUI.
// There's likely a direct ArrayBuffer -> Blob conversion, but we're already using b64
// as an intermediate format.
const base64Png = base64ArrayBuffer(resultArray[0]);
sendImageToWebUi(
webUiTab,
sendToControlnet,
imageWidgetIndex,
b64toBlob(base64Png, "image/png"));
});
}
// Send image to a specific image widget in a Web UI tab. This basically navigates the DOM graph via
// queries, and magically presses buttons. You web developers sure work some dark magic.
function sendImageToWebUi(webUiTab, sendToControlNet, controlnetModelIndex, blob) {
const file = new File([blob], "photopea_output.png")
switch (webUiTab) {
case "txt2img":
switch_to_txt2img();
break;
case "img2img":
switch_to_img2img();
break;
case "extras":
switch_to_extras();
break;
}
if (sendToControlNet) {
// First, select the ControlNet accordion div.
const tabId = webUiTab === "txt2img"
? "#txt2img_script_container"
: "#img2img_script_container";
const controlNetDiv = gradioApp().querySelector(tabId).querySelector("#controlnet");
// Check if the ControlNet accordion is open by finding the image editing iFrames.
setImageOnControlNetInput(controlNetDiv, controlnetModelIndex, file);
} else {
// For regular tabs, it's less involved - we can simply set the image on input directly.
const imageInput = gradioApp().getElementById(`mode_${webUiTab}`).querySelector("input[type='file']");
setImageOnInput(imageInput, file);
}
}
// I couldn't figure out a way to inject a mask directly on an image widget. So to have an easy way
// of masking inpainting via selection, we send the image to "Inpaint Upload", and create a mask
// from selection.
function sendImageWithMaskSelectionToWebUi() {
// Start by verifying if there actually is a selection in the document.
postMessageToPhotopea(getPhotopeaScriptString(selectionExists))
.then((response) => {
if (response[0] === false) {
// In case there isn't, do an in-photopea alert (which is less intrusive but more
// visible).
postMessageToPhotopea(`alert("No selection in active document!");`);
} else {
// Let's start by swapping to the correct tab. This is a bit more involved due to
// Gradio's reconstruction of disabled UI elements.
goToImg2ImgInpaintUpload(() => {
// In case there is a selection, we'll pass a whole script payload to Photopea
// to create the mask and export it.
const fullMessage =
getPhotopeaScriptString(createMaskFromSelection) + // 1. Create the mask
getPhotopeaScriptString(exportSelectedLayerOnly) + // 2. Function that exports the image
`app.activeDocument.activeLayer.remove();`; // 3. Removes the temp mask layer
postMessageToPhotopea(fullMessage).then((resultArray) => {
// Set the mask.
const base64Png = base64ArrayBuffer(resultArray[0]);
const maskInput = gradioApp().getElementById("img_inpaint_mask").querySelector("input");
const blob = b64toBlob(base64Png, "image/png");
const file = new File([blob], "photopea_output.png");
setImageOnInput(maskInput, file);
// Now go in and get the actual image.
const saveMessage = activeLayerOnly()
? getPhotopeaScriptString(exportSelectedLayerOnly)
: 'app.activeDocument.saveToOE("png");';
postMessageToPhotopea(saveMessage)
.then((resultArray) => {
const base64Png = base64ArrayBuffer(resultArray[0]);
const baseImgInput = gradioApp().getElementById("img_inpaint_base").querySelector("input");
const blob = b64toBlob(base64Png, "image/png");
const file = new File([blob], "photopea_output.png");
setImageOnInput(baseImgInput, file);
});
});
});
}
});
}
// Navigates to the correct ControlNet model tab, then sets the image.
function setImageOnControlNetInput(controlNetDiv, controlNetModelIndex, file) {
if (controlNetAccordionIsCollapsed(controlNetDiv)) {
// The accordion is not open. Find the little icon arrow and click it (yes, if the arrow
// ever changes, this will break).
controlNetDiv.querySelector("span.icon").click();
}
waitForWebUiUpdate(controlNetDiv).then(() => {
// When more than one Controlnet model is enabled in the WebUI settings, there will be a
// series of Controlnet tabs. The one selected in the dropdown will be passed in by the
// `controlnetModelIndex`.
const tabs = controlNetDiv.querySelectorAll("div.tab-nav > button");
if (tabs !== null && tabs.length > 1) {
tabs[controlNetModelIndex].click();
}
// HACK: multiplying the index by 2 to match the proper input on the newest ControlNet extension
// was determined empirically and will likely break in the future (as all other DOM-based
// addressing tends to)
imageInput = controlNetDiv.querySelectorAll("input[type='file']")[controlNetModelIndex * 2];
setImageOnInput(imageInput, file);
}
);
}
// 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);
}
// Transforms a JS function body into a string that can be passed as a message to Photopea.
function getPhotopeaScriptString(func) {
return func.toString() + `${func.name}();`
}
// Posts a message and receives back a promise that will eventually return a 2-element array. One of
// them will be Photopea's "done" message, and the other the actual payload.
async function postMessageToPhotopea(message) {
var request = new Promise(function (resolve, reject) {
var responses = [];
var photopeaMessageHandle = function (response) {
responses.push(response.data);
// Photopea will first return the resulting data as a message to the parent window, then
// another message saying "done". When we receive the latter, we fulfill the promise.
if (response.data == "done") {
window.removeEventListener("message", photopeaMessageHandle);
resolve(responses)
}
};
// Add a listener to wait for Photopea's response messages.
window.addEventListener("message", photopeaMessageHandle);
});
// Actually execute the request to Photopea.
photopeaWindow.postMessage(message, "*");
return await request;
}
// Returns a promise that will be resolved when the div passed in the parameter is modified.
// This will happen when Gradio reconstructs the UI after, e.g., changing tabs.
async function waitForWebUiUpdate(divToWatch) {
const promise = new Promise((resolve, reject) => {
// Options for the observer (which mutations to observe)
const mutationConfig = { attributes: true, childList: true, subtree: true };
// Callback for when mutation happened. Will simply invoke the passed `onDivUpdated` and
// stop observing.
const onMutationHappened = (mutationList, observer) => {
observer.disconnect();
resolve();
}
const observer = new MutationObserver(onMutationHappened);
observer.observe(divToWatch, mutationConfig);
});
return await promise;
}
// Gradio keeps changing how their DOM works, so we just use some heuristic here to find out
// which child div is the one that contains the ControlNet image inputs. If that one is not
// displayed, we consider the accordion is closed. Other methods include direct indices, or
// checking if the arrow is tilted 90 degrees on the style, both seemed flakier.
function controlNetAccordionIsCollapsed(controlNetDiv) {
// Get the immediate children of the ControlNet accordion. One of them will contain the
// actual image widgets.
const directDescendents = controlNetDiv.children;
// All of the image iframes are contained within the same content div, so we can use any.
const sampleIframe = controlNetDiv.querySelectorAll("iframe")[0];
for(var i = 0; i < directDescendents.length; i++) {
if(directDescendents[i].contains(sampleIframe)) {
return directDescendents[i].style['display'] === 'none';
}
}
// As a fallback, to prevent constantly triggering the toggle in case future versions break
// this heuristic, we just return false.
return false;
}