// ==UserScript==
// @name AI 이미지 EXIF 뷰어
// @namespace https://gf.qytechs.cn/users/815641
// @match https://arca.live/b/aiart/*
// @version 1.3.6
// @author 우흐
// @require https://gf.qytechs.cn/scripts/452821-upng-js/code/UPNGjs.js?version=1103227
// @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
// @grant GM_xmlhttpRequest
// @description AI 이미지 메타데이터 보기
// @license MIT
// ==/UserScript==
(async function () {
"use strict";
const url = location.href;
const write = "https://arca.live/b/aiart/write";
if (url === write) {
document.arrive(".images-multi-upload", function () {
document.getElementById("saveExif").checked = true;
});
return;
}
function http(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
responseType: "blob",
url: url,
method: "GET",
onload: (response) => resolve(response.response),
onerror: reject,
});
});
}
function analyze(png) {
try {
let prompt;
let negativePrompt;
let steps;
let sampler;
let cfgScale;
let seed;
let size;
let denoisingStrength;
let software;
let rawData;
if (png.tabs.tEXt.Description) {
rawData = `"Prompt": ${png.tabs.tEXt.Description} ${png.tabs.tEXt.Comment}`;
const comment = JSON.parse(png.tabs.tEXt.Comment);
prompt = png.tabs.tEXt.Description ?? "정보 없음";
negativePrompt = comment.uc ?? "정보 없음";
steps = comment.steps ?? "정보 없음";
sampler = comment.sampler ?? "정보 없음";
cfgScale = comment.scale ?? "정보 없음";
seed = comment.seed ?? "정보 없음";
size = `${png.width}x${png.height}` ?? "정보 없음";
denoisingStrength = comment.strength ?? "정보 없음";
software = png.tabs.tEXt.Software ?? "정보 없음";
} else if (png.tabs.tEXt.parameters) {
rawData = png.tabs.tEXt.parameters;
let parameters = png.tabs.tEXt.parameters;
if (!parameters.includes("Negative prompt")) {
parameters = parameters.replace(
"Steps",
"\nNegative prompt: 정보 없음\nSteps"
);
}
const data = parameters.split(": ");
let temp = [];
data.forEach((el, i) => {
if (i < 2) {
temp = [...temp, el.substr(el.lastIndexOf("\n")).replace("\n", "")];
} else {
temp = [...temp, el.substr(el.lastIndexOf(",")).replace(", ", "")];
}
});
temp.pop();
let dataObj = {};
temp.forEach((el, i) => {
if (i === temp.length - 1) {
const arr = parameters
.substring(parameters.indexOf(temp[i]))
.split(": ");
const obj = {};
obj[`${arr[0]}`] = arr[1];
dataObj = { ...dataObj, ...obj };
} else {
const arr = parameters
.substring(
parameters.indexOf(temp[i]),
parameters.indexOf(temp[i + 1])
)
.split(": ");
const obj = {};
obj[`${arr[0]}`] = arr[1]?.replace(", ", "");
dataObj = { ...dataObj, ...obj };
}
});
if (parameters.indexOf("Negative prompt") === 0) {
prompt = "정보 없음";
} else {
prompt = data[0]?.replace("Negative prompt", "") ?? "정보 없음";
}
negativePrompt = dataObj["Negative prompt"] ?? "정보 없음";
steps = dataObj["Steps"] ?? "정보 없음";
sampler = dataObj["Sampler"] ?? "정보 없음";
cfgScale = dataObj["CFG scale"] ?? "정보 없음";
seed = dataObj["Seed"] ?? "정보 없음";
size = dataObj["Size"] ?? "정보 없음";
denoisingStrength = dataObj["Denoising strength"] ?? "정보 없음";
software = "Web UI 또는 기타...";
}
Swal.fire({
title: "메타데이터 요약",
html: `
<style>
.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;
}
</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 prompt</td>
<td id="negativePrompt">${negativePrompt}</td>
<td class="nowrap">
<button class="copy_btn" data-clipboard-target="#negativePrompt">복사</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">Denoising strength</td>
<td id="denoisingStrength">${denoisingStrength}</td>
<td class="nowrap">
<button class="copy_btn" data-clipboard-target="#denoisingStrength">복사</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>
`,
footer: `
<details>
<summary style="text-align: center;">원본 보기</summary>
<p>${rawData}}</p>
</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로 찾아볼까요?",
showCancelButton: true,
confirmButtonText: "네",
cancelButtonText: "아니오",
showLoaderOnConfirm: true,
backdrop: true,
preConfirm: async () => {
return http(`https://deepdanbooru.donmai.us/?url=${src}&min_score=0.4`)
.then((res) => {
return res.text();
})
.catch((error) => {
Swal.showValidationMessage(`Request failed: ${error}`);
});
},
allowOutsideClick: () => !Swal.isLoading(),
}).then((result) => {
const obj = JSON.parse(result.value);
if (result.isConfirmed) {
const tags = obj.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>
`,
});
}
});
}
document.arrive("a > img", function () {
if (this.classList.contains("channel-icon")) return;
this.parentNode.removeAttribute("href");
this.onclick = async () => {
const src = `${this.src}?type=orig`;
if (
src.includes(".png") ||
src.includes(".jpg") ||
src.includes(".jpeg")
) {
Swal.fire({
title: "로드 중!",
width: "15%",
didOpen: () => {
Swal.showLoading();
},
});
const res = await http(src);
console.log(res);
const buffer = await res.arrayBuffer();
try {
const png = await UPNG.decode(buffer);
new ClipboardJS(".copy_btn");
if (png?.tabs?.tEXt?.Description || png?.tabs?.tEXt?.parameters) {
analyze(png);
} else {
deepDanbooru(src);
}
} catch (error) {
deepDanbooru(src);
}
} else {
Swal.fire({
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 1000,
timerProgressBar: true,
icon: "error",
title: "지원되지 않는 형식의 이미지",
});
}
};
});
})();