Stable Diffusion image metadata viewer

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

  1. // ==UserScript==
  2. // @name Stable Diffusion image metadata viewer
  3. // @namespace https://github.com/himuro-majika
  4. // @version 0.2.5
  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.20.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.src);
  41.  
  42. function readExif(url) {
  43. fetch(url).then((response) => response.arrayBuffer())
  44. .then((fileBuffer) => loadTags(fileBuffer))
  45. .catch(() => {
  46. GM_xmlhttpRequest({
  47. method: "GET",
  48. url: url,
  49. responseType: "arraybuffer",
  50. onload: (res) => {
  51. loadTags(res.response);
  52. },
  53. onerror: (e) => {
  54. console.log(e);
  55. return;
  56. }
  57. });
  58. });
  59. }
  60.  
  61. function loadTags(fileBuffer) {
  62. if (!fileBuffer) return;
  63. try {
  64. const tags = ExifReader.load(fileBuffer, {expanded: true});
  65. const prompt = getPrompt(tags);
  66. makeData(prompt);
  67. } catch(e) {
  68. console.log(e);
  69. }
  70. }
  71.  
  72. function getPrompt(tags) {
  73. // console.dir(JSON.parse(JSON.stringify(tags)));
  74.  
  75. let com = ""
  76. let prompt = {
  77. positive: "",
  78. negative: "",
  79. others: ""
  80. }
  81.  
  82. // Exif
  83. if (tags.exif && tags.exif.UserComment) {
  84. com = decodeUnicode(tags.exif.UserComment.value);
  85. try {
  86. prompt.positive = com.match(/([^]+)Negative prompt: /)[1];
  87. prompt.negative = com.match(/Negative prompt: ([^]+)Steps: /)[1];
  88. prompt.others = com.match(/(Steps: [^]+)/)[1];
  89. } catch (e) {
  90. console.log(com);
  91. prompt.others = com;
  92. }
  93. return prompt;
  94. }
  95. // iTXt
  96. if (!tags.pngText) return;
  97. // A1111
  98. if (tags.pngText.parameters) {
  99. com = tags.pngText.parameters.description;
  100. try {
  101. prompt.positive = com.match(/([^]+)Negative prompt: /)[1];
  102. prompt.negative = com.match(/Negative prompt: ([^]+)Steps: /)[1];
  103. prompt.others = com.match(/(Steps: [^]+)/)[1];
  104. } catch (e) {
  105. console.log(com);
  106. prompt.others = com;
  107. }
  108. return prompt;
  109. }
  110. // NMKD
  111. if (tags.pngText.Dream) {
  112. com = tags.pngText.Dream.description;
  113. com += tags.pngText["sd-metadata"] ? "\r\n" + tags.pngText["sd-metadata"].description : "";
  114. try {
  115. prompt.positive = com.match(/([^]+?)\[[^[]+\]/)[1];
  116. prompt.negative = com.match(/\[([^[]+?)(\]|Steps: )/)[1];
  117. prompt.others = com.match(/\]([^]+)/)[1];
  118. } catch (e) {
  119. console.log(com);
  120. prompt.others = com;
  121. }
  122. return prompt;
  123. }
  124. // NAI
  125. if (tags.pngText.Comment) {
  126. const comment = tags.pngText.Comment.description.replaceAll(/\\u00a0/g, " ");
  127. const positive = tags.pngText.Description ? tags.pngText.Description.description : JSON.parse(comment).prompt;
  128. const negative= JSON.parse(comment).uc;
  129. let others = comment + "\r\n";
  130. others += tags.pngText.Software ? tags.pngText.Software.description + "\r\n" : "";
  131. others += tags.pngText.Title ? tags.pngText.Title.description + "\r\n" : "";
  132. others += tags.pngText.Source ? tags.pngText.Source.description : "";
  133. others += tags.pngText["Generation time"] ? "\r\nGeneration time: " + tags.pngText["Generation time"].description : "";
  134. prompt.positive = positive;
  135. prompt.negative = negative;
  136. prompt.others = others;
  137. return prompt;
  138. }
  139.  
  140. Object.keys(tags.pngText).forEach(tag => {
  141. com += tags.pngText[tag].description;
  142. });
  143.  
  144. // console.log(com);
  145. prompt.others = com;
  146. return prompt;
  147. }
  148.  
  149. function decodeUnicode(array) {
  150. const plain = array.map(t => t.toString(16).padStart(2, "0")).join("");
  151. if (!plain.match(/^554e49434f44450/)) {
  152. // console.log(array);
  153. return;
  154. }
  155. const hex = plain.replace(/^554e49434f44450[0-9]/, "").replace(/[0-9a-f]{4}/g, ",0x$&").replace(/^,/, "");
  156. const arhex = hex.split(",");
  157. let decode = "";
  158. arhex.forEach(v => {
  159. decode += String.fromCodePoint(v);
  160. })
  161. return decode;
  162. }
  163.  
  164. function makeButton() {
  165. addStyle();
  166. const button = document.createElement("button");
  167. button.id = "_gm_simv_open_button";
  168. button.innerHTML = "Show SD metadata";
  169. button.addEventListener("click", showModal);
  170. document.body.insertBefore(button, img);
  171. }
  172.  
  173. function makeData(prompt) {
  174. const positive = prompt.positive;
  175. const negative = prompt.negative;
  176. const others = prompt.others;
  177. if (!positive && !negative && !others) return;
  178. makeButton();
  179. const container = document.createElement("div");
  180. container.id ="_gm_simv_container";
  181. const copybutton = location.protocol == "http:" ? "" : `<button class="_gm_simv_copybutton" type="button">copy</button>`;
  182. container.innerHTML = `
  183. <div class="_gm_simv_modal">
  184. <div class="_gm_simv_modal_title">
  185. <h5>Stable Diffusion image metadata</h5>
  186. <button id="_gm_simv_closebutton" type="button">❎</button>
  187. </div>
  188. <div class="_gm_simv_modal_body">
  189. <div>
  190. <div class="_gm_simv_section">
  191. <label>Prompt</label>
  192. ${copybutton}
  193. </div>
  194. <textarea rows="6">${positive}</textarea>
  195. </div>
  196. <div>
  197. <div class="_gm_simv_section">
  198. <label>Negative Prompt</label>
  199. ${copybutton}
  200. </div>
  201. <textarea rows="6">${negative}</textarea>
  202. </div>
  203. <div>
  204. <div class="_gm_simv_section">
  205. <label>Other info</label>
  206. ${copybutton}
  207. </div>
  208. <textarea rows="6">${others}</textarea>
  209. </div>
  210. </div>
  211. </div>`;
  212. document.body.insertBefore(container, img);
  213. document.getElementById("_gm_simv_closebutton").addEventListener("click", closeModal);
  214. document.querySelectorAll("._gm_simv_copybutton").forEach(item => {
  215. item.addEventListener("click", copyText);
  216. });
  217. }
  218.  
  219. function addStyle() {
  220. GM_addElement("style", { textContent: `
  221. img {
  222. display: block; margin: auto;
  223. }
  224. #_gm_simv_open_button {
  225. position: absolute;
  226. }
  227. #_gm_simv_container {
  228. display: none; width: 100%;
  229. }
  230. ._gm_simv_modal {
  231. 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;
  232. }
  233. ._gm_simv_modal_title {
  234. display:flex; justify-content: space-between; padding: 0px 10px;
  235. }
  236. ._gm_simv_modal_body {
  237. padding: 10px;
  238. }
  239. #_gm_simv_closebutton {
  240. cursor: pointer; height: 4em; opacity: 0.5; padding: 1em; background: #0000; border: 0; width: 3em;
  241. }
  242. ._gm_simv_section {
  243. display:flex; justify-content: space-between;
  244. }
  245. ._gm_simv_modal textarea {
  246. display: block; width: 774px; max-width: 100%; background: #cccc; border: 0px none; margin: 10px 0;
  247. }
  248. ._gm_simv_copybutton {
  249. cursor: pointer; opacity: 0.5;
  250. }`});
  251. }
  252.  
  253. function showModal() {
  254. document.getElementById("_gm_simv_container").style.display = "block";
  255. }
  256.  
  257. function closeModal() {
  258. document.getElementById("_gm_simv_container").style.display = "none";
  259. }
  260.  
  261. function copyText() {
  262. const value = this.parentNode.parentNode.querySelector("textarea").value;
  263. navigator.clipboard.writeText(value);
  264. }
  265.  
  266. })();