// ==UserScript==
// @name AI 이미지 EXIF 뷰어
// @namespace https://gf.qytechs.cn/users/815641
// @match https://www.pixiv.net/artworks/*
// @match https://arca.live/b/aiart/*
// @match https://arca.live/b/hypernetworks/*
// @match https://arca.live/b/aiartreal/*
// @match https://arca.live/b/aireal/*
// @version 1.6.0
// @author 우흐
// @require https://gf.qytechs.cn/scripts/452821-upng-js/code/UPNGjs.js?version=1103227
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/exif-library.min.js
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/clipboard.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/arrive/2.4.1/arrive.min.js
// @require https://gf.qytechs.cn/scripts/421384-gm-fetch/code/GM_fetch.js
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @description AI 이미지 메타데이터 보기
// @license MIT
// ==/UserScript==
(async function () {
"use strict";
try {
if (typeof GM_registerMenuCommand == undefined) {
return;
} else {
GM_registerMenuCommand("Pixiv 뷰어 사용 토글", function () {
if (GM_getValue("usePixiv", false)) {
GM_setValue("usePixiv", false);
Swal.fire({
toast: true,
position: "bottom",
showConfirmButton: false,
timer: 2000,
icon: "error",
title: "Pixiv 비활성화",
});
} else {
GM_setValue("usePixiv", true);
Swal.fire({
toast: true,
position: "bottom",
showConfirmButton: false,
timer: 2000,
icon: "success",
title: "Pixiv 활성화",
});
}
});
}
} catch (err) {
console.log(err);
}
if (location.href.match("/write")) {
document.arrive(".images-multi-upload", function () {
document.getElementById("saveExif").checked = true;
});
return;
}
unsafeWindow.toggle = function () {
const dots = document.getElementById("dots");
const moreText = document.getElementById("more");
const btnText = document.getElementById("moreBtn");
if (dots.style.display === "none") {
dots.style.display = "inline";
btnText.innerHTML = " 더 보기";
moreText.style.display = "none";
} else {
dots.style.display = "none";
btnText.innerHTML = "숨기기";
moreText.style.display = "inline";
}
};
function analyze(exif, src) {
try {
let prompt;
let negativePrompt;
let steps;
let sampler;
let cfgScale;
let seed;
let size;
let model;
let modelHash;
let software;
let rawData;
let negativePromptCopy;
const maxLength = 350;
if (exif.tabs?.tEXt?.Description || exif.tabs?.iTXt?.Description) {
rawData = `${exif.tabs.tEXt.Description}\n${exif.tabs.tEXt.Comment}`;
rawData = exif.tabs.tEXt.Description
? `${exif.tabs.tEXt.Description}\n${exif.tabs.tEXt.Comment}`
: `${exif.tabs.iTXt.Description}\n${exif.tabs.tEXt.Comment}`;
const comment = JSON.parse(exif.tabs.tEXt.Comment);
prompt =
(exif.tabs.tEXt?.Description
? exif.tabs.tEXt?.Description
: exif.tabs.iTXt?.Description) ?? "정보 없음";
negativePrompt = comment.uc ?? "정보 없음";
steps = comment.steps ?? "정보 없음";
sampler = comment.sampler ?? "정보 없음";
cfgScale = comment.scale ?? "정보 없음";
seed = comment.seed ?? "정보 없음";
size = `${exif.width}x${exif.height}` ?? "정보 없음";
model = "정보 없음";
modelHash = "정보 없음";
software = exif.tabs.tEXt.Software ?? "정보 없음";
negativePromptCopy = negativePrompt;
if (negativePrompt.length > maxLength) {
negativePrompt = `${negativePrompt.slice(
0,
maxLength
)}<span id="dots">...</span><span id="more">${negativePrompt.slice(
maxLength
)}</span> <button id="moreBtn" onclick="toggle();">더 보기</button>`;
}
} else {
let parameters = exif.replaceAll("<", "<").replaceAll(">", ">");
rawData = parameters;
if (!parameters.includes("Negative prompt")) {
parameters = parameters.replace("Steps", "\nNegative prompt: 정보 없음\nSteps");
}
parameters = parameters.split("Steps: ");
parameters = `${parameters[0]
.replaceAll(": ", ":")
.replace("Negative prompt:", "Negative prompt: ")}Steps: ${parameters[1]}`;
const commentStr = parameters.substring(parameters.indexOf("Steps"), parameters.length);
const keyValuePairs = commentStr.split(", ");
const comment = {};
for (const pair of keyValuePairs) {
const [key, value] = pair.split(": ");
comment[key] = value;
}
prompt =
parameters.indexOf("Negative prompt") === 0
? "정보 없음"
: parameters.substring(0, parameters.indexOf("Negative prompt:"));
negativePrompt = parameters
.substring(parameters.indexOf("Negative prompt:"), parameters.indexOf("Steps:"))
.replace("Negative prompt:", "");
steps = comment["Steps"] ?? "정보 없음";
sampler = comment["Sampler"] ?? "정보 없음";
cfgScale = comment["CFG scale"] ?? "정보 없음";
seed = comment["Seed"] ?? "정보 없음";
size = comment["Size"] ?? "정보 없음";
model = comment["Model"]
? `${comment["Model"]} [${comment["Model hash"]}]`
: comment["Model hash"] ?? "정보 없음";
modelHash = comment["Model hash"] ?? "정보 없음";
software = "Stable Diffusion web UI";
negativePromptCopy = negativePrompt;
if (negativePrompt.length > maxLength) {
negativePrompt = `${negativePrompt.slice(
0,
maxLength
)}<span id="dots">...</span><span id="more">${negativePrompt.slice(
maxLength
)}</span> <button id="moreBtn" onclick="toggle();">더 보기</button>`;
}
}
new ClipboardJS(".copy_btn");
Swal.fire({
title: "메타데이터 요약",
html: `
<style>
table{
border-collapse: collapse;
}
.modalTable {
border:1px solid #b3adad;
padding:5px;
font-size: 12px;
}
.modalTable td {
border:1px solid #b3adad;
text-align:left;
padding:5px;
}
.modalTable td.nowrap {
white-space:nowrap;
font-weight: bold;
}
.copy_btn {
border: 0;
border-radius: .25em;
background-color: #7066e0;
font-size: 1em;
color: #fff;
line-height: 1.5;
padding: .375rem .75rem;
cursor: pointer;
}
#moreBtn {
border: 0;
border-radius: .25em;
background-color: #82C3EC;
color: #fff;
padding: .175rem 0.55rem;
cursor: pointer;
}
#modelBtn {
border: 0;
border-radius: .25em;
background-color: #228be6;
color: #fff;
padding: .175rem 0.55rem;
line-height: 1.5;
font-size: 0.8em;
cursor: pointer;
}
a {
font-size: 15px;
}
#rawData > pre {
white-space: pre-wrap;
margin-bottom: 0;
}
#more {
display: none;
}
</style>
<table class="modalTable" width="100%">
<tbody>
<tr>
<td class="nowrap">Prompt</td>
<td id="prompt">${prompt}</td>
<td class="nowrap">
<button class="copy_btn" data-clipboard-target="#prompt">복사</button>
</td>
</tr>
<tr>
<td class="nowrap">Negative<br>Prompt</td>
<td id="negativePrompt">${negativePrompt}</td>
<td class="nowrap">
<button class="copy_btn" data-clipboard-text="${negativePromptCopy}">복사</button>
</td>
</tr>
<tr>
<td class="nowrap">Steps</td>
<td id="steps">${steps}</td>
<td class="nowrap">
<button class="copy_btn" data-clipboard-target="#steps">복사</button>
</td>
</tr>
<tr>
<td class="nowrap">Sampler</td>
<td id="sampler">${sampler}</td>
<td class="nowrap">
<button class="copy_btn" data-clipboard-target="#sampler">복사</button>
</td>
</tr>
<tr>
<td class="nowrap">CFG scale</td>
<td id="cfgScale">${cfgScale}</td>
<td class="nowrap">
<button class="copy_btn" data-clipboard-target="#cfgScale">복사</button>
</td>
</tr>
<tr>
<td class="nowrap">Seed</td>
<td id="seed">${seed}</td>
<td class="nowrap">
<button class="copy_btn" data-clipboard-target="#seed">복사</button>
</td>
</tr>
<tr>
<td class="nowrap">Size</td>
<td id="size">${size}</td>
<td class="nowrap">
<button class="copy_btn" data-clipboard-target="#size">복사</button>
</td>
</tr>
<tr>
<td class="nowrap">Model</td>
<td id="model">${model} <a href='https://civitai.com/?query=${modelHash}' target='_blank'><button id="modelBtn">CIVITAI</button></a></td>
<td class="nowrap">
<button class="copy_btn" data-clipboard-text="${modelHash}">복사</button>
</td>
</tr>
<tr>
<td class="nowrap">Software</td>
<td id="software">${software}</td>
<td class="nowrap">
<button class="copy_btn" data-clipboard-target="#software">복사</button>
</td>
</tr>
</tbody>
</table>
<a href="${src}" target="_blank">원본 링크</a>
`,
footer: `
<details>
<summary style="text-align: center;">원본 보기</summary>
<table class="modalTable" width="100%">
<tbody>
<tr>
<td id="rawData"><pre>${rawData}</pre></td>
<td class="nowrap">
<button class="copy_btn" data-clipboard-target="#rawData">복사</button>
</td>
</tr>
</tbody>
</table>
</details>
`,
width: "50rem",
confirmButtonText: "확인",
});
} catch (error) {
Swal.fire({
icon: "error",
title: "분석 오류",
html: `
${error}<br>
오류내용과 이미지를 댓글로 알려주세요`,
});
console.log(error);
}
}
function deepDanbooru(src) {
Swal.fire({
icon: "error",
title: "메타데이터 없음!",
text: "Deep Danbooru로 찾아볼까요?",
footer: `<a href="${src}" target="_blank">원본 링크</a>`,
showCancelButton: true,
confirmButtonText: "네",
cancelButtonText: "아니오",
showLoaderOnConfirm: true,
backdrop: true,
preConfirm: async () => {
return GM_fetch(`https://deepdanbooru.donmai.us/?url=${src}&min_score=0.4`)
.then((res) => {
if (!res.status === 200) {
Swal.showValidationMessage(`https://deepdanbooru.donmai.us 접속되는지 확인!`);
}
return res.json();
})
.catch((error) => {
console.log(error);
Swal.showValidationMessage(`https://deepdanbooru.donmai.us 접속되는지 확인!`);
});
},
allowOutsideClick: () => !Swal.isLoading(),
}).then((result) => {
if (result.isConfirmed) {
const tags = result.value.map((el) => el[0]).join(", ");
Swal.fire({
confirmButtonText: "닫기",
html: `
<style>
#tags {
width: 100%;
height: 250px;
padding: 10px;
background: #FFF;
color: black;
border-radius: 8px;
font-size: 15px;
resize:none;
}
#tags::-webkit-scrollbar{
display:none;
}
.copy_btn {
cursor: pointer;
}
</style>
<textarea id="tags">${tags}</textarea>
<span class="copy_btn" data-clipboard-target="#tags">여기 눌려 전체 복사</span>
`,
});
}
});
}
function blobToBase64(blob) {
return new Promise((resolve, _) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
}
async function extract(src, Referer) {
if (
src.includes(".png") ||
src.includes(".jpg") ||
src.includes(".jpeg") ||
src.includes(".webp")
) {
Swal.fire({
title: "로드 중!",
width: "15rem",
didOpen: () => {
Swal.showLoading();
},
});
const res = await GM_fetch(src, {
headers: { Referer },
});
const blob = await res.blob();
try {
const exif = exifLib.load(await blobToBase64(blob));
console.log(exif);
if (exif.Title === "AI generated image") {
const novelAi = await UPNG.decode(await blob.arrayBuffer());
console.log(novelAi);
analyze(novelAi, src);
} else if (exif.parameters) {
const png = exif.parameters;
console.log(png);
analyze(png, src);
} else if (exif.Exif[37510]) {
const jpgWebp = exif.Exif[37510].replace("UNICODE", "").replaceAll("\u0000", "");
console.log(jpgWebp);
analyze(jpgWebp, src);
} else {
deepDanbooru(src);
}
} catch (error) {
deepDanbooru(src);
}
} else {
Swal.fire({
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 1000,
timerProgressBar: true,
icon: "error",
title: "지원되지 않는 형식의 이미지",
});
}
}
if (GM_getValue("usePixiv", false) && location.href.match("pixiv.net")) {
let isAi = false;
document.arrive("footer > ul > li > span > a", function () {
if (this.href === "https://www.pixiv.help/hc/articles/11866167926809") {
isAi = true;
}
});
document.arrive("a > img", function () {
if (this.alt === "pixiv") return;
if (isAi) {
this.onclick = async () => {
const src = `${this.parentNode.href}`;
extract(src, "https://www.pixiv.net/");
document.arrive("div[role=presentation]:last-child > div > div", function () {
this.click();
});
};
}
});
}
if (location.href.match("arca.live")) {
document.arrive('a[href$="type=orig"] > img', function () {
if (this.classList.contains("channel-icon")) return;
this.parentNode.removeAttribute("href");
this.onclick = async () => {
const src = `${this.src}?type=orig`;
extract(src, "https://arca.live/");
};
});
}
})();