ImageFX 0.3.5
// ==UserScript==
// @name ImageFX Downloader
// @namespace http://tampermonkey.net/
// @version 0.3.5
// @description Downloads ImageFX generations with prompt, seed and generation metadata
// @author You
// @match https://labs.google/fx*tools/image-fx*
// @icon https://www.google.com/s2/favicons?sz=64&domain=labs.google
// @grant none
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
enableAutoDownload: true, // Автоматическое скачивание всех генераций
enableManualDownload: true, // Ручное скачивание качает генерации с метаданными
enableWideInputField: true, // Широкое поле для промпта
imageFormat: 'jpg', // Варианты: 'jpg', 'png', 'webp'. Сохранение меты реализовано только для JPG.
imageQuality: 0.95 // Качество от 0.0 до 1.0 for jpeg/webp, ignored for png
};
const originalFetch = window.fetch;
const piexifScriptUrl = 'https://cdn.jsdelivr.net/npm/piexifjs';
let piexifLoaded = false;
const loadPiexif = () => {
if (piexifLoaded) return Promise.resolve(window.piexif);
return new Promise(resolve => {
const script = document.createElement('script');
script.src = piexifScriptUrl;
script.onload = () => {
piexifLoaded = true;
resolve(window.piexif);
};
document.head.appendChild(script);
});
};
const generateFilename = (seed) => {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const timestamp = `${year}-${month}-${day} ${hours}-${minutes}-${seconds}`;
return `ImageFX - ${timestamp} - ${seed}.${CONFIG.imageFormat}`;
// Примеры ниже. Главное сохранить .${CONFIG.imageFormat} в конце.
// return `Elle Enjoyer.${CONFIG.imageFormat}`;
// return `I FREAKING LOVE ELSA - ${timestamp}.${CONFIG.imageFormat}`;
// return `Хрю-хрю - ${month}-${day}.${CONFIG.imageFormat}`;
};
const createImageWithMetadata = async (imageData, metadata) => {
const dataUrl = imageData.startsWith('data:') ? imageData : `data:image/png;base64,${imageData}`;
const response = await fetch(dataUrl);
const blob = await response.blob();
const img = await createImageBitmap(blob);
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0);
// Only add EXIF metadata for JPEG format
if (CONFIG.imageFormat === 'jpg') {
const piexif = await loadPiexif();
const jpegDataUrl = canvas.toDataURL('image/jpeg', CONFIG.imageQuality);
const exifObj = piexif.load(jpegDataUrl);
const utf8Metadata = unescape(encodeURIComponent(JSON.stringify(metadata)));
exifObj['0th'][piexif.ImageIFD.ImageDescription] = utf8Metadata;
const exifBytes = piexif.dump(exifObj);
const newDataUrl = piexif.insert(exifBytes, jpegDataUrl);
return { dataUrl: newDataUrl, fileName: generateFilename(metadata.seed) };
} else {
const mimeType = `image/${CONFIG.imageFormat}`;
const dataUrl = canvas.toDataURL(mimeType, CONFIG.imageFormat !== 'png' ? CONFIG.imageQuality : undefined);
return { dataUrl, fileName: generateFilename(metadata.seed) };
}
};
const downloadImage = ({ dataUrl, fileName }) => {
const byteString = atob(dataUrl.split(',')[1]);
const buffer = new Uint8Array(byteString.length);
for (let i = 0; i < byteString.length; i++) {
buffer[i] = byteString.charCodeAt(i);
}
const mimeType = `image/${CONFIG.imageFormat}`;
const blob = new Blob([buffer], { type: mimeType });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = fileName;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(link.href);
};
if (CONFIG.enableAutoDownload) {
const processGeneratedImages = async (data) => {
if (!data?.imagePanels?.[0]?.generatedImages) return;
const { generatedImages, prompt } = data.imagePanels[0];
const batchSize = generatedImages.length;
for (let index = 0; index < batchSize; index++) {
const { encodedImage, seed } = generatedImages[index];
if (!encodedImage) continue;
const metadata = { prompt, seed, index, batchSize };
const imageData = await createImageWithMetadata(encodedImage, metadata);
downloadImage(imageData);
}
};
window.fetch = async function(input, init) {
const url = typeof input === 'string' ? input : input.url;
if (!url.includes('v1:runImageFx')) {
return originalFetch.apply(this, arguments);
}
const response = await originalFetch.apply(this, arguments);
try {
const data = await response.clone().json();
if (response.status === 200) {
processGeneratedImages(data);
}
} catch (err) {}
return response;
};
}
if (CONFIG.enableManualDownload) {
const findImageIndex = (images, targetSrc) => {
if (!targetSrc) return -1;
const targetPrefix = targetSrc.substring(0, 100);
return images.findIndex(img =>
img.src && img.src.substring(0, 100) === targetPrefix
);
};
const findImageContainer = (imgElement) => {
let container = imgElement;
let hasSwiper = false;
while (container && container !== document.body) {
if (container.classList?.contains('swiper')) {
hasSwiper = true;
break;
}
container = container.parentElement;
}
const xpath = hasSwiper ?
'/html/body/div/div/div/div/div[1]/div[1]/div/div/div[2]' :
'/html/body/div/div/div/div/div[1]/div[1]/div';
return {
container: document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue,
hasSwiper
};
};
const getImageMetadata = (imgElement) => {
if (!imgElement) return {};
const { container, hasSwiper } = findImageContainer(imgElement);
let allImages;
if (!container) {
let parentContainer = imgElement.parentElement;
for (let i = 0; i < 2; i++) {
parentContainer = parentContainer?.parentElement;
}
allImages = parentContainer ? Array.from(parentContainer.querySelectorAll('img')) : [imgElement];
} else {
allImages = hasSwiper ?
Array.from(container.querySelectorAll('img')) :
Array.from(container.children)
.filter(el => el.querySelector('img'))
.map(el => el.querySelector('img'));
}
const index = hasSwiper ?
findImageIndex(allImages, imgElement.src) :
allImages.indexOf(imgElement);
const promptElement = document.querySelector('span[data-slate-string="true"]');
const prompt = promptElement?.innerText.trim() || 'Unknown Prompt';
const seedInput = document.querySelector('#imagefx-seed-input');
const seed = seedInput?.matches('input[type="number"]') ?
seedInput.value :
-1;
return { prompt, seed, index, batchSize: allImages.length };
};
const findDisplayedImage = (buttonElement) => {
let current = buttonElement;
let depth = 0;
while (current && depth < 6) {
const img = current.querySelector('img');
if (img) return img;
current = current.parentElement;
depth++;
}
return null;
};
const handleManualDownload = async (e) => {
e.preventDefault();
e.stopPropagation();
const imgElement = findDisplayedImage(e.currentTarget);
if (!imgElement) {
console.warn("Image element not found.");
return;
}
const metadata = getImageMetadata(imgElement);
const response = await fetch(imgElement.src);
const blob = await response.blob();
const img = await createImageBitmap(blob);
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0);
let imageData;
if (CONFIG.imageFormat === 'jpg') {
imageData = await createImageWithMetadata(
canvas.toDataURL('image/jpeg', CONFIG.imageQuality),
metadata
);
} else {
const mimeType = `image/${CONFIG.imageFormat}`;
const dataUrl = canvas.toDataURL(mimeType, CONFIG.imageFormat !== 'png' ? CONFIG.imageQuality : undefined);
imageData = { dataUrl, fileName: generateFilename(metadata.seed) };
}
downloadImage(imageData);
};
const overrideDownloadButtons = () => {
document.querySelectorAll("button").forEach(button => {
if (button.textContent.includes("download") && !button.dataset.overridden) {
button.dataset.overridden = "true";
button.addEventListener("click", handleManualDownload, true);
}
});
};
const observer = new MutationObserver(overrideDownloadButtons);
observer.observe(document.body, { childList: true, subtree: true });
overrideDownloadButtons();
}
if (CONFIG.enableWideInputField) {
const applyWideStyling = () => {
const xpathSelector = '/html/body/div/div[1]/div/div/div[1]/div[2]';
const xpathResult = document.evaluate(xpathSelector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
const element = xpathResult.singleNodeValue;
if (element) element.style.maxWidth = '100%';
};
applyWideStyling();
}
})();