AI 이미지 EXIF 뷰어

AI 이미지 메타데이터 보기

  1. // ==UserScript==
  2. // @name AI 이미지 EXIF 뷰어
  3. // @namespace https://github.com/nyqui/AI-Image-EXIF-Viewer
  4. // @match https://www.pixiv.net/*
  5. // @match https://arca.live/b/aiart*
  6. // @match https://arca.live/b/hypernetworks*
  7. // @match https://arca.live/b/aiartreal*
  8. // @match https://arca.live/b/aireal*
  9. // @match https://arca.live/b/characterai*
  10. // @version 2.1.1
  11. // @author nyqui
  12. // @require https://gf.qytechs.cn/scripts/452821-upng-js/code/UPNGjs.js?version=1103227
  13. // @require https://cdn.jsdelivr.net/npm/casestry-exif-library@2.0.3/dist/exif-library.min.js
  14. // @require https://cdn.jsdelivr.net/npm/sweetalert2@11
  15. // @require https://cdn.jsdelivr.net/npm/clipboard@2.0.10/dist/clipboard.min.js
  16. // @require https://cdnjs.cloudflare.com/ajax/libs/arrive/2.4.1/arrive.min.js
  17. // @require https://gf.qytechs.cn/scripts/421384-gm-fetch/code/GM_fetch.js
  18. // @grant GM_xmlhttpRequest
  19. // @grant GM_registerMenuCommand
  20. // @grant GM_setValue
  21. // @grant GM_getValue
  22. // @grant GM_addStyle
  23. // @grant GM_download
  24.  
  25. // @description AI 이미지 메타데이터 보기
  26. // @license MIT
  27. // ==/UserScript==
  28.  
  29. //this URL must be changed manually to be linked properly
  30. const scriptGreasyforkURL = "https://gf.qytechs.cn/scripts/464214";
  31. //toast timer in ms
  32. const toastTimer = 3000;
  33. const colorOption1 = "#5cc964";
  34. const colorOption2 = "#ff9d0b";
  35. const colorClose = "#b41b29";
  36.  
  37.  
  38. const footerString = "<div class=\"version\">v" + GM_info.script.version +
  39. " - <a href=\"" + scriptGreasyforkURL + "\" target=\"_blank\">Greasy Fork镜像</a> - <a href=\"" +
  40. GM_info.script.namespace + "\" target=\"_blank\">GitHub</a></div>";
  41.  
  42. (async function() {
  43. "use strict";
  44.  
  45. const modalCSS = /* css */ `
  46. font-family: -apple-system, BlinkMacSystemFont, NanumBarunGothic, NanumGothic, system-ui, sans-serif;
  47. .swal2-popup {
  48. font-size: 15px;
  49. }
  50. .swal2-actions {
  51. margin: .4em auto 0;
  52. }
  53. .swal2-footer{
  54. margin: 1em 1.6em .3em;
  55. padding: 1em 0 0;
  56. overflow: auto;
  57. font-size: 1.125em;
  58. }
  59. #dropzone {
  60. z-index: 100000000;
  61. display: none;
  62. position: fixed;
  63. width: 100%;
  64. height: 100%;
  65. left: 0;
  66. top: 0;
  67. }
  68.  
  69. .md-grid {
  70. display: grid;
  71. grid-template-rows: repeat(3, auto);
  72. text-align: left;
  73. }
  74.  
  75. .md-grid-item {
  76. border-bottom: 1px solid #b3b3b3;
  77. padding: .6em;
  78. }
  79.  
  80. .md-grid-item:last-child {
  81. border-bottom: 0px;
  82. }
  83.  
  84. .md-nested-grid {
  85. display: grid;
  86. grid-template-columns: repeat(3, 1fr);
  87. grid-template-rows: repeat(4, auto);
  88. gap: .5em;
  89. }
  90.  
  91. .md-title {
  92. line-height: 1em;
  93. font-weight: bold;
  94. font-size: .9em;
  95. padding-bottom: .2em;
  96. display: flex;
  97. color: #1A1A1A;
  98. }
  99.  
  100. .md-info {
  101. line-height: 1.5em;
  102. font-size: .8em;
  103. word-break: break-word;
  104. color: #444444;
  105. }
  106.  
  107. .md-hidden {
  108. overflow: hidden;
  109. position: relative;
  110. max-height: 5em;
  111. }
  112.  
  113. .md-hidden:after {
  114. content: "";
  115. position: absolute;
  116. bottom: 0;
  117. left: 0;
  118. width: 100%;
  119. height: 2em;
  120. background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4) 0%, white 100%);
  121. }
  122. .md-info > a{
  123. text-decoration: none;
  124. }
  125. .md-info > a:hover{
  126. text-decoration: underline !important;
  127. }
  128. pre.md-show-and-hide{
  129. font-family: monospace;
  130. margin: 0px;
  131. white-space: pre-line;
  132. }
  133.  
  134. .md-visible {
  135. height: auto;
  136. overflow: auto;
  137. }
  138.  
  139. .md-model {
  140. grid-column-start: 1;
  141. grid-column-end: 3;
  142. }
  143.  
  144. .md-show-more {
  145. text-align: center;
  146. cursor: pointer;
  147. }
  148.  
  149. #md-tags {
  150. width: 100%;
  151. height: 20em;
  152. padding-top: .5em;
  153. text-align: left;
  154. font-size: 0.9em;
  155. }
  156. span.md-button {
  157. margin-left: .15em;
  158. cursor: pointer;
  159. }
  160.  
  161. span.md-copy {
  162. content: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Crect width='16' height='16' stroke='none' fill='%23000000' opacity='0'/%3E%3Cg transform='matrix(0.6 0 0 0.6 8 8)' %3E%3Cpath style='stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;' transform=' translate(-12, -12)' d='M 4 2 C 2.895 2 2 2.895 2 4 L 2 18 L 4 18 L 4 4 L 18 4 L 18 2 L 4 2 z M 8 6 C 6.895 6 6 6.895 6 8 L 6 20 C 6 21.105 6.895 22 8 22 L 20 22 C 21.105 22 22 21.105 22 20 L 22 8 C 22 6.895 21.105 6 20 6 L 8 6 z M 8 8 L 20 8 L 20 20 L 8 20 L 8 8 z' stroke-linecap='round' /%3E%3C/g%3E%3C/svg%3E");
  163. }
  164.  
  165. span.md-civitai {
  166. content: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAllBMVEVHcEwORtARe/UMPdYPY+gOUuAKKc0RdvENT94LKc0LJMcLMM8LMc8SgPUSffIRePERcu4MN9EQbewQauoLKswLKswUIo0PZ+gWDmT///8LLtMPLKwQcvESA00LF4UiGWgKB2wNSuAMNM8LOtgPRb2XlLxDQY7Fw9kOW+cMO9NBNHG7utXR0OLv7vTc2+fe3utNSIs7N4PzUc8zAAAAFnRSTlMABvvJy9Kz18fsI9hVphtuSzKwKn11Da4jUwAAALdJREFUeNpNz8eWwjAMBVDZCQk9VMndTiGFPv//c3NimIG3ku5CRw8gZr1areETVuSc84L97ZvccCJu8k1c9ztjqHGuIWN2ewBIiRoX2ja4hiiNoAOepDxh0JQygLRHcZWXi3x47CcMYNqjv3Uh/LTiDeUIQrRPgeUbxFV2nbx7FyEp0frxqLdYJiMMgxb2fLZCD/UI26yqK7TWVXWVbOOvy4VSWiu1WP6XO86Umh3YV995ls1f0y8RAhFlMPQmQwAAAABJRU5ErkJggg==");
  167. }
  168.  
  169. .version {
  170. margin: 1px;
  171. text-align: right;
  172. font-size: .5em;
  173. font-style: italic;
  174. }
  175. `;
  176.  
  177. const toastmix = Swal.mixin({
  178. toast: true,
  179. position: "bottom",
  180. showConfirmButton: false,
  181. timer: `${toastTimer}`,
  182. timerProgressBar: true,
  183. });
  184.  
  185. function registerMenu() {
  186. try {
  187. if (typeof GM_registerMenuCommand == undefined) {
  188. return;
  189. } else {
  190. GM_registerMenuCommand("(로그인 필수) Pixiv 뷰어 사용 토글", () => {
  191. if (GM_getValue("usePixiv", false)) {
  192. GM_setValue("usePixiv", false);
  193. toastmix.fire({
  194. icon: "error",
  195. title: `Pixiv 비활성화
  196. 창이 닫힌 새로고침 됩니다`,
  197. didDestroy: () => {
  198. location.reload();
  199. },
  200. });
  201. } else {
  202. GM_setValue("usePixiv", true);
  203. toastmix.fire({
  204. icon: "success",
  205. title: `Pixiv 활성화
  206. 창이 닫힌 새로고침 됩니다`,
  207. didDestroy: () => {
  208. location.reload();
  209. },
  210. });
  211. }
  212. });
  213. GM_registerMenuCommand("아카라이브 EXIF 보존 토글", () => {
  214. if (GM_getValue("saveExifDefault", true)) {
  215. GM_setValue("saveExifDefault", false);
  216. toastmix.fire({
  217. icon: "error",
  218. title: `아카라이브 EXIF 보존 비활성화
  219. 다음번 작성시부터 버려집니다`,
  220. });
  221. } else {
  222. GM_setValue("saveExifDefault", true);
  223. toastmix.fire({
  224. icon: "success",
  225. title: `아카라이브 EXIF 보존 활성화
  226. 다음번 작성시부터 보존됩니다`,
  227. });
  228. }
  229. });
  230. GM_registerMenuCommand("아카라이브 글쓰기 창 스크립트 토글", () => {
  231. if (GM_getValue("useDragdropUpload", true)) {
  232. GM_setValue("useDragdropUpload", false);
  233. toastmix.fire({
  234. icon: "error",
  235. title: `아카 글쓰기 스크립트 비활성화
  236. 다음번 작성시부터 적용됩니다`,
  237. });
  238. } else {
  239. GM_setValue("useDragdropUpload", true);
  240. toastmix.fire({
  241. icon: "success",
  242. title: `아카 글쓰기 스크립트 활성화
  243. 다음번 작성시부터 적용됩니다`,
  244. });
  245. }
  246. });
  247. }
  248. } catch (err) {
  249. console.log(err);
  250. }
  251. }
  252.  
  253. class DropZone {
  254. constructor() {
  255. const dropZone = document.createElement("div");
  256. dropZone.setAttribute("id", "dropzone");
  257. document.body.appendChild(dropZone);
  258. this.dropZone = document.getElementById("dropzone");
  259. this.setupEventListeners();
  260. }
  261.  
  262. showDropZone() {
  263. this.dropZone.style.display = "block";
  264. }
  265.  
  266. hideDropZone() {
  267. this.dropZone.style.display = "none";
  268. }
  269.  
  270. allowDrag(e) {
  271. e.preventDefault();
  272. }
  273.  
  274. async handleDrop(e) {
  275. e.preventDefault();
  276. this.hideDropZone();
  277.  
  278. const file = e.dataTransfer.files[0];
  279. if (!file) return;
  280.  
  281. const blob = await fileToBlob(file);
  282. const type = blob.type;
  283. if (isArcaEditor) {
  284. const uploadableType = handleUploadable(type)
  285. let editor = document.querySelector('.write-body .fr-element')
  286. let saveEXIF = GM_getValue("saveExifDefault", true)
  287. if (uploadableType == "image") {
  288. try {
  289. saveEXIF = document.getElementById("saveExif").checked
  290. } catch {};
  291. uploadArca(blob, uploadableType, saveEXIF)
  292. .then(url => {
  293. editor.innerHTML = editor.innerHTML + `<p><img src="${url}" class="fr-fic fr-dii"></p><p><br></p>`
  294. Swal.close();
  295. })
  296. } else if (uploadableType == "video") {
  297. uploadArca(blob, uploadableType, false)
  298. .then(url => {
  299. editor.innerHTML = editor.innerHTML + `<p><span class="fr-video fr-dvi fr-draggable"><video class="fr-draggable" controls="" loop="" muted="" playsinline="" src="${url}">귀하의 브라우저는 html5 video 지원하지 않습니다.</video></span></p><p><br></p>`
  300. Swal.close();
  301. })
  302. } else {
  303. Swal.close();
  304. toastmix.fire({
  305. icon: "error",
  306. title: `업로드 오류:
  307. 업로드 있는 포맷이 아닙니다.`,
  308. });
  309. }
  310. } else {
  311. if (!isSupportedImageFormat(blob.type)) {
  312. notSupportedFormat();
  313. return;
  314. }
  315. const metadata = await extractImageMetadata(blob, type);
  316. metadata ? showMetadataModal(metadata) : showTagExtractionModal(null, blob);
  317. }
  318. }
  319.  
  320. setupEventListeners() {
  321. window.addEventListener("dragenter", () => this.showDropZone());
  322. this.dropZone.addEventListener("dragenter", (e) => this.allowDrag(e));
  323. this.dropZone.addEventListener("dragover", (e) => this.allowDrag(e));
  324. this.dropZone.addEventListener("dragleave", () => this.hideDropZone());
  325. this.dropZone.addEventListener("drop", (e) => this.handleDrop(e));
  326. }
  327. }
  328.  
  329. function getMetadataPNGChunk(chunk) {
  330. const isValidPNG = chunk
  331. .slice(0, 8)
  332. .every((byte, index) => [137, 80, 78, 71, 13, 10, 26, 10][index] === byte);
  333. if (!isValidPNG) {
  334. console.error("Invalid PNG");
  335. return null;
  336. }
  337.  
  338. const textDecoder = new TextDecoder("utf-8");
  339. let metadata = {};
  340.  
  341. function checkForChunks() {
  342. let position = 8;
  343. while (true) {
  344. const chunkLength = getUint32(position);
  345.  
  346. if (chunk.byteLength < position + chunkLength + 12) {
  347. return;
  348. }
  349. const name = String.fromCharCode(...chunk.subarray(position + 4, position + 8));
  350. const data = chunk.subarray(position + 8, position + chunkLength + 8);
  351. const dataString = textDecoder.decode(data);
  352.  
  353. if (name === "tEXt") {
  354. const [key, value] = dataString.split("\0");
  355. metadata[key] = value;
  356. } else if (name === "iTXt") {
  357. const [key, value] = dataString.split("\0\0\0\0\0");
  358. metadata[key] = value;
  359. } else if (name === "IDAT") {
  360. metadata[name] = true;
  361. return;
  362. }
  363. position += chunkLength + 12;
  364. }
  365. }
  366.  
  367. function getUint32(offset) {
  368. return (
  369. (chunk[offset] << 24) |
  370. (chunk[offset + 1] << 16) |
  371. (chunk[offset + 2] << 8) |
  372. chunk[offset + 3]
  373. );
  374. }
  375. checkForChunks();
  376. return metadata;
  377. }
  378.  
  379. function getMetadataJPEGChunk(chunk) {
  380. if (chunk[0] !== 255 || chunk[1] !== 216) { // 0xFF 0xD8
  381. console.error("Invalid JPEG");
  382. return null;
  383. }
  384. const textDecoder = new TextDecoder();
  385. let offset = 2;
  386. if (chunk[offset] === 0xff) {
  387. switch (chunk[offset + 1]) {
  388. case 0xe0: {
  389. offset += ((chunk[offset + 2] << 8) | chunk[offset + 3]) + 2;
  390. }
  391. case 0xe1: {
  392. const length = (chunk[offset + 2] << 8) | chunk[offset + 3];
  393. const data = chunk.subarray(offset + 4, offset + 2 + length);
  394. if (
  395. data[0] === 69 && //0x45 E
  396. data[1] === 120 && //0x78 x
  397. data[2] === 105 && //0x69 i
  398. data[3] === 102 && //0x66 f
  399. data[4] === 0 && // null
  400. data[5] === 0 // null
  401. ) {
  402. const userCommentData = data.subarray(46, offset + 2 + length);
  403. const parameters = textDecoder
  404. .decode(userCommentData)
  405. .replace("UNICODE", "")
  406. .replaceAll("\u0000", "");
  407. return {
  408. parameters
  409. };
  410. }
  411. }
  412. default:
  413. return null;
  414. }
  415. }
  416. return null;
  417. }
  418.  
  419. function getFileName(url) {
  420. if (url === "/") return;
  421. const fileName = url.split('?')[0];
  422. return fileName;
  423. }
  424.  
  425. function parseMetadata(exif) {
  426. try {
  427. let metadata = {};
  428. if (exif.parameters) {
  429. let parameters = exif.parameters.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
  430. metadata.rawMetadata = parameters;
  431.  
  432. if (!parameters.includes("Negative prompt")) {
  433. parameters = parameters.replace("Steps", "\nNegative prompt: 정보 없음\nSteps");
  434. }
  435.  
  436. parameters = parameters.split("Steps: ");
  437. parameters = `${parameters[0]
  438. .replaceAll(": ", ":")
  439. .replace("Negative prompt:", "Negative prompt: ")}Steps: ${parameters[1]}`;
  440.  
  441. const metadataStr = parameters.substring(parameters.indexOf("Steps"), parameters.length);
  442. const keyValuePairs = metadataStr.split(", ");
  443.  
  444. for (const pair of keyValuePairs) {
  445. const [key, value] = pair.split(": ");
  446. metadata[key] = value;
  447. }
  448.  
  449. metadata.prompt =
  450. parameters.indexOf("Negative prompt") === 0 ?
  451. "정보 없음" :
  452. parameters.substring(0, parameters.indexOf("Negative prompt:"));
  453. metadata.negativePrompt = parameters.includes("Negative prompt:") ?
  454. parameters
  455. .substring(parameters.indexOf("Negative prompt:"), parameters.indexOf("Steps:"))
  456. .replace("Negative prompt:", "") :
  457. null;
  458.  
  459. return metadata;
  460. } else if (exif.Description) {
  461. metadata.rawMetadata = `${exif.Description}\n${exif.Comment}`;
  462. const comment = JSON.parse(exif.Comment);
  463.  
  464. metadata.prompt = exif.Description;
  465. metadata.negativePrompt = comment.uc;
  466. metadata["Steps"] = comment.steps;
  467. metadata["Sampler"] = comment.sampler;
  468. metadata["CFG scale"] = comment.scale;
  469. metadata["Seed"] = comment.seed;
  470. metadata["Software"] = "NovelAI";
  471.  
  472. return metadata;
  473. } else if (exif["sd-metadata"]) {
  474. metadata.rawMetadata = exif["sd-metadata"];
  475. const parameters = JSON.parse(exif["sd-metadata"]);
  476. const rowPrompt = parameters.image.prompt[0].prompt;
  477. const PromptRegex = /[^[\]]+(?=\[|$)/g;
  478. const negativePromptRegex = /\[.*?\]/g;
  479. const promptArray = rowPrompt.match(PromptRegex);
  480. const negativePromptArray = rowPrompt.match(negativePromptRegex);
  481. const prompt = promptArray.map((prompt) => prompt.replace(/^\,|\,$/g, ""));
  482. const negativePrompt = negativePromptArray.map((prompt) =>
  483. prompt.replace(/^\[|\]$/g, "").replace(/^\,|\,$/g, "")
  484. );
  485.  
  486. metadata.prompt = prompt.join(", ");
  487. metadata.negativePrompt = negativePrompt.join(", ");
  488. metadata["Steps"] = parameters?.image.steps;
  489. metadata["Model"] = parameters?.model;
  490. metadata["Model hash"] = parameters?.model_hash;
  491. metadata["Sampler"] = parameters?.image.sampler;
  492. metadata["CFG scale"] = parameters?.image.cfg_scale;
  493. metadata["Seed"] = parameters?.image.seed;
  494. metadata["Size"] = `${parameters?.image.width}x${parameters?.image.height}`;
  495. metadata["Software"] = "InvokeAI";
  496.  
  497. return metadata;
  498. }
  499. } catch (error) {
  500. console.log(error);
  501. Swal.fire({
  502. icon: "error",
  503. confirmButtonColor: `${colorClose}`,
  504. confirmButtonText: "닫기",
  505. title: "분석 오류",
  506. html: `
  507. ${error}<br>
  508. 오류내용과 이미지를 댓글로 알려주세요`,
  509. });
  510. }
  511. }
  512.  
  513. function infer(metadata) {
  514. if (metadata?.Software) return [metadata.Software];
  515. const inferList = [];
  516. const denoising = metadata?.["Denoising strength"];
  517. const hires = metadata?.["Hires upscaler"];
  518.  
  519. inferList.push("T2I");
  520. if (denoising && !hires) {
  521. inferList[0] = "I2I";
  522. } else if (hires) {
  523. inferList.push("Hires. fix");
  524. }
  525. (metadata?.["AddNet Enabled"] ||
  526. metadata?.prompt?.includes("lora:") ||
  527. metadata?.negativePrompt?.includes("lora:")) &&
  528. inferList.push("LoRa");
  529. (metadata?.prompt?.includes("lyco:") ||
  530. metadata?.negativePrompt?.includes("lyco:")) &&
  531. inferList.push("LyCORIS");
  532. (metadata?.["Hypernet"] ||
  533. metadata?.prompt?.includes("hypernet:") ||
  534. metadata?.negativePrompt?.includes("hypernet:")) &&
  535. inferList.push("Hypernet");
  536.  
  537. const controlNetRegex = /(ControlNet)/;
  538. for (const key in metadata) {
  539. if (controlNetRegex.test(key)) {
  540. inferList.push("ControlNet");
  541. break;
  542. }
  543. }
  544. metadata?.["SD upscale upscaler"] && inferList.push("SD upscale");
  545. metadata?.["Ultimate SD upscale upscaler"] && inferList.push("Ultimate SD upscale");
  546. metadata?.["Latent Couple"] && inferList.push("Latent Couple");
  547. metadata?.["Dynamic thresholding enabled"] && inferList.push("Dynamic thresholding");
  548. metadata?.["LLuL Enabled"] && inferList.push("LLuL");
  549. metadata?.["Cutoff enabled"] && inferList.push("Cutoff");
  550. metadata?.["Tiled Diffusion"] && inferList.push("Tiled Diffusion");
  551. metadata?.["DDetailer model a"] && inferList.push("DDetailer");
  552. metadata?.["ADetailer version"] && inferList.push("ADetailer"); // DDetailer/ADetailer를 DINO 하나로 묶는 게 나을까?
  553.  
  554. return inferList;
  555. }
  556.  
  557. function showAndHide(elementSelector) {
  558. const contentEls = document.querySelectorAll(elementSelector);
  559.  
  560. contentEls.forEach((contentEl) => {
  561. const containerEl = contentEl.parentElement;
  562. const showMoreEl = containerEl.nextElementSibling;
  563.  
  564. if (contentEl.offsetHeight > containerEl.offsetHeight) {
  565. showMoreEl.style.display = "block";
  566. containerEl.classList.add("md-hidden");
  567. } else {
  568. showMoreEl.style.display = "none";
  569. containerEl.classList.remove("md-hidden");
  570. containerEl.classList.add("md-visible");
  571. }
  572.  
  573. showMoreEl.addEventListener("click", () => {
  574. const isMore = showMoreEl.textContent === "더 보기";
  575. showMoreEl.textContent = isMore ? "숨기기" : "더 보기";
  576. containerEl.classList.toggle("md-hidden", !isMore);
  577. containerEl.classList.toggle("md-visible", isMore);
  578. });
  579. });
  580. }
  581.  
  582. function showMetadataModal(metadata, url) {
  583. metadata = parseMetadata(metadata);
  584. const inferList = infer(metadata);
  585. const showMeta = Swal.mixin({
  586. title: "메타데이터 요약",
  587. html: /*html*/ `
  588. <div class="md-grid">
  589. <div class="md-grid-item">
  590. <div class="md-title">Prompt <span class="md-copy md-button" data-clipboard-target="#prompt"></span></div>
  591. <div class="md-info" id="prompt">
  592. ${metadata.prompt ?? "정보 없음"}
  593. </div>
  594. </div>
  595. <div class="md-grid-item">
  596. <div class="md-title">Negative Prompt
  597. <span class="md-copy md-button" data-clipboard-target="#negative-prompt"></span>
  598. </div>
  599. <div class="md-info">
  600. <div class="md-hidden">
  601. <div class="md-show-and-hide" id="negative-prompt">
  602. ${metadata.negativePrompt ?? "정보 없음"}
  603. </div>
  604. </div>
  605. <div class="md-show-more">더 보기</div>
  606. </div>
  607. </div>
  608. <div class="md-grid-item">
  609. <div class="md-nested-grid">
  610. <div>
  611. <div class="md-title">Sampler <span class="md-copy md-button" data-clipboard-target="#sampler"></span></div>
  612. <div class="md-info" id="sampler">${metadata["Sampler"] ?? "정보 없음"}</div>
  613. </div>
  614. <div>
  615. <div class="md-title">Seed <span class="md-copy md-button" data-clipboard-target="#seed"></span></div>
  616. <div class="md-info" id="seed">${metadata["Seed"] ?? "정보 없음"}</div>
  617. </div>
  618. <div>
  619. <div class="md-title">Steps <span class="md-copy md-button" data-clipboard-target="#steps"></span></div>
  620. <div class="md-info" id="steps">${metadata["Steps"] ?? "정보 없음"}</div>
  621. </div>
  622. <div>
  623. <div class="md-title">Size <span class="md-copy md-button" data-clipboard-target="#size"></span></div>
  624. <div class="md-info" id="size">${metadata["Size"] ?? "정보 없음"}</div>
  625. </div>
  626. <div>
  627. <div class="md-title">CFG scale <span class="md-copy md-button" data-clipboard-target="#cfg-scale"></span></div>
  628. <div class="md-info" id="cfg-scale">${metadata["CFG scale"] ?? "정보 없음"}</div>
  629. </div>
  630. <div>
  631. <div class="md-title">Denoising strength <span class="md-copy md-button" data-clipboard-target="#denoising-strength"></span></div>
  632. <div class="md-info" id="denoising-strength">${metadata["Denoising strength"] ?? "정보 없음"}</div>
  633. </div>
  634. <div class="md-model">
  635. <div class="md-title">Model
  636. <span class="md-copy md-button" data-clipboard-target="#model"></span>
  637. <a href='https://civitai.com/?query=${metadata["Model hash"]}' target='_blank'><span class="md-civitai md-button"></span></a>
  638. </div>
  639. <div class="md-info" id="model">${
  640. metadata["Model"]
  641. ? `${metadata["Model"]} [${metadata["Model hash"]}]`
  642. : metadata["Model hash"] ?? "정보 없음"
  643. }</div>
  644. </div>
  645. <div>
  646. <div class="md-title">Infer...</div>
  647. <div class="md-info">${inferList.join(", ")}</div>
  648. </div>
  649. </div>
  650. </div>
  651. `,
  652. footer: /*html*/ `
  653. <div class="md-grid-item">
  654. <div class="md-title">Raw Metadata <span class="md-copy md-button" data-clipboard-target="#raw-metadata"></span>
  655. </div>
  656. <div class="md-info">
  657. <div class="md-hidden">
  658. <pre class="md-show-and-hide" id="raw-metadata">
  659. ${metadata.rawMetadata ?? "정보 없음"}
  660. </pre>
  661. </div>
  662. <div class="md-show-more">더 보기</div>
  663. </div>
  664. ${footerString}
  665. </div>
  666. `,
  667. width: "50em",
  668. showDenyButton: true,
  669. showCancelButton: true,
  670. focusCancel: true,
  671. confirmButtonColor: `${colorOption1}`,
  672. denyButtonColor: `${colorOption2}`,
  673. cancelButtonColor: `${colorClose}`,
  674. confirmButtonText: "이미지 열기",
  675. denyButtonText: "이미지 저장",
  676. cancelButtonText: "닫기"
  677. })
  678.  
  679. // if image has URL, options are available to open in new tab or download
  680. if (url != null) {
  681. showMeta.fire().then((result) => {
  682. if (result.isConfirmed) {
  683. window.open(url, '_blank');
  684. } else if (result.isDenied) {
  685. GM_download(url, getFileName(url));
  686. }
  687. });
  688. } else { // if image has no URL, then it must have been dragged and dropped, hence no open in new tab or download options
  689. showMeta.fire({
  690. showDenyButton: false,
  691. showCancelButton: false,
  692. focusCancel: false,
  693. focusConfirm: true,
  694. confirmButtonColor: `${colorClose}`,
  695. confirmButtonText: "닫기",
  696. });
  697. };
  698. showAndHide(".md-show-and-hide");
  699. }
  700.  
  701. function showTagExtractionModal(url, blob) {
  702. let noMeta = Swal.mixin({
  703. footer: `
  704. <div style="width: 100%;">
  705. <div class="md-info" style="text-align: center;">
  706. <a href="${url}" target="_blank">Open image...</a>
  707. </div>
  708. ${footerString}
  709. </div>
  710. `
  711. });
  712. if (url == null) {
  713. noMeta = Swal.mixin({
  714. footer: `
  715. <div style="width: 100%;">
  716. ${footerString}
  717. </div>
  718. `
  719. });
  720. };
  721.  
  722. function getOptimizedImageURL(url) {
  723. if (isArca) {
  724. return url.replace("ac.namu.la", "ac-o.namu.la").replace("&type=orig", "");
  725. }
  726. if (isPixiv) {
  727. const extension = url.substring(url.lastIndexOf(".") + 1);
  728. return url
  729. .replace("/img-original/", "/c/600x1200_90_webp/img-master/")
  730. .replace(`.${extension}`, "_master1200.jpg");
  731. }
  732. }
  733. noMeta.fire({
  734. icon: "error",
  735. title: "메타데이터 없음!",
  736. text: "찾아볼까요?",
  737. showCancelButton: true,
  738. showDenyButton: true,
  739. confirmButtonText: "Danbooru Autotagger",
  740. denyButtonText: "WD 1.4 Tagger",
  741. cancelButtonText: "아니오",
  742. showLoaderOnConfirm: true,
  743. showLoaderOnDeny: true,
  744. focusCancel: true,
  745. confirmButtonColor: `${colorOption1}`,
  746. denyButtonColor: `${colorOption2}`,
  747. cancelButtonColor: `${colorClose}`,
  748. backdrop: true,
  749. preConfirm: async () => {
  750. if (url != null) {
  751. const res = await GM_fetch(getOptimizedImageURL(url), {
  752. headers: {
  753. Referer: `${location.protocol}//${location.hostname}`
  754. },
  755. });
  756. blob = await res.blob();
  757. };
  758. let formData = new FormData();
  759. formData.append('threshold', '0.4');
  760. formData.append('format', 'json');
  761. formData.append('file', blob);
  762.  
  763. return GM_fetch("https://autotagger.donmai.us/evaluate", {
  764. method: "POST",
  765. body: formData,
  766. })
  767. .then((res) => {
  768. if (!res.status === 200) {
  769. Swal.showValidationMessage(`https://autotagger.donmai.us 접속되는지 확인!`);
  770. }
  771. return res.json();
  772. })
  773. .catch((error) => {
  774. console.log(error);
  775. Swal.showValidationMessage(`https://autotagger.donmai.us 접속되는지 확인!`);
  776. });
  777. },
  778. preDeny: async () => {
  779. if (url != null) {
  780. const res = await GM_fetch(getOptimizedImageURL(url), {
  781. headers: {
  782. Referer: `${location.protocol}//${location.hostname}`
  783. },
  784. });
  785. blob = await res.blob();
  786. };
  787. const optimizedBase64 = await blobToBase64(blob);
  788.  
  789. return fetch("https://smilingwolf-wd-v1-4-tags.hf.space/run/predict", {
  790. method: "POST",
  791. headers: {
  792. "Content-Type": "application/json"
  793. },
  794. body: JSON.stringify({
  795. data: [optimizedBase64, "SwinV2", 0.35, 0.85],
  796. }),
  797. })
  798. .then((res) => res.json())
  799. .catch((error) => {
  800. Swal.showValidationMessage(error);
  801. });
  802. },
  803. allowOutsideClick: () => !Swal.isLoading(),
  804. }).then((result) => {
  805. if (result.isDismissed) return;
  806. let tags;
  807. if (result.isConfirmed) {
  808. tags = Object.keys(result.value[0].tags).join(', ').replaceAll('_', ' ');
  809. } else if (result.isDenied) {
  810. tags = result.value.data[3]?.label ?
  811. `${result.value.data[3]?.label}, ${result.value.data[0]}` :
  812. result.value.data[0];
  813. }
  814.  
  815. Swal.fire({
  816. confirmButtonColor: `${colorClose}`,
  817. confirmButtonText: "닫기",
  818. html: /*html*/ `
  819. <div class="md-title">Output
  820. <span class="md-copy md-button" data-clipboard-target="#md-tags"></span>
  821. </div>
  822. <div class="md-info" id="md-tags">${tags}</div>
  823. `,
  824. });
  825. });
  826. }
  827.  
  828. function fileToBlob(file) {
  829. return new Promise((resolve) => {
  830. const reader = new FileReader();
  831. reader.onload = () => resolve(new Blob([reader.result], {
  832. type: file.type
  833. }));
  834. reader.readAsArrayBuffer(file);
  835. });
  836. }
  837.  
  838. function blobToBase64(blob) {
  839. return new Promise((resolve) => {
  840. const reader = new FileReader();
  841. reader.onloadend = () => resolve(reader.result);
  842. reader.readAsDataURL(blob);
  843. });
  844. }
  845.  
  846. function notSupportedFormat() {
  847. toastmix.fire({
  848. position: "top-end",
  849. icon: "error",
  850. title: "지원하지 않는 파일 형식입니다.",
  851. });
  852. }
  853.  
  854. function isSupportedImageFormat(url) {
  855. const supportedExtensions = /\.(png|jpe?g|webp)|image\/(jpeg|webp|png)/;
  856. return supportedExtensions.test(url);
  857. }
  858.  
  859. function handleUploadable(MIME) {
  860. const uploadableSubtypes = /(jpe?g|jfif|pjp|png|gif|web[pm]|mov|mp4|m4[ab])/;
  861. const [type, subtype] = MIME.split('/');
  862. if (uploadableSubtypes.test(subtype)) {
  863. return type;
  864. } else {
  865. return null;
  866. }
  867. }
  868.  
  869. async function extractImageMetadata(blob, type) {
  870. try {
  871. switch (type) {
  872. case "image/jpeg":
  873. case "image/webp": {
  874. const exif = exifLib.load(await blobToBase64(blob));
  875. const parameters = exif.Exif[37510].replace("UNICODE", "").replaceAll("\u0000", "");
  876. return {
  877. parameters
  878. };
  879. }
  880. case "image/png": {
  881. const chunks = UPNG.decode(await blob.arrayBuffer());
  882. let parameters = chunks.tabs.tEXt?.parameters || chunks.tabs.iTXt?.parameters;
  883. const description = chunks.tabs.tEXt?.Description || chunks.tabs.iTXt?.Description;
  884. if (parameters) {
  885. return {
  886. parameters
  887. };
  888. } else if (description) {
  889. return chunks.tabs?.tEXt || chunks.tabs?.iTXt;
  890. } else {
  891. return null;
  892. }
  893. }
  894. }
  895. } catch (error) {
  896. console.log(error);
  897. return null;
  898. }
  899. }
  900.  
  901. async function fetchAndDecode(url) {
  902. try {
  903. let response, contentType, reader;
  904. const Referer = `${location.protocol}//${location.hostname}`;
  905. if (isArca) {
  906. response = await fetch(url.replace("ac.namu.la", "ac-o.namu.la"));
  907. contentType = response.headers.get("content-type");
  908. reader = response.body.getReader();
  909. } else if (useTampermonkey) {
  910. response = await new Promise((resolve) => {
  911. GM_xmlhttpRequest({
  912. url,
  913. responseType: "stream",
  914. headers: {
  915. Referer
  916. },
  917. onreadystatechange: (data) => {
  918. resolve(data);
  919. },
  920. });
  921. });
  922. const headers = Object.fromEntries(
  923. response.responseHeaders.split("\n").map((line) => {
  924. const [key, value] = line.split(":").map((part) => part.trim());
  925. return [key, value];
  926. })
  927. );
  928. contentType = headers["content-type"];
  929. reader = response.response.getReader();
  930. } else {
  931. response = await GM_fetch(url, {
  932. headers: {
  933. Referer
  934. },
  935. });
  936. contentType = response.headers.get("content-type");
  937. reader = response.body.getReader();
  938. }
  939. if (
  940. (isPixiv && !url.includes(".jpg") && contentType === "text/html") ||
  941. (isPixiv && url.includes(".jpg"))
  942. ) {
  943. url = url.replace(".png", ".jpg");
  944. showTagExtractionModal(url);
  945. return;
  946. }
  947.  
  948. let metadata;
  949. let chunks = [];
  950. while (true) {
  951. const {
  952. done,
  953. value
  954. } = await reader.read();
  955. if (done || metadata || metadata === null) {
  956. reader.cancel();
  957. break;
  958. }
  959. switch (contentType) {
  960. case "image/jpeg":
  961. metadata = getMetadataJPEGChunk(value);
  962. break;
  963. case "image/png":
  964. metadata = getMetadataPNGChunk(value);
  965. metadata?.IDAT && reader.cancel();
  966. break;
  967. case "image/webp":
  968. chunks.push(value);
  969. break;
  970. default:
  971. notSupportedFormat();
  972. reader.cancel();
  973. break;
  974. }
  975. }
  976. if (contentType === "image/webp") {
  977. const blob = new Blob(chunks, {
  978. type: "image/webp"
  979. });
  980. const base64 = await blobToBase64(blob);
  981. const exif = exifLib.load(base64);
  982. const parameters = exif.Exif[37510].replace("UNICODE", "").replaceAll("\u0000", "");
  983. metadata = {
  984. parameters
  985. };
  986. }
  987. return metadata;
  988. } catch (error) {
  989. console.log(error);
  990. return null;
  991. }
  992. }
  993.  
  994. async function extract(url) {
  995. if (!isSupportedImageFormat(url)) {
  996. notSupportedFormat();
  997. return;
  998. }
  999.  
  1000. Swal.fire({
  1001. title: "로드 중!",
  1002. width: "15rem",
  1003. didOpen: () => {
  1004. Swal.showLoading();
  1005. },
  1006. });
  1007.  
  1008. console.time("modal open");
  1009. console.time("fetch");
  1010. const metadata = await fetchAndDecode(url);
  1011. console.timeEnd("fetch");
  1012. console.log(metadata);
  1013.  
  1014. if (metadata?.Description || metadata?.parameters || metadata?.["sd-metadata"]) {
  1015. showMetadataModal(metadata, url);
  1016. } else {
  1017. showTagExtractionModal(url);
  1018. }
  1019. console.timeEnd("modal open");
  1020. }
  1021.  
  1022. function getCSRFToken() {
  1023. return new Promise(resolve => {
  1024. const csrf = document.querySelector("input[name=_csrf]")
  1025. const token = document.querySelector("input[name=token]")
  1026. if (csrf && token) {
  1027. resolve([csrf.value, token.value])
  1028. }
  1029. })
  1030. }
  1031.  
  1032. function uploadArca(blob, type, saveEXIF = true, token = null) {
  1033. return new Promise(async (resolve, reject) => {
  1034. let swalText = "비디오는 EXIF 보존 설정에 영향을 받지 않습니다.";
  1035. if (type == "image") {
  1036. swalText = "EXIF 보존: " + saveEXIF;
  1037. }
  1038. let xhr = new XMLHttpRequest();
  1039. xhr.upload.addEventListener("progress", null, false);
  1040. let formData = new FormData();
  1041. if (!document.querySelector("#article_write_form > input[name=token]")) {
  1042. await getCSRFToken().then(tokenList => {
  1043. token = tokenList[1]
  1044. })
  1045. }
  1046.  
  1047. formData.append('upload', blob);
  1048. formData.append('token', token || document.querySelector("#article_write_form > input[name=token]").value);
  1049. formData.append('saveExif', saveEXIF);
  1050. formData.append('saveFilename', false);
  1051.  
  1052. xhr.onload = function() {
  1053. let response = JSON.parse(xhr.responseText)
  1054. if (response.uploaded === true) {
  1055. resolve(response.url)
  1056. } else {
  1057. Swal.close();
  1058. console.error(xhr.responseText);
  1059. toastmix.fire({
  1060. icon: "error",
  1061. title: `업로드 오류`,
  1062. });
  1063. }
  1064. }
  1065. xhr.open("POST", "https://arca.live/b/upload");
  1066. xhr.send(formData);
  1067. Swal.fire({
  1068. title: '파일 업로드중',
  1069. text: swalText,
  1070. showConfirmButton: false,
  1071. allowOutsideClick: false,
  1072. didOpen: () => {
  1073. Swal.showLoading()
  1074. },
  1075. });
  1076. });
  1077. }
  1078.  
  1079. const {
  1080. hostname,
  1081. href,
  1082. pathname
  1083. } = location;
  1084. const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
  1085. const isPixiv = hostname === "www.pixiv.net";
  1086. const isArca = hostname === "arca.live";
  1087. const isArcaViewer = /(arca.live)(\/)(b\/.*)(\/)(\d*)/.test(href);
  1088. const isArcaEditor = /(arca.live\/b\/.*\/)(edit|write)/.test(href);
  1089. const useTampermonkey = GM_xmlhttpRequest?.RESPONSE_TYPE_STREAM && true;
  1090. const isPixivDragUpload = pathname === "/illustration/create" || pathname === "/upload.php";
  1091.  
  1092. if (GM_getValue("usePixiv", false) && isPixiv) {
  1093. function getOriginalUrl(url) {
  1094. const extension = url.substring(url.lastIndexOf(".") + 1);
  1095. const originalUrl = url
  1096. .replace("/c/600x1200_90_webp/img-master/", "/img-original/")
  1097. .replace("/c/100x100/img-master/", "/img-original/")
  1098. .replace("_master1200", "")
  1099. .replace(`.${extension}`, ".png");
  1100. return originalUrl;
  1101. }
  1102.  
  1103. let isAi = false;
  1104. if (!isMobile) {
  1105. document.arrive("footer > ul > li > span > a", function() {
  1106. if (this.href === "https://www.pixiv.help/hc/articles/11866167926809") isAi = true;
  1107. });
  1108. document.arrive("div[role=presentation]:last-child > div > div", function() {
  1109. isAi && this.click();
  1110. });
  1111. } else {
  1112. document.arrive("a.ai-generated", () => {
  1113. isAi = true;
  1114. });
  1115. document.arrive("button.nav-back", function() {
  1116. isAi && this.click();
  1117. });
  1118. }
  1119.  
  1120. document.arrive("a > img", function() {
  1121. if (this.alt === "pixiv") return;
  1122.  
  1123. if (isAi) {
  1124. let src;
  1125. if (!isMobile) {
  1126. src = this.parentNode.href;
  1127. } else {
  1128. src = getOriginalUrl(this.src);
  1129. }
  1130.  
  1131. this.onclick = function() {
  1132. extract(src);
  1133. };
  1134. }
  1135. });
  1136. }
  1137.  
  1138. if (isArcaViewer) {
  1139. document.arrive('a[href$="type=orig"] > img', {
  1140. existing: true
  1141. }, function() {
  1142. if (this.classList.contains("channel-icon")) return;
  1143.  
  1144. this.parentNode.onclick = (event) => {
  1145. if (event.button === 0) {
  1146. event.preventDefault();
  1147. }
  1148. };
  1149. this.onclick = function() {
  1150. const src = `${this.src}&type=orig`;
  1151. extract(src);
  1152. };
  1153. });
  1154. }
  1155.  
  1156. let ArcaDragUpload = true;
  1157. if (isArcaEditor) {
  1158. if (GM_getValue("saveExifDefault", true)) {
  1159. document.arrive(".images-multi-upload", {
  1160. onceOnly: true
  1161. }, () => {
  1162. document.getElementById("saveExif").checked = true;
  1163. });
  1164. }
  1165. if (!GM_getValue("useDragdropUpload", true)) ArcaDragUpload = false;
  1166. }
  1167.  
  1168. !isMobile && !isPixivDragUpload && ArcaDragUpload && new DropZone();
  1169. GM_addStyle(modalCSS);
  1170. new ClipboardJS(".md-copy");
  1171. registerMenu();
  1172. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址