Stable Diffusion image metadata viewer

显示 Stable Diffusion 生成的图像的元数据

当前为 2023-05-12 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Stable Diffusion image metadata viewer
  3. // @namespace https://github.com/himuro-majika
  4. // @version 0.2.4
  5. // @description Show Stable Diffusion generated image's metadata
  6. // @description:ja Stable Diffusionで生成された画像の埋め込みメタデータを表示します
  7. // @description:ko 표시 Stable Diffusion 생성된 이미지의 메타데이터
  8. // @description:de Metadaten des durch Stabile Diffusion erzeugten Bildes anzeigen
  9. // @description:es Mostrar los metadatos de la imagen generada por Stable Diffusion
  10. // @description:fr Afficher les métadonnées de l'image générée par la Stable Diffusion
  11. // @description:it Mostrare i metadati dell'immagine generata da Stable Diffusion
  12. // @description:zh-CN 显示 Stable Diffusion 生成的图像的元数据
  13. // @description:zh-SG 显示 Stable Diffusion 生成的图像的元数据
  14. // @description:zh-TW 顯示 Stable Diffusion 生成圖像的元數據
  15. // @description:zh-HK 顯示 Stable Diffusion 生成圖像的元數據
  16. // @author himuro_majika
  17. // @match http://*/*.png
  18. // @match http://*/*.jpg
  19. // @match http://*/*.jpeg
  20. // @match http://*/*.webp
  21. // @match https://*/*.png
  22. // @match https://*/*.jpg
  23. // @match https://*/*.jpeg
  24. // @match https://*/*.webp
  25. // @match file:///*.png
  26. // @match file:///*.jpg
  27. // @match file:///*.jpeg
  28. // @match file:///*.webp
  29. // @require https://cdn.jsdelivr.net/npm/exifreader@4.12.0/dist/exif-reader.min.js
  30. // @license MIT
  31. // @grant GM_xmlhttpRequest
  32. // @grant GM_addElement
  33. // ==/UserScript==
  34.  
  35. (function() {
  36. 'use strict';
  37.  
  38. const img = document.images[0];
  39. if (!img) return;
  40. readExif(img);
  41.  
  42. function readExif(img) {
  43. fetch(img.src).then((response) => response.arrayBuffer())
  44. .then((fileBuffer) => loadTags(fileBuffer))
  45. .catch(() => {
  46. GM_xmlhttpRequest({
  47. method: "GET",
  48. url: img.src,
  49. responseType: "arraybuffer",
  50. onload: (res) => {
  51. loadTags(res.response);
  52. },
  53. onerror: (e) => {
  54. console.log(e);
  55. return;
  56. }
  57. });
  58. });
  59. function loadTags(fileBuffer) {
  60. if (!fileBuffer) return;
  61. try {
  62. const tags = ExifReader.load(fileBuffer, {expanded: true});
  63. getComment(tags);
  64. } catch(e) {
  65. console.log(e);
  66. }
  67. }
  68. }
  69.  
  70. function getComment(tags) {
  71. // console.dir(JSON.parse(JSON.stringify(tags)));
  72.  
  73. let com = ""
  74.  
  75. // Exif
  76. if (tags.exif && tags.exif.UserComment) {
  77. com = decodeUnicode(tags.exif.UserComment.value);
  78. extractPrompt(com);
  79. return;
  80. }
  81. // iTXt
  82. if (!tags.pngText) return;
  83. // A1111
  84. if (tags.pngText.parameters) {
  85. com = tags.pngText.parameters.description;
  86. extractPrompt(com);
  87. return;
  88. }
  89. // NMKD
  90. if (tags.pngText.Dream) {
  91. com = tags.pngText.Dream.description;
  92. com += tags.pngText["sd-metadata"] ? "\r\n" + tags.pngText["sd-metadata"].description : "";
  93. extractPrompt(com);
  94. return;
  95. }
  96. // NAI
  97. if (tags.pngText.Software && tags.pngText.Software.description == "NovelAI") {
  98. const positive = tags.pngText.Description.description;
  99. const negative = tags.pngText.Comment.description.replaceAll(/\\u00a0/g, " ").match(/"uc": "([^]+)"[,}]/)[1];
  100. let others = tags.pngText.Comment.description.replaceAll(/\\u00a0/g, " ") + "\r\n";
  101. others += tags.pngText.Software.description + "\r\n";
  102. others += tags.pngText.Title.description + "\r\n";
  103. others += tags.pngText.Source.description;
  104. const prompt = {
  105. positive: positive,
  106. negative: negative,
  107. others: others
  108. }
  109. makeData(prompt);
  110. return;
  111. }
  112.  
  113. Object.keys(tags.pngText).forEach(tag => {
  114. com += tags.pngText[tag].description;
  115. });
  116.  
  117. // console.log(com);
  118. extractPrompt(com);
  119. return;
  120. }
  121.  
  122. function decodeUnicode(array) {
  123. const plain = array.map(t => t.toString(16).padStart(2, "0")).join("");
  124. if (!plain.match(/^554e49434f44450/)) {
  125. // console.log(array);
  126. return;
  127. }
  128. const hex = plain.replace(/^554e49434f44450[0-9]/, "").replace(/[0-9a-f]{4}/g, ",0x$&").replace(/^,/, "");
  129. const arhex = hex.split(",");
  130. let decode = "";
  131. arhex.forEach(v => {
  132. decode += String.fromCodePoint(v);
  133. })
  134. return decode;
  135. }
  136.  
  137. function extractPrompt(com) {
  138. const positive = extractPositivePrompt(com);
  139. const negative = extractNegativePrompt(com);
  140. const others = extractOthers(com);
  141. if (!positive && !negative && !others) return;
  142. const prompt = {
  143. positive: positive,
  144. negative: negative,
  145. others: others
  146. }
  147. makeData(prompt);
  148. }
  149.  
  150. function makeButton() {
  151. addStyle();
  152. const button = document.createElement("button");
  153. button.id = "_gm_simv_open_button";
  154. button.innerHTML = "Show SD metadata";
  155. button.addEventListener("click", showModal);
  156. document.body.insertBefore(button, img);
  157. }
  158.  
  159. function makeData(prompt) {
  160. makeButton();
  161. const positive = prompt.positive;
  162. const negative = prompt.negative;
  163. const others = prompt.others;
  164. const container = document.createElement("div");
  165. container.id ="_gm_simv_container";
  166. const copybutton = location.protocol == "http:" ? "" : `<button class="_gm_simv_copybutton" type="button">copy</button>`;
  167. container.innerHTML = `
  168. <div class="_gm_simv_modal">
  169. <div class="_gm_simv_modal_title">
  170. <h5>Stable Diffusion image metadata</h5>
  171. <button id="_gm_simv_closebutton" type="button">❎</button>
  172. </div>
  173. <div class="_gm_simv_modal_body">
  174. <div>
  175. <div class="_gm_simv_section">
  176. <label>Prompt</label>
  177. ${copybutton}
  178. </div>
  179. <textarea rows="6">${positive}</textarea>
  180. </div>
  181. <div>
  182. <div class="_gm_simv_section">
  183. <label>Negative Prompt</label>
  184. ${copybutton}
  185. </div>
  186. <textarea rows="6">${negative}</textarea>
  187. </div>
  188. <div>
  189. <div class="_gm_simv_section">
  190. <label>Other info</label>
  191. ${copybutton}
  192. </div>
  193. <textarea rows="3">${others}</textarea>
  194. </div>
  195. </div>
  196. </div>`;
  197. document.body.insertBefore(container, img);
  198. document.getElementById("_gm_simv_closebutton").addEventListener("click", closeModal);
  199. document.querySelectorAll("._gm_simv_copybutton").forEach(item => {
  200. item.addEventListener("click", copyText);
  201. });
  202. }
  203.  
  204. function addStyle() {
  205. GM_addElement("style", { textContent: `
  206. img {
  207. display: block; margin: auto;
  208. }
  209. #_gm_simv_open_button {
  210. position: absolute;
  211. }
  212. #_gm_simv_container {
  213. display: none; width: 100%;
  214. }
  215. ._gm_simv_modal {
  216. color: #eee; width: 800px; max-width: 100%; margin-left: auto; margin-right: auto; z-index: 2; position: fixed; inset: auto 0; margin: auto; background: #000a; border-radius: 6px; box-shadow: #000 0px 0px 2px;
  217. }
  218. ._gm_simv_modal_title {
  219. display:flex; justify-content: space-between; padding: 0px 10px;
  220. }
  221. ._gm_simv_modal_body {
  222. padding: 10px;
  223. }
  224. #_gm_simv_closebutton {
  225. cursor: pointer; height: 4em; opacity: 0.5; padding: 1em; background: #0000; border: 0; width: 3em;
  226. }
  227. ._gm_simv_section {
  228. display:flex; justify-content: space-between;
  229. }
  230. ._gm_simv_modal textarea {
  231. display: block; width: 774px; max-width: 100%; background: #cccc; border: 0px none; margin: 10px 0;
  232. }
  233. ._gm_simv_copybutton {
  234. cursor: pointer; opacity: 0.5;
  235. }`});
  236. }
  237.  
  238. function extractPositivePrompt(text) {
  239. try {
  240. let matchtext =
  241. text.match(/([^]+)Negative prompt: /) ||
  242. text.match(/([^]+)Steps: /) ||
  243. text.match(/([^]+){"steps"/) ||
  244. text.match(/([^]+)\[[^[]+\]/);
  245. return matchtext[1];
  246. } catch (e) {
  247. console.log(text);
  248. return "";
  249. }
  250. }
  251.  
  252. function extractNegativePrompt(text) {
  253. try {
  254. let matchtext =
  255. text.match(/Negative prompt: ([^]+)Steps: /) ||
  256. text.match(/"uc": "([^]+)"}/) ||
  257. text.match(/\[([^[]+)\]/);
  258. return matchtext[1];
  259. } catch (e) {
  260. console.log(text);
  261. return "";
  262. }
  263. }
  264.  
  265. function extractOthers(text) {
  266. try {
  267. let matchtext =
  268. text.match(/(Steps: [^]+)/) ||
  269. text.match(/{("steps"[^]+)"uc": /) ||
  270. text.match(/\]([^]+)/);
  271. return matchtext[1];
  272. } catch (e) {
  273. console.log(text);
  274. return text;
  275. }
  276. }
  277.  
  278. function showModal() {
  279. document.getElementById("_gm_simv_container").style.display = "block";
  280. }
  281.  
  282. function closeModal() {
  283. document.getElementById("_gm_simv_container").style.display = "none";
  284. }
  285.  
  286. function copyText() {
  287. const value = this.parentNode.parentNode.querySelector("textarea").value;
  288. navigator.clipboard.writeText(value);
  289. }
  290.  
  291. })();