// ==UserScript==
// @name AI 이미지 EXIF 뷰어
// @namespace https://github.com/nyqui/AI-Image-EXIF-Viewer
// @match https://www.pixiv.net/*
// @match https://arca.live/b/aiart*
// @match https://arca.live/b/hypernetworks*
// @match https://arca.live/b/aiartreal*
// @match https://arca.live/b/aireal*
// @match https://arca.live/b/characterai*
// @version 2.1.1
// @author nyqui
// @require https://greasyfork.org/scripts/452821-upng-js/code/UPNGjs.js?version=1103227
// @require https://cdn.jsdelivr.net/npm/casestry-exif-library@2.0.3/dist/exif-library.min.js
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
// @require https://cdn.jsdelivr.net/npm/clipboard@2.0.10/dist/clipboard.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/arrive/2.4.1/arrive.min.js
// @require https://greasyfork.org/scripts/421384-gm-fetch/code/GM_fetch.js
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_download
// @description AI 이미지 메타데이터 보기
// @license MIT
// @downloadURL https://update.greasyfork.icu/scripts/464218/AI%20%EC%9D%B4%EB%AF%B8%EC%A7%80%20EXIF%20%EB%B7%B0%EC%96%B4.user.js
// @updateURL https://update.greasyfork.icu/scripts/464218/AI%20%EC%9D%B4%EB%AF%B8%EC%A7%80%20EXIF%20%EB%B7%B0%EC%96%B4.meta.js
// ==/UserScript==
//this URL must be changed manually to be linked properly
const scriptGreasyforkURL = "https://greasyfork.org/scripts/464214";
//toast timer in ms
const toastTimer = 3000;
const colorOption1 = "#5cc964";
const colorOption2 = "#ff9d0b";
const colorClose = "#b41b29";
const footerString = "
Prompt
${metadata.prompt ?? "정보 없음"}
Negative Prompt
${metadata.negativePrompt ?? "정보 없음"}
더 보기
Sampler
${metadata["Sampler"] ?? "정보 없음"}
Seed
${metadata["Seed"] ?? "정보 없음"}
Steps
${metadata["Steps"] ?? "정보 없음"}
Size
${metadata["Size"] ?? "정보 없음"}
CFG scale
${metadata["CFG scale"] ?? "정보 없음"}
Denoising strength
${metadata["Denoising strength"] ?? "정보 없음"}
${
metadata["Model"]
? `${metadata["Model"]} [${metadata["Model hash"]}]`
: metadata["Model hash"] ?? "정보 없음"
}
Infer...
${inferList.join(", ")}
`,
footer: /*html*/ `
Raw Metadata
${metadata.rawMetadata ?? "정보 없음"}
더 보기
${footerString}
`,
width: "50em",
showDenyButton: true,
showCancelButton: true,
focusCancel: true,
confirmButtonColor: `${colorOption1}`,
denyButtonColor: `${colorOption2}`,
cancelButtonColor: `${colorClose}`,
confirmButtonText: "이미지 열기",
denyButtonText: "이미지 저장",
cancelButtonText: "닫기"
})
// if image has URL, options are available to open in new tab or download
if (url != null) {
showMeta.fire().then((result) => {
if (result.isConfirmed) {
window.open(url, '_blank');
} else if (result.isDenied) {
GM_download(url, getFileName(url));
}
});
} else { // if image has no URL, then it must have been dragged and dropped, hence no open in new tab or download options
showMeta.fire({
showDenyButton: false,
showCancelButton: false,
focusCancel: false,
focusConfirm: true,
confirmButtonColor: `${colorClose}`,
confirmButtonText: "닫기",
});
};
showAndHide(".md-show-and-hide");
}
function showTagExtractionModal(url, blob) {
let noMeta = Swal.mixin({
footer: `
`
});
if (url == null) {
noMeta = Swal.mixin({
footer: `
${footerString}
`
});
};
function getOptimizedImageURL(url) {
if (isArca) {
return url.replace("ac.namu.la", "ac-o.namu.la").replace("&type=orig", "");
}
if (isPixiv) {
const extension = url.substring(url.lastIndexOf(".") + 1);
return url
.replace("/img-original/", "/c/600x1200_90_webp/img-master/")
.replace(`.${extension}`, "_master1200.jpg");
}
}
noMeta.fire({
icon: "error",
title: "메타데이터 없음!",
text: "찾아볼까요?",
showCancelButton: true,
showDenyButton: true,
confirmButtonText: "Danbooru Autotagger",
denyButtonText: "WD 1.4 Tagger",
cancelButtonText: "아니오",
showLoaderOnConfirm: true,
showLoaderOnDeny: true,
focusCancel: true,
confirmButtonColor: `${colorOption1}`,
denyButtonColor: `${colorOption2}`,
cancelButtonColor: `${colorClose}`,
backdrop: true,
preConfirm: async () => {
if (url != null) {
const res = await GM_fetch(getOptimizedImageURL(url), {
headers: {
Referer: `${location.protocol}//${location.hostname}`
},
});
blob = await res.blob();
};
let formData = new FormData();
formData.append('threshold', '0.4');
formData.append('format', 'json');
formData.append('file', blob);
return GM_fetch("https://autotagger.donmai.us/evaluate", {
method: "POST",
body: formData,
})
.then((res) => {
if (!res.status === 200) {
Swal.showValidationMessage(`https://autotagger.donmai.us 접속되는지 확인!`);
}
return res.json();
})
.catch((error) => {
console.log(error);
Swal.showValidationMessage(`https://autotagger.donmai.us 접속되는지 확인!`);
});
},
preDeny: async () => {
if (url != null) {
const res = await GM_fetch(getOptimizedImageURL(url), {
headers: {
Referer: `${location.protocol}//${location.hostname}`
},
});
blob = await res.blob();
};
const optimizedBase64 = await blobToBase64(blob);
return fetch("https://smilingwolf-wd-v1-4-tags.hf.space/run/predict", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
data: [optimizedBase64, "SwinV2", 0.35, 0.85],
}),
})
.then((res) => res.json())
.catch((error) => {
Swal.showValidationMessage(error);
});
},
allowOutsideClick: () => !Swal.isLoading(),
}).then((result) => {
if (result.isDismissed) return;
let tags;
if (result.isConfirmed) {
tags = Object.keys(result.value[0].tags).join(', ').replaceAll('_', ' ');
} else if (result.isDenied) {
tags = result.value.data[3]?.label ?
`${result.value.data[3]?.label}, ${result.value.data[0]}` :
result.value.data[0];
}
Swal.fire({
confirmButtonColor: `${colorClose}`,
confirmButtonText: "닫기",
html: /*html*/ `
Output
${tags}
`,
});
});
}
function fileToBlob(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(new Blob([reader.result], {
type: file.type
}));
reader.readAsArrayBuffer(file);
});
}
function blobToBase64(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
}
function notSupportedFormat() {
toastmix.fire({
position: "top-end",
icon: "error",
title: "지원하지 않는 파일 형식입니다.",
});
}
function isSupportedImageFormat(url) {
const supportedExtensions = /\.(png|jpe?g|webp)|image\/(jpeg|webp|png)/;
return supportedExtensions.test(url);
}
function handleUploadable(MIME) {
const uploadableSubtypes = /(jpe?g|jfif|pjp|png|gif|web[pm]|mov|mp4|m4[ab])/;
const [type, subtype] = MIME.split('/');
if (uploadableSubtypes.test(subtype)) {
return type;
} else {
return null;
}
}
async function extractImageMetadata(blob, type) {
try {
switch (type) {
case "image/jpeg":
case "image/webp": {
const exif = exifLib.load(await blobToBase64(blob));
const parameters = exif.Exif[37510].replace("UNICODE", "").replaceAll("\u0000", "");
return {
parameters
};
}
case "image/png": {
const chunks = UPNG.decode(await blob.arrayBuffer());
let parameters = chunks.tabs.tEXt?.parameters || chunks.tabs.iTXt?.parameters;
const description = chunks.tabs.tEXt?.Description || chunks.tabs.iTXt?.Description;
if (parameters) {
return {
parameters
};
} else if (description) {
return chunks.tabs?.tEXt || chunks.tabs?.iTXt;
} else {
return null;
}
}
}
} catch (error) {
console.log(error);
return null;
}
}
async function fetchAndDecode(url) {
try {
let response, contentType, reader;
const Referer = `${location.protocol}//${location.hostname}`;
if (isArca) {
response = await fetch(url.replace("ac.namu.la", "ac-o.namu.la"));
contentType = response.headers.get("content-type");
reader = response.body.getReader();
} else if (useTampermonkey) {
response = await new Promise((resolve) => {
GM_xmlhttpRequest({
url,
responseType: "stream",
headers: {
Referer
},
onreadystatechange: (data) => {
resolve(data);
},
});
});
const headers = Object.fromEntries(
response.responseHeaders.split("\n").map((line) => {
const [key, value] = line.split(":").map((part) => part.trim());
return [key, value];
})
);
contentType = headers["content-type"];
reader = response.response.getReader();
} else {
response = await GM_fetch(url, {
headers: {
Referer
},
});
contentType = response.headers.get("content-type");
reader = response.body.getReader();
}
if (
(isPixiv && !url.includes(".jpg") && contentType === "text/html") ||
(isPixiv && url.includes(".jpg"))
) {
url = url.replace(".png", ".jpg");
showTagExtractionModal(url);
return;
}
let metadata;
let chunks = [];
while (true) {
const {
done,
value
} = await reader.read();
if (done || metadata || metadata === null) {
reader.cancel();
break;
}
switch (contentType) {
case "image/jpeg":
metadata = getMetadataJPEGChunk(value);
break;
case "image/png":
metadata = getMetadataPNGChunk(value);
metadata?.IDAT && reader.cancel();
break;
case "image/webp":
chunks.push(value);
break;
default:
notSupportedFormat();
reader.cancel();
break;
}
}
if (contentType === "image/webp") {
const blob = new Blob(chunks, {
type: "image/webp"
});
const base64 = await blobToBase64(blob);
const exif = exifLib.load(base64);
const parameters = exif.Exif[37510].replace("UNICODE", "").replaceAll("\u0000", "");
metadata = {
parameters
};
}
return metadata;
} catch (error) {
console.log(error);
return null;
}
}
async function extract(url) {
if (!isSupportedImageFormat(url)) {
notSupportedFormat();
return;
}
Swal.fire({
title: "로드 중!",
width: "15rem",
didOpen: () => {
Swal.showLoading();
},
});
console.time("modal open");
console.time("fetch");
const metadata = await fetchAndDecode(url);
console.timeEnd("fetch");
console.log(metadata);
if (metadata?.Description || metadata?.parameters || metadata?.["sd-metadata"]) {
showMetadataModal(metadata, url);
} else {
showTagExtractionModal(url);
}
console.timeEnd("modal open");
}
function getCSRFToken() {
return new Promise(resolve => {
const csrf = document.querySelector("input[name=_csrf]")
const token = document.querySelector("input[name=token]")
if (csrf && token) {
resolve([csrf.value, token.value])
}
})
}
function uploadArca(blob, type, saveEXIF = true, token = null) {
return new Promise(async (resolve, reject) => {
let swalText = "비디오는 EXIF 보존 설정에 영향을 받지 않습니다.";
if (type == "image") {
swalText = "EXIF 보존: " + saveEXIF;
}
let xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", null, false);
let formData = new FormData();
if (!document.querySelector("#article_write_form > input[name=token]")) {
await getCSRFToken().then(tokenList => {
token = tokenList[1]
})
}
formData.append('upload', blob);
formData.append('token', token || document.querySelector("#article_write_form > input[name=token]").value);
formData.append('saveExif', saveEXIF);
formData.append('saveFilename', false);
xhr.onload = function() {
let response = JSON.parse(xhr.responseText)
if (response.uploaded === true) {
resolve(response.url)
} else {
Swal.close();
console.error(xhr.responseText);
toastmix.fire({
icon: "error",
title: `업로드 오류`,
});
}
}
xhr.open("POST", "https://arca.live/b/upload");
xhr.send(formData);
Swal.fire({
title: '파일 업로드중',
text: swalText,
showConfirmButton: false,
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading()
},
});
});
}
const {
hostname,
href,
pathname
} = location;
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const isPixiv = hostname === "www.pixiv.net";
const isArca = hostname === "arca.live";
const isArcaViewer = /(arca.live)(\/)(b\/.*)(\/)(\d*)/.test(href);
const isArcaEditor = /(arca.live\/b\/.*\/)(edit|write)/.test(href);
const useTampermonkey = GM_xmlhttpRequest?.RESPONSE_TYPE_STREAM && true;
const isPixivDragUpload = pathname === "/illustration/create" || pathname === "/upload.php";
if (GM_getValue("usePixiv", false) && isPixiv) {
function getOriginalUrl(url) {
const extension = url.substring(url.lastIndexOf(".") + 1);
const originalUrl = url
.replace("/c/600x1200_90_webp/img-master/", "/img-original/")
.replace("/c/100x100/img-master/", "/img-original/")
.replace("_master1200", "")
.replace(`.${extension}`, ".png");
return originalUrl;
}
let isAi = false;
if (!isMobile) {
document.arrive("footer > ul > li > span > a", function() {
if (this.href === "https://www.pixiv.help/hc/articles/11866167926809") isAi = true;
});
document.arrive("div[role=presentation]:last-child > div > div", function() {
isAi && this.click();
});
} else {
document.arrive("a.ai-generated", () => {
isAi = true;
});
document.arrive("button.nav-back", function() {
isAi && this.click();
});
}
document.arrive("a > img", function() {
if (this.alt === "pixiv") return;
if (isAi) {
let src;
if (!isMobile) {
src = this.parentNode.href;
} else {
src = getOriginalUrl(this.src);
}
this.onclick = function() {
extract(src);
};
}
});
}
if (isArcaViewer) {
document.arrive('a[href$="type=orig"] > img', {
existing: true
}, function() {
if (this.classList.contains("channel-icon")) return;
this.parentNode.onclick = (event) => {
if (event.button === 0) {
event.preventDefault();
}
};
this.onclick = function() {
const src = `${this.src}&type=orig`;
extract(src);
};
});
}
let ArcaDragUpload = true;
if (isArcaEditor) {
if (GM_getValue("saveExifDefault", true)) {
document.arrive(".images-multi-upload", {
onceOnly: true
}, () => {
document.getElementById("saveExif").checked = true;
});
}
if (!GM_getValue("useDragdropUpload", true)) ArcaDragUpload = false;
}
!isMobile && !isPixivDragUpload && ArcaDragUpload && new DropZone();
GM_addStyle(modalCSS);
new ClipboardJS(".md-copy");
registerMenu();
})();