// ==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 = "
v" + GM_info.script.version + " - Greasy Fork - GitHub
"; (async function() { "use strict"; const modalCSS = /* css */ ` font-family: -apple-system, BlinkMacSystemFont, NanumBarunGothic, NanumGothic, system-ui, sans-serif; .swal2-popup { font-size: 15px; } .swal2-actions { margin: .4em auto 0; } .swal2-footer{ margin: 1em 1.6em .3em; padding: 1em 0 0; overflow: auto; font-size: 1.125em; } #dropzone { z-index: 100000000; display: none; position: fixed; width: 100%; height: 100%; left: 0; top: 0; } .md-grid { display: grid; grid-template-rows: repeat(3, auto); text-align: left; } .md-grid-item { border-bottom: 1px solid #b3b3b3; padding: .6em; } .md-grid-item:last-child { border-bottom: 0px; } .md-nested-grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(4, auto); gap: .5em; } .md-title { line-height: 1em; font-weight: bold; font-size: .9em; padding-bottom: .2em; display: flex; color: #1A1A1A; } .md-info { line-height: 1.5em; font-size: .8em; word-break: break-word; color: #444444; } .md-hidden { overflow: hidden; position: relative; max-height: 5em; } .md-hidden:after { content: ""; position: absolute; bottom: 0; left: 0; width: 100%; height: 2em; background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4) 0%, white 100%); } .md-info > a{ text-decoration: none; } .md-info > a:hover{ text-decoration: underline !important; } pre.md-show-and-hide{ font-family: monospace; margin: 0px; white-space: pre-line; } .md-visible { height: auto; overflow: auto; } .md-model { grid-column-start: 1; grid-column-end: 3; } .md-show-more { text-align: center; cursor: pointer; } #md-tags { width: 100%; height: 20em; padding-top: .5em; text-align: left; font-size: 0.9em; } span.md-button { margin-left: .15em; cursor: pointer; } span.md-copy { 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"); } span.md-civitai { content: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAllBMVEVHcEwORtARe/UMPdYPY+gOUuAKKc0RdvENT94LKc0LJMcLMM8LMc8SgPUSffIRePERcu4MN9EQbewQauoLKswLKswUIo0PZ+gWDmT///8LLtMPLKwQcvESA00LF4UiGWgKB2wNSuAMNM8LOtgPRb2XlLxDQY7Fw9kOW+cMO9NBNHG7utXR0OLv7vTc2+fe3utNSIs7N4PzUc8zAAAAFnRSTlMABvvJy9Kz18fsI9hVphtuSzKwKn11Da4jUwAAALdJREFUeNpNz8eWwjAMBVDZCQk9VMndTiGFPv//c3NimIG3ku5CRw8gZr1areETVuSc84L97ZvccCJu8k1c9ztjqHGuIWN2ewBIiRoX2ja4hiiNoAOepDxh0JQygLRHcZWXi3x47CcMYNqjv3Uh/LTiDeUIQrRPgeUbxFV2nbx7FyEp0frxqLdYJiMMgxb2fLZCD/UI26yqK7TWVXWVbOOvy4VSWiu1WP6XO86Umh3YV995ls1f0y8RAhFlMPQmQwAAAABJRU5ErkJggg=="); } .version { margin: 1px; text-align: right; font-size: .5em; font-style: italic; } `; const toastmix = Swal.mixin({ toast: true, position: "bottom", showConfirmButton: false, timer: `${toastTimer}`, timerProgressBar: true, }); function registerMenu() { try { if (typeof GM_registerMenuCommand == undefined) { return; } else { GM_registerMenuCommand("(로그인 필수) Pixiv 뷰어 사용 토글", () => { if (GM_getValue("usePixiv", false)) { GM_setValue("usePixiv", false); toastmix.fire({ icon: "error", title: `Pixiv 비활성화 창이 닫힌 후 새로고침 됩니다`, didDestroy: () => { location.reload(); }, }); } else { GM_setValue("usePixiv", true); toastmix.fire({ icon: "success", title: `Pixiv 활성화 창이 닫힌 후 새로고침 됩니다`, didDestroy: () => { location.reload(); }, }); } }); GM_registerMenuCommand("아카라이브 EXIF 보존 토글", () => { if (GM_getValue("saveExifDefault", true)) { GM_setValue("saveExifDefault", false); toastmix.fire({ icon: "error", title: `아카라이브 EXIF 보존 비활성화 다음번 작성시부터 버려집니다`, }); } else { GM_setValue("saveExifDefault", true); toastmix.fire({ icon: "success", title: `아카라이브 EXIF 보존 활성화 다음번 작성시부터 보존됩니다`, }); } }); GM_registerMenuCommand("아카라이브 글쓰기 창 스크립트 토글", () => { if (GM_getValue("useDragdropUpload", true)) { GM_setValue("useDragdropUpload", false); toastmix.fire({ icon: "error", title: `아카 글쓰기 창 스크립트 비활성화 다음번 작성시부터 적용됩니다`, }); } else { GM_setValue("useDragdropUpload", true); toastmix.fire({ icon: "success", title: `아카 글쓰기 창 스크립트 활성화 다음번 작성시부터 적용됩니다`, }); } }); } } catch (err) { console.log(err); } } class DropZone { constructor() { const dropZone = document.createElement("div"); dropZone.setAttribute("id", "dropzone"); document.body.appendChild(dropZone); this.dropZone = document.getElementById("dropzone"); this.setupEventListeners(); } showDropZone() { this.dropZone.style.display = "block"; } hideDropZone() { this.dropZone.style.display = "none"; } allowDrag(e) { e.preventDefault(); } async handleDrop(e) { e.preventDefault(); this.hideDropZone(); const file = e.dataTransfer.files[0]; if (!file) return; const blob = await fileToBlob(file); const type = blob.type; if (isArcaEditor) { const uploadableType = handleUploadable(type) let editor = document.querySelector('.write-body .fr-element') let saveEXIF = GM_getValue("saveExifDefault", true) if (uploadableType == "image") { try { saveEXIF = document.getElementById("saveExif").checked } catch {}; uploadArca(blob, uploadableType, saveEXIF) .then(url => { editor.innerHTML = editor.innerHTML + `


` Swal.close(); }) } else if (uploadableType == "video") { uploadArca(blob, uploadableType, false) .then(url => { editor.innerHTML = editor.innerHTML + `


` Swal.close(); }) } else { Swal.close(); toastmix.fire({ icon: "error", title: `업로드 오류: 업로드 할 수 있는 포맷이 아닙니다.`, }); } } else { if (!isSupportedImageFormat(blob.type)) { notSupportedFormat(); return; } const metadata = await extractImageMetadata(blob, type); metadata ? showMetadataModal(metadata) : showTagExtractionModal(null, blob); } } setupEventListeners() { window.addEventListener("dragenter", () => this.showDropZone()); this.dropZone.addEventListener("dragenter", (e) => this.allowDrag(e)); this.dropZone.addEventListener("dragover", (e) => this.allowDrag(e)); this.dropZone.addEventListener("dragleave", () => this.hideDropZone()); this.dropZone.addEventListener("drop", (e) => this.handleDrop(e)); } } function getMetadataPNGChunk(chunk) { const isValidPNG = chunk .slice(0, 8) .every((byte, index) => [137, 80, 78, 71, 13, 10, 26, 10][index] === byte); if (!isValidPNG) { console.error("Invalid PNG"); return null; } const textDecoder = new TextDecoder("utf-8"); let metadata = {}; function checkForChunks() { let position = 8; while (true) { const chunkLength = getUint32(position); if (chunk.byteLength < position + chunkLength + 12) { return; } const name = String.fromCharCode(...chunk.subarray(position + 4, position + 8)); const data = chunk.subarray(position + 8, position + chunkLength + 8); const dataString = textDecoder.decode(data); if (name === "tEXt") { const [key, value] = dataString.split("\0"); metadata[key] = value; } else if (name === "iTXt") { const [key, value] = dataString.split("\0\0\0\0\0"); metadata[key] = value; } else if (name === "IDAT") { metadata[name] = true; return; } position += chunkLength + 12; } } function getUint32(offset) { return ( (chunk[offset] << 24) | (chunk[offset + 1] << 16) | (chunk[offset + 2] << 8) | chunk[offset + 3] ); } checkForChunks(); return metadata; } function getMetadataJPEGChunk(chunk) { if (chunk[0] !== 255 || chunk[1] !== 216) { // 0xFF 0xD8 console.error("Invalid JPEG"); return null; } const textDecoder = new TextDecoder(); let offset = 2; if (chunk[offset] === 0xff) { switch (chunk[offset + 1]) { case 0xe0: { offset += ((chunk[offset + 2] << 8) | chunk[offset + 3]) + 2; } case 0xe1: { const length = (chunk[offset + 2] << 8) | chunk[offset + 3]; const data = chunk.subarray(offset + 4, offset + 2 + length); if ( data[0] === 69 && //0x45 E data[1] === 120 && //0x78 x data[2] === 105 && //0x69 i data[3] === 102 && //0x66 f data[4] === 0 && // null data[5] === 0 // null ) { const userCommentData = data.subarray(46, offset + 2 + length); const parameters = textDecoder .decode(userCommentData) .replace("UNICODE", "") .replaceAll("\u0000", ""); return { parameters }; } } default: return null; } } return null; } function getFileName(url) { if (url === "/") return; const fileName = url.split('?')[0]; return fileName; } function parseMetadata(exif) { try { let metadata = {}; if (exif.parameters) { let parameters = exif.parameters.replaceAll("<", "<").replaceAll(">", ">"); metadata.rawMetadata = 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 metadataStr = parameters.substring(parameters.indexOf("Steps"), parameters.length); const keyValuePairs = metadataStr.split(", "); for (const pair of keyValuePairs) { const [key, value] = pair.split(": "); metadata[key] = value; } metadata.prompt = parameters.indexOf("Negative prompt") === 0 ? "정보 없음" : parameters.substring(0, parameters.indexOf("Negative prompt:")); metadata.negativePrompt = parameters.includes("Negative prompt:") ? parameters .substring(parameters.indexOf("Negative prompt:"), parameters.indexOf("Steps:")) .replace("Negative prompt:", "") : null; return metadata; } else if (exif.Description) { metadata.rawMetadata = `${exif.Description}\n${exif.Comment}`; const comment = JSON.parse(exif.Comment); metadata.prompt = exif.Description; metadata.negativePrompt = comment.uc; metadata["Steps"] = comment.steps; metadata["Sampler"] = comment.sampler; metadata["CFG scale"] = comment.scale; metadata["Seed"] = comment.seed; metadata["Software"] = "NovelAI"; return metadata; } else if (exif["sd-metadata"]) { metadata.rawMetadata = exif["sd-metadata"]; const parameters = JSON.parse(exif["sd-metadata"]); const rowPrompt = parameters.image.prompt[0].prompt; const PromptRegex = /[^[\]]+(?=\[|$)/g; const negativePromptRegex = /\[.*?\]/g; const promptArray = rowPrompt.match(PromptRegex); const negativePromptArray = rowPrompt.match(negativePromptRegex); const prompt = promptArray.map((prompt) => prompt.replace(/^\,|\,$/g, "")); const negativePrompt = negativePromptArray.map((prompt) => prompt.replace(/^\[|\]$/g, "").replace(/^\,|\,$/g, "") ); metadata.prompt = prompt.join(", "); metadata.negativePrompt = negativePrompt.join(", "); metadata["Steps"] = parameters?.image.steps; metadata["Model"] = parameters?.model; metadata["Model hash"] = parameters?.model_hash; metadata["Sampler"] = parameters?.image.sampler; metadata["CFG scale"] = parameters?.image.cfg_scale; metadata["Seed"] = parameters?.image.seed; metadata["Size"] = `${parameters?.image.width}x${parameters?.image.height}`; metadata["Software"] = "InvokeAI"; return metadata; } } catch (error) { console.log(error); Swal.fire({ icon: "error", confirmButtonColor: `${colorClose}`, confirmButtonText: "닫기", title: "분석 오류", html: ` ${error}
오류내용과 이미지를 댓글로 알려주세요`, }); } } function infer(metadata) { if (metadata?.Software) return [metadata.Software]; const inferList = []; const denoising = metadata?.["Denoising strength"]; const hires = metadata?.["Hires upscaler"]; inferList.push("T2I"); if (denoising && !hires) { inferList[0] = "I2I"; } else if (hires) { inferList.push("Hires. fix"); } (metadata?.["AddNet Enabled"] || metadata?.prompt?.includes("lora:") || metadata?.negativePrompt?.includes("lora:")) && inferList.push("LoRa"); (metadata?.prompt?.includes("lyco:") || metadata?.negativePrompt?.includes("lyco:")) && inferList.push("LyCORIS"); (metadata?.["Hypernet"] || metadata?.prompt?.includes("hypernet:") || metadata?.negativePrompt?.includes("hypernet:")) && inferList.push("Hypernet"); const controlNetRegex = /(ControlNet)/; for (const key in metadata) { if (controlNetRegex.test(key)) { inferList.push("ControlNet"); break; } } metadata?.["SD upscale upscaler"] && inferList.push("SD upscale"); metadata?.["Ultimate SD upscale upscaler"] && inferList.push("Ultimate SD upscale"); metadata?.["Latent Couple"] && inferList.push("Latent Couple"); metadata?.["Dynamic thresholding enabled"] && inferList.push("Dynamic thresholding"); metadata?.["LLuL Enabled"] && inferList.push("LLuL"); metadata?.["Cutoff enabled"] && inferList.push("Cutoff"); metadata?.["Tiled Diffusion"] && inferList.push("Tiled Diffusion"); metadata?.["DDetailer model a"] && inferList.push("DDetailer"); metadata?.["ADetailer version"] && inferList.push("ADetailer"); // DDetailer/ADetailer를 DINO 하나로 묶는 게 나을까? return inferList; } function showAndHide(elementSelector) { const contentEls = document.querySelectorAll(elementSelector); contentEls.forEach((contentEl) => { const containerEl = contentEl.parentElement; const showMoreEl = containerEl.nextElementSibling; if (contentEl.offsetHeight > containerEl.offsetHeight) { showMoreEl.style.display = "block"; containerEl.classList.add("md-hidden"); } else { showMoreEl.style.display = "none"; containerEl.classList.remove("md-hidden"); containerEl.classList.add("md-visible"); } showMoreEl.addEventListener("click", () => { const isMore = showMoreEl.textContent === "더 보기"; showMoreEl.textContent = isMore ? "숨기기" : "더 보기"; containerEl.classList.toggle("md-hidden", !isMore); containerEl.classList.toggle("md-visible", isMore); }); }); } function showMetadataModal(metadata, url) { metadata = parseMetadata(metadata); const inferList = infer(metadata); const showMeta = Swal.mixin({ title: "메타데이터 요약", html: /*html*/ `
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"] ?? "정보 없음"}
Model
${ 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: `
${footerString}
` }); 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(); })();