Stable Diffusion image metadata viewer

Show Stable Diffusion generated image's metadata

目前为 2023-03-14 提交的版本。查看 最新版本

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