// ==UserScript== // @name Huggingface Image Downloader // @description Add buttons to quickly download images from Stable Diffusion // @author Isaiah Odhner // @namespace https://isaiahodhner.io // @version 1.1 // @license MIT // @match https://*.hf.space/* // @icon https://www.google.com/s2/favicons?sz=64&domain=huggingface.co // @grant none // @downloadURL none // ==/UserScript== // v1.1 adds support for Stable Diffusion 2. It expands the domain scope to match other applications on HuggingFace, and includes negative prompts in the filename, with format: "'' (anti '')". let foundGrid = false; setInterval(() => { // assuming prompt comes before negative prompt in DOM const input = document.querySelector('#prompt-text-input input, [name=prompt], [placeholder*="prompt"]'); const negativeInput = document.querySelector('#negative-prompt-text-input input, [name=negative-prompt], [placeholder*="negative prompt"]'); const dlButtons = []; for (const img of document.querySelectorAll(".grid img")) { const existingA = img.parentElement.querySelector("a"); if (existingA) { if (existingA._imgSrc !== img.src) { existingA.remove(); const index = dlButtons.indexOf(existingA); if (index > -1) { dlButtons.splice(index); } } else { continue; // don't add a duplicate or change the supposed prompt it was generated with } } const a = document.createElement("a"); a.style.position = "absolute"; a.style.opacity = "0"; a.style.top = "0"; a.style.left = "0"; a.style.background = "black"; a.style.color = "white"; a.style.borderRadius = "5px"; a.style.padding = "5px"; a.style.margin = "5px"; a.style.fontSize = "50px"; a.style.lineHeight = "50px"; a.textContent = "Download"; a._imgSrc = img.src; let filename = `'${sanitizeFilename(input.value)}'`; if (negativeInput) { filename += ` (anti '${sanitizeFilename(negativeInput.value)}')`; } filename += ".jpeg"; a.download = filename; a.href = img.src; img.parentElement.append(a); dlButtons.push(a); // Can't be delegated because it needs to stop the click event from bubbling up to the handler that zooms in a.addEventListener("click", (event) => { // Prevent also zooming into the image when clicking Download event.stopImmediatePropagation(); }); } const grid = document.querySelector(".grid"); if (grid && !foundGrid) { foundGrid = true; grid.addEventListener("mouseover", (event) => { for (const a of dlButtons) { a.style.opacity = "0"; } const cell = event.target.closest(".gallery-item"); if (cell) { cell.querySelector("a[download]").style.opacity = "1"; } }); grid.addEventListener("mouseout", (event) => { if (event.target instanceof HTMLImageElement) { const cell = event.target.closest(".gallery-item"); const newCell = event.relatedTarget.closest(".gallery-item"); if (cell === newCell) return; cell.querySelector("a[download]").style.opacity = "0"; } }); document.addEventListener("mouseleave", (event) => { for (const a of document.querySelectorAll(".gallery-item a[download]")) { a.style.opacity = "0"; } }); } }, 300); function sanitizeFilename(str) { // Sanitize for file name, replacing symbols rather than removing them str = str.replace(/\//g, "⧸"); str = str.replace(/\\/g, "⧹"); str = str.replace(//g, "ᐳ"); str = str.replace(/:/g, "꞉"); str = str.replace(/\|/g, "∣"); str = str.replace(/\?/g, "?"); str = str.replace(/\*/g, "∗"); str = str.replace(/(^|[-—\s(\["])'/g, "$1\u2018"); // opening singles str = str.replace(/'/g, "\u2019"); // closing singles & apostrophes str = str.replace(/(^|[-—/\[(‘\s])"/g, "$1\u201c"); // opening doubles str = str.replace(/"/g, "\u201d"); // closing doubles str = str.replace(/--/g, "\u2014"); // em-dashes str = str.replace(/\.\.\./g, "…"); // ellipses str = str.replace(/~/g, "\u301C"); // Chrome at least doesn't like tildes str = str.trim(); return str; }