Stable Diffusion image metadata viewer

Show Stable Diffusion generated image's metadata

目前为 2023-03-31 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Stable Diffusion image metadata viewer
  3. // @namespace https://github.com/himuro-majika
  4. // @version 0.2.2
  5. // @description Show Stable Diffusion generated image's metadata
  6. // @author himuro_majika
  7. // @match http://*/*.png
  8. // @match http://*/*.jpg
  9. // @match http://*/*.jpeg
  10. // @match http://*/*.webp
  11. // @match https://*/*.png
  12. // @match https://*/*.jpg
  13. // @match https://*/*.jpeg
  14. // @match https://*/*.webp
  15. // @match file:///*.png
  16. // @match file:///*.jpg
  17. // @match file:///*.jpeg
  18. // @match file:///*.webp
  19. // @require https://cdn.jsdelivr.net/npm/exifreader@4.11.0/dist/exif-reader.min.js
  20. // @license MIT
  21. // @grant none
  22. // ==/UserScript==
  23.  
  24. (function() {
  25. 'use strict';
  26.  
  27. const img = document.images[0];
  28. if (img) readExif(img);
  29.  
  30. async function readExif(img) {
  31. const fileBuffer = await fetch(img.src).then(response => response.arrayBuffer()).catch(() => {return;});
  32. if (!fileBuffer) return;
  33. const tags = ExifReader.load(fileBuffer);
  34. getComment(tags);
  35. }
  36.  
  37. function getComment(tags) {
  38. delete tags["MakerNote"];
  39. delete tags["Bit Depth"];
  40. delete tags["Color Type"];
  41. delete tags["Compression"];
  42. delete tags["Filter"];
  43. delete tags["Image Height"];
  44. delete tags["Image Width"];
  45. delete tags["Interlace"];
  46. // console.dir(JSON.parse(JSON.stringify(tags)));
  47.  
  48. let com = ""
  49.  
  50. // Exif
  51. if (tags["Exif IFD Pointer"] &&
  52. tags["UserComment"] &&
  53. tags["UserComment"].description == "[Unicode encoded text]") {
  54. com = decodeUnicode(tags["UserComment"].value);
  55. extractPrompt(com);
  56. return;
  57. }
  58.  
  59. // iTXt
  60. // A1111
  61. if (tags["parameters"]) {
  62. com += tags["parameters"].description;
  63. extractPrompt(com);
  64. return;
  65. }
  66.  
  67. // NAI
  68. if (tags["Software"] && tags["Software"].description == "NovelAI") {
  69. const positive = tags["Description"].description;
  70. const negative = tags["Comment"].description.replaceAll(/\\u00a0/g, " ").match(/"uc": "([^]+)"[,}]/)[1];
  71. let others = tags["Comment"].description.replaceAll(/\\u00a0/g, " ") + "\r\n";
  72. others += tags["Software"].description + "\r\n";
  73. others += tags["Title"].description + "\r\n";
  74. others += tags["Source"].description;
  75. const prompt = {
  76. positive: positive,
  77. negative: negative,
  78. others: others
  79. }
  80. makeButton();
  81. makeData(prompt);
  82. return;
  83. }
  84.  
  85. Object.keys(tags).forEach(tag => {
  86. com += tags[tag].description;
  87. });
  88.  
  89. // console.log(com);
  90. extractPrompt(com);
  91. return;
  92. }
  93.  
  94. function decodeUnicode(array) {
  95. const plain = array.map(t => t.toString(16).padStart(2, "0")).join("");
  96. if (!plain.match(/^554e49434f44450/)) {
  97. // console.log(array);
  98. return;
  99. }
  100. const hex = plain.replace(/^554e49434f44450[0-9]/, "").replace(/[0-9a-f]{4}/g, ",0x$&").replace(/^,/, "");
  101. const arhex = hex.split(",");
  102. let decode = "";
  103. arhex.forEach(v => {
  104. decode += String.fromCodePoint(v);
  105. })
  106. return decode;
  107. }
  108.  
  109. function extractPrompt(com) {
  110. const positive = extractPositivePrompt(com);
  111. const negative = extractNegativePrompt(com);
  112. const others = extractOthers(com);
  113. if (!positive && !negative && !others) return;
  114. const prompt = {
  115. positive: positive,
  116. negative: negative,
  117. others: others
  118. }
  119. makeButton();
  120. makeData(prompt);
  121. }
  122.  
  123. function makeButton() {
  124. const button = document.createElement("button");
  125. button.innerHTML = "Show SD metadata";
  126. button.addEventListener("click", showModal);
  127. document.body.insertBefore(button, img);
  128. }
  129.  
  130. function makeData(prompt) {
  131. const positive = prompt.positive;
  132. const negative = prompt.negative;
  133. const others = prompt.others;
  134. const container = document.createElement("div");
  135. container.id ="_gm_sipv_container";
  136. container.style.display = "none";
  137. container.style.width = "100%";
  138. const copybutton = location.protocol == "http:" ? "" : `<button class="_gm_sipv_copybutton" type="button" style="cursor: pointer; opacity: 0.5;">copy</button>`;
  139. container.innerHTML = `
  140. <div style="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;">
  141. <div style="display:flex; justify-content: space-between; padding: 0px 10px;">
  142. <h5>Stable Diffusion image metadata</h5>
  143. <button id="_gm_sipv_closebutton" type="button" style="cursor: pointer; height: 4em; opacity: 0.5; padding: 1em; background: #0000; border: 0; width: 3em;">❎</button>
  144. </div>
  145. <div style="padding: 10px;">
  146. <div>
  147. <div style="display:flex; justify-content: space-between;">
  148. <label>Prompt</label>
  149. ${copybutton}
  150. </div>
  151. <textarea rows="6" style="display: block; width: 774px; max-width: 100%; background: #cccc; border: 0px none; margin: 10px 0;">${positive}</textarea>
  152. </div>
  153. <div>
  154. <div style="display:flex; justify-content: space-between;">
  155. <label>Negative Prompt</label>
  156. ${copybutton}
  157. </div>
  158. <textarea rows="6" style="display: block; width: 774px; max-width: 100%; background: #cccc; border: 0px none; margin: 10px 0;">${negative}</textarea>
  159. </div>
  160. <div>
  161. <div style="display:flex; justify-content: space-between;">
  162. <label>Other info</label>
  163. ${copybutton}
  164. </div>
  165. <textarea rows="3" style="display: block; width: 774px; max-width: 100%;background: #cccc; border: 0px none; margin: 10px 0;">${others}</textarea>
  166. </div>
  167. </div>
  168. </div>`;
  169. document.body.insertBefore(container, img);
  170. document.getElementById("_gm_sipv_closebutton").addEventListener("click", closeModal);
  171. document.querySelectorAll("._gm_sipv_copybutton").forEach(item => {
  172. item.addEventListener("click", copyText);
  173. });
  174. }
  175.  
  176. function extractPositivePrompt(text) {
  177. try {
  178. let matchtext =
  179. text.match(/([^]+)Negative prompt: /) ||
  180. text.match(/([^]+)Steps: /) ||
  181. text.match(/([^]+){"steps"/) ||
  182. text.match(/([^]+)\[[^[]+\]/);
  183. return matchtext[1];
  184. } catch (e) {
  185. console.log(text);
  186. return "";
  187. }
  188. }
  189.  
  190. function extractNegativePrompt(text) {
  191. try {
  192. let matchtext =
  193. text.match(/Negative prompt: ([^]+)Steps: /) ||
  194. text.match(/"uc": "([^]+)"}/) ||
  195. text.match(/\[([^[]+)\]/);
  196. return matchtext[1];
  197. } catch (e) {
  198. console.log(text);
  199. return "";
  200. }
  201. }
  202.  
  203. function extractOthers(text) {
  204. try {
  205. let matchtext =
  206. text.match(/(Steps: [^]+)/) ||
  207. text.match(/{("steps"[^]+)"uc": /) ||
  208. text.match(/\]([^]+)/);
  209. return matchtext[1];
  210. } catch (e) {
  211. console.log(text);
  212. return text;
  213. }
  214. }
  215.  
  216. function showModal() {
  217. document.getElementById("_gm_sipv_container").style.display = "block";
  218. }
  219.  
  220. function closeModal() {
  221. document.getElementById("_gm_sipv_container").style.display = "none";
  222. }
  223.  
  224. function copyText() {
  225. const value = this.parentNode.parentNode.querySelector("textarea").value;
  226. navigator.clipboard.writeText(value);
  227. }
  228.  
  229. })();