AI 이미지 EXIF 뷰어

AI 이미지 메타데이터 보기

目前为 2023-02-23 提交的版本。查看 最新版本

// ==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("<", "&lt;").replaceAll(">", "&gt;");
        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/");
      };
    });
  }
})();

QingJ © 2025

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