Sleazy Fork is available in English.

RE: AI 이미지 EXIF 뷰어

AI 이미지 메타데이터 보기

  1. // ==UserScript==
  2. // @name RE: AI 이미지 EXIF 뷰어
  3. // @namespace https://github.com/panta5/AI-Image-EXIF-Viewer
  4. // @match https://www.pixiv.net/*
  5. // @match https://arca.live/b/aiart*
  6. // @match https://arca.live/b/hypernetworks*
  7. // @match https://arca.live/b/aiartreal*
  8. // @match https://arca.live/b/aireal*
  9. // @match https://arca.live/b/characterai*
  10. // @version 2.1.1+1.1
  11. // @author PantaFive
  12. // @homepageURL https://github.com/panta5/AI-Image-EXIF-Viewer
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js
  14. // @require https://greasyfork.org/scripts/452821-upng-js/code/UPNGjs.js?version=1103227
  15. // @require https://cdn.jsdelivr.net/npm/casestry-exif-library@2.0.3/dist/exif-library.min.js
  16. // @require https://cdn.jsdelivr.net/npm/sweetalert2@11
  17. // @require https://cdn.jsdelivr.net/npm/clipboard@2.0.10/dist/clipboard.min.js
  18. // @require https://cdnjs.cloudflare.com/ajax/libs/arrive/2.4.1/arrive.min.js
  19. // @require https://greasyfork.org/scripts/421384-gm-fetch/code/GM_fetch.js
  20. // @grant GM_xmlhttpRequest
  21. // @grant GM_registerMenuCommand
  22. // @grant GM_setValue
  23. // @grant GM_getValue
  24. // @grant GM_addStyle
  25. // @grant GM_download
  26. // @grant GM_info
  27.  
  28. // @description AI 이미지 메타데이터 보기
  29. // @license MIT
  30. // ==/UserScript==
  31.  
  32. //this URL must be changed manually to be linked properly
  33. // const scriptGreasyforkURL = '#';
  34. //toast timer in ms
  35. const toastTimer = 3000;
  36. const colorOption1 = '#5cc964';
  37. const colorOption2 = '#ff9d0b';
  38. const colorClose = '#b41b29';
  39.  
  40. const footerString = `<div class="version">v${GM_info.script.version} - <a href="${GM_info.script.namespace}" target="_blank">Forked Repository</a> - <a href="https://github.com/nyqui/AI-Image-EXIF-Viewer" target="_blank">Original GitHub Repository</a></div>`;
  41.  
  42. (async function () {
  43. 'use strict';
  44.  
  45. const modalCSS = /* css */ `
  46. font-family: -apple-system, BlinkMacSystemFont, NanumBarunGothic, NanumGothic, system-ui, sans-serif;
  47. .swal2-popup {
  48. font-size: 15px;
  49. }
  50. .swal2-actions {
  51. margin: .4em auto 0;
  52. }
  53. .swal2-footer{
  54. margin: 1em 1.6em .3em;
  55. padding: 1em 0 0;
  56. overflow: auto;
  57. font-size: 1.125em;
  58. }
  59. #dropzone {
  60. z-index: 100000000;
  61. display: none;
  62. position: fixed;
  63. width: 100%;
  64. height: 100%;
  65. left: 0;
  66. top: 0;
  67. }
  68.  
  69. .md-grid {
  70. display: grid;
  71. grid-template-rows: repeat(3, auto);
  72. text-align: left;
  73. }
  74.  
  75. .md-grid-item {
  76. border-bottom: 1px solid #b3b3b3;
  77. padding: .6em;
  78. }
  79.  
  80. .md-grid-item:last-child {
  81. border-bottom: 0px;
  82. }
  83.  
  84. .md-nested-grid {
  85. display: grid;
  86. grid-template-columns: repeat(3, 1fr);
  87. grid-template-rows: repeat(4, auto);
  88. gap: .5em;
  89. }
  90.  
  91. .md-title {
  92. line-height: 1em;
  93. font-weight: bold;
  94. font-size: .9em;
  95. padding-bottom: .2em;
  96. display: flex;
  97. color: #1A1A1A;
  98. }
  99.  
  100. .md-info {
  101. line-height: 1.5em;
  102. font-size: .8em;
  103. word-break: break-word;
  104. color: #444444;
  105. }
  106.  
  107. .md-hidden {
  108. overflow: hidden;
  109. position: relative;
  110. max-height: 5em;
  111. }
  112.  
  113. .md-hidden:after {
  114. content: "";
  115. position: absolute;
  116. bottom: 0;
  117. left: 0;
  118. width: 100%;
  119. height: 2em;
  120. background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4) 0%, white 100%);
  121. }
  122. .md-info > a{
  123. text-decoration: none;
  124. }
  125. .md-info > a:hover{
  126. text-decoration: underline !important;
  127. }
  128. pre.md-show-and-hide{
  129. font-family: monospace;
  130. margin: 0px;
  131. white-space: pre-line;
  132. }
  133.  
  134. .md-visible {
  135. height: auto;
  136. overflow: auto;
  137. }
  138.  
  139. .md-model {
  140. grid-column-start: 1;
  141. grid-column-end: 3;
  142. }
  143.  
  144. .md-show-more {
  145. text-align: center;
  146. cursor: pointer;
  147. }
  148.  
  149. #md-tags {
  150. width: 100%;
  151. height: 20em;
  152. padding-top: .5em;
  153. text-align: left;
  154. font-size: 0.9em;
  155. }
  156. span.md-button {
  157. margin-left: .15em;
  158. cursor: pointer;
  159. }
  160.  
  161. span.md-copy {
  162. content: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Crect width='16' height='16' stroke='none' fill='%23000000' opacity='0'/%3E%3Cg transform='matrix(0.6 0 0 0.6 8 8)' %3E%3Cpath style='stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;' transform=' translate(-12, -12)' d='M 4 2 C 2.895 2 2 2.895 2 4 L 2 18 L 4 18 L 4 4 L 18 4 L 18 2 L 4 2 z M 8 6 C 6.895 6 6 6.895 6 8 L 6 20 C 6 21.105 6.895 22 8 22 L 20 22 C 21.105 22 22 21.105 22 20 L 22 8 C 22 6.895 21.105 6 20 6 L 8 6 z M 8 8 L 20 8 L 20 20 L 8 20 L 8 8 z' stroke-linecap='round' /%3E%3C/g%3E%3C/svg%3E");
  163. }
  164.  
  165. span.md-civitai {
  166. content: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAllBMVEVHcEwORtARe/UMPdYPY+gOUuAKKc0RdvENT94LKc0LJMcLMM8LMc8SgPUSffIRePERcu4MN9EQbewQauoLKswLKswUIo0PZ+gWDmT///8LLtMPLKwQcvESA00LF4UiGWgKB2wNSuAMNM8LOtgPRb2XlLxDQY7Fw9kOW+cMO9NBNHG7utXR0OLv7vTc2+fe3utNSIs7N4PzUc8zAAAAFnRSTlMABvvJy9Kz18fsI9hVphtuSzKwKn11Da4jUwAAALdJREFUeNpNz8eWwjAMBVDZCQk9VMndTiGFPv//c3NimIG3ku5CRw8gZr1areETVuSc84L97ZvccCJu8k1c9ztjqHGuIWN2ewBIiRoX2ja4hiiNoAOepDxh0JQygLRHcZWXi3x47CcMYNqjv3Uh/LTiDeUIQrRPgeUbxFV2nbx7FyEp0frxqLdYJiMMgxb2fLZCD/UI26yqK7TWVXWVbOOvy4VSWiu1WP6XO86Umh3YV995ls1f0y8RAhFlMPQmQwAAAABJRU5ErkJggg==");
  167. }
  168.  
  169. .version {
  170. margin: 1px;
  171. text-align: right;
  172. font-size: .5em;
  173. font-style: italic;
  174. }
  175. `;
  176.  
  177. const toastmix = Swal.mixin({
  178. toast: true,
  179. position: 'bottom',
  180. showConfirmButton: false,
  181. timer: `${toastTimer}`,
  182. timerProgressBar: true,
  183. });
  184.  
  185. function registerMenu() {
  186. try {
  187. if (typeof GM_registerMenuCommand == undefined) {
  188. return;
  189. } else {
  190. GM_registerMenuCommand('(로그인 필수) Pixiv 뷰어 사용 토글', () => {
  191. if (GM_getValue('usePixiv', false)) {
  192. GM_setValue('usePixiv', false);
  193. toastmix.fire({
  194. icon: 'error',
  195. title: `Pixiv 비활성화
  196. 창이 닫힌 새로고침 됩니다`,
  197. didDestroy: () => {
  198. location.reload();
  199. },
  200. });
  201. } else {
  202. GM_setValue('usePixiv', true);
  203. toastmix.fire({
  204. icon: 'success',
  205. title: `Pixiv 활성화
  206. 창이 닫힌 새로고침 됩니다`,
  207. didDestroy: () => {
  208. location.reload();
  209. },
  210. });
  211. }
  212. });
  213. GM_registerMenuCommand('아카라이브 EXIF 보존 토글', () => {
  214. if (GM_getValue('saveExifDefault', true)) {
  215. GM_setValue('saveExifDefault', false);
  216. toastmix.fire({
  217. icon: 'error',
  218. title: `아카라이브 EXIF 보존 비활성화
  219. 다음번 작성시부터 버려집니다`,
  220. });
  221. } else {
  222. GM_setValue('saveExifDefault', true);
  223. toastmix.fire({
  224. icon: 'success',
  225. title: `아카라이브 EXIF 보존 활성화
  226. 다음번 작성시부터 보존됩니다`,
  227. });
  228. }
  229. });
  230. // https://arca.live/b/aiart/121872652 ::::: 수정 250211
  231. // todo: 일단 주석처리해보고 문제 없으면 그대로 두기
  232. // GM_registerMenuCommand('아카라이브 글쓰기 창 스크립트 토글', () => {
  233. // if (GM_getValue('useDragdropUpload', true)) {
  234. // GM_setValue('useDragdropUpload', false);
  235. // toastmix.fire({
  236. // icon: 'error',
  237. // title: `아카 글쓰기 창 스크립트 비활성화
  238. // 다음번 작성시부터 적용됩니다`,
  239. // });
  240. // } else {
  241. // GM_setValue('useDragdropUpload', true);
  242. // toastmix.fire({
  243. // icon: 'success',
  244. // title: `아카 글쓰기 창 스크립트 활성화
  245. // 다음번 작성시부터 적용됩니다`,
  246. // });
  247. // }
  248. // });
  249. }
  250. } catch (err) {
  251. console.log(err);
  252. }
  253. }
  254.  
  255. class DropZone {
  256. constructor() {
  257. const dropZone = document.createElement('div');
  258. dropZone.setAttribute('id', 'dropzone');
  259. document.body.appendChild(dropZone);
  260. this.dropZone = document.getElementById('dropzone');
  261. this.setupEventListeners();
  262. }
  263.  
  264. showDropZone() {
  265. this.dropZone.style.display = 'block';
  266. }
  267.  
  268. hideDropZone() {
  269. this.dropZone.style.display = 'none';
  270. }
  271.  
  272. allowDrag(e) {
  273. e.preventDefault();
  274. }
  275.  
  276. async handleDrop(e) {
  277. e.preventDefault();
  278. this.hideDropZone();
  279.  
  280. const file = e.dataTransfer.files[0];
  281. if (!file) return;
  282.  
  283. const blob = await fileToBlob(file);
  284. const type = blob.type;
  285. if (isArcaEditor) {
  286. const uploadableType = handleUploadable(type);
  287. let editor = document.querySelector('.write-body .fr-element');
  288. let saveEXIF = GM_getValue('saveExifDefault', true);
  289. if (uploadableType == 'image') {
  290. try {
  291. saveEXIF = document.getElementById('saveExif').checked;
  292. } catch {}
  293. uploadArca(blob, uploadableType, saveEXIF).then((url) => {
  294. editor.innerHTML =
  295. editor.innerHTML + `<p><img src="${url}" class="fr-fic fr-dii"></p><p><br></p>`;
  296. Swal.close();
  297. });
  298. } else if (uploadableType == 'video') {
  299. uploadArca(blob, uploadableType, false).then((url) => {
  300. editor.innerHTML =
  301. editor.innerHTML +
  302. `<p><span class="fr-video fr-dvi fr-draggable"><video class="fr-draggable" controls="" loop="" muted="" playsinline="" src="${url}">귀하의 브라우저는 html5 video 지원하지 않습니다.</video></span></p><p><br></p>`;
  303. Swal.close();
  304. });
  305. } else {
  306. Swal.close();
  307. toastmix.fire({
  308. icon: 'error',
  309. title: `업로드 오류:
  310. 업로드 있는 포맷이 아닙니다.`,
  311. });
  312. }
  313. } else {
  314. if (!isSupportedImageFormat(blob.type)) {
  315. notSupportedFormat();
  316. return;
  317. }
  318. const metadata = await extractImageMetadata(blob, type);
  319. metadata ? showMetadataModal(metadata) : showTagExtractionModal(null, blob);
  320. }
  321. }
  322.  
  323. setupEventListeners() {
  324. window.addEventListener('dragenter', () => this.showDropZone());
  325. this.dropZone.addEventListener('dragenter', (e) => this.allowDrag(e));
  326. this.dropZone.addEventListener('dragover', (e) => this.allowDrag(e));
  327. this.dropZone.addEventListener('dragleave', () => this.hideDropZone());
  328. this.dropZone.addEventListener('drop', (e) => this.handleDrop(e));
  329. }
  330. }
  331.  
  332. function getMetadataPNGChunk(chunk) {
  333. const isValidPNG = chunk.slice(0, 8).every((byte, index) => [137, 80, 78, 71, 13, 10, 26, 10][index] === byte);
  334. if (!isValidPNG) {
  335. console.error('Invalid PNG');
  336. return null;
  337. }
  338.  
  339. const textDecoder = new TextDecoder('utf-8');
  340. let metadata = {};
  341.  
  342. function checkForChunks() {
  343. let position = 8;
  344. while (true) {
  345. const chunkLength = getUint32(position);
  346.  
  347. if (chunk.byteLength < position + chunkLength + 12) {
  348. return;
  349. }
  350. const name = String.fromCharCode(...chunk.subarray(position + 4, position + 8));
  351. const data = chunk.subarray(position + 8, position + chunkLength + 8);
  352. const dataString = textDecoder.decode(data);
  353.  
  354. if (name === 'tEXt') {
  355. const [key, value] = dataString.split('\0');
  356. metadata[key] = value;
  357. } else if (name === 'iTXt') {
  358. const [key, value] = dataString.split('\0\0\0\0\0');
  359. metadata[key] = value;
  360. } else if (name === 'IDAT') {
  361. metadata[name] = true;
  362. return;
  363. }
  364. position += chunkLength + 12;
  365. }
  366. }
  367.  
  368. function getUint32(offset) {
  369. return (chunk[offset] << 24) | (chunk[offset + 1] << 16) | (chunk[offset + 2] << 8) | chunk[offset + 3];
  370. }
  371. checkForChunks();
  372. return metadata;
  373. }
  374.  
  375. function getMetadataJPEGChunk(chunk) {
  376. if (chunk[0] !== 255 || chunk[1] !== 216) {
  377. // 0xFF 0xD8
  378. console.error('Invalid JPEG');
  379. return null;
  380. }
  381. const textDecoder = new TextDecoder();
  382. let offset = 2;
  383. if (chunk[offset] === 0xff) {
  384. switch (chunk[offset + 1]) {
  385. case 0xe0: {
  386. offset += ((chunk[offset + 2] << 8) | chunk[offset + 3]) + 2;
  387. }
  388. case 0xe1: {
  389. const length = (chunk[offset + 2] << 8) | chunk[offset + 3];
  390. const data = chunk.subarray(offset + 4, offset + 2 + length);
  391. if (
  392. data[0] === 69 && //0x45 E
  393. data[1] === 120 && //0x78 x
  394. data[2] === 105 && //0x69 i
  395. data[3] === 102 && //0x66 f
  396. data[4] === 0 && // null
  397. data[5] === 0 // null
  398. ) {
  399. const userCommentData = data.subarray(46, offset + 2 + length);
  400. const parameters = textDecoder
  401. .decode(userCommentData)
  402. .replace('UNICODE', '')
  403. .replaceAll('\u0000', '');
  404. return {
  405. parameters,
  406. };
  407. }
  408. }
  409. default:
  410. return null;
  411. }
  412. }
  413. return null;
  414. }
  415.  
  416. function getFileName(url) {
  417. if (url === '/') return;
  418. const fileName = url.split('?')[0];
  419. return fileName;
  420. }
  421.  
  422. function parseMetadata(exif) {
  423. try {
  424. let metadata = {};
  425. if (exif.parameters) {
  426. let parameters = exif.parameters.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
  427. metadata.rawMetadata = parameters;
  428.  
  429. if (!parameters.includes('Negative prompt')) {
  430. parameters = parameters.replace('Steps', '\nNegative prompt: 정보 없음\nSteps');
  431. }
  432.  
  433. parameters = parameters.split('Steps: ');
  434. parameters = `${parameters[0]
  435. .replaceAll(': ', ':')
  436. .replace('Negative prompt:', 'Negative prompt: ')}Steps: ${parameters[1]}`;
  437.  
  438. const metadataStr = parameters.substring(parameters.indexOf('Steps'), parameters.length);
  439. const keyValuePairs = metadataStr.split(', ');
  440.  
  441. for (const pair of keyValuePairs) {
  442. const [key, value] = pair.split(': ');
  443. metadata[key] = value;
  444. }
  445.  
  446. metadata.prompt =
  447. parameters.indexOf('Negative prompt') === 0
  448. ? '정보 없음'
  449. : parameters.substring(0, parameters.indexOf('Negative prompt:'));
  450. metadata.negativePrompt = parameters.includes('Negative prompt:')
  451. ? parameters
  452. .substring(parameters.indexOf('Negative prompt:'), parameters.indexOf('Steps:'))
  453. .replace('Negative prompt:', '')
  454. : null;
  455.  
  456. return metadata;
  457. } else if (exif.Description) {
  458. metadata.rawMetadata = `${exif.Description}\n${exif.Comment}`;
  459. const comment = JSON.parse(exif.Comment);
  460.  
  461. metadata.prompt = exif.Description;
  462. metadata.negativePrompt = comment.uc;
  463. metadata['Steps'] = comment.steps;
  464. metadata['Sampler'] = comment.sampler;
  465. metadata['CFG scale'] = comment.scale;
  466. metadata['Seed'] = comment.seed;
  467. metadata['Software'] = 'NovelAI';
  468. metadata['Model'] = exif.Source; // Model (v1.1에서 추가)
  469. const model = exif.Source.split(' '); // Model hash (v1.1에서 추가)
  470. metadata['Model hash'] = model[model.length - 1]; // Model hash (v1.1에서 추가)
  471. metadata['Size'] = `${comment.width}x${comment.height}`; // Size (v1.1에서 추가)
  472.  
  473. if (comment?.strength) {
  474. metadata['Denoising strength'] = comment.strength; // Denoising strength (v1.1에서 추가)
  475. }
  476.  
  477. // v4 prompt/negative prompt https://arca.live/b/aiart/124370349
  478. if (comment?.v4_prompt || comment?.v4_negative_prompt) {
  479. console.log('v4 감지');
  480. function extractAndCleanCaptions(data, id) {
  481. function recursiveExtract(obj) {
  482. let captions = [];
  483. if (typeof obj === 'object' && obj !== null) {
  484. for (const key in obj) {
  485. if (key.includes('caption') && typeof obj[key] === 'string') {
  486. captions.push(obj[key]);
  487. } else {
  488. captions = captions.concat(recursiveExtract(obj[key]));
  489. }
  490. }
  491. } else if (Array.isArray(obj)) {
  492. obj.forEach((item) => {
  493. captions = captions.concat(recursiveExtract(item));
  494. });
  495. }
  496. return captions;
  497. }
  498.  
  499. const captions = recursiveExtract(data[id]);
  500. let concatenated = captions.join(', ');
  501. concatenated = concatenated.replace(/,\s*,/g, ',').replace(/\s\s+/g, ' ').replace(/,\s*$/, '');
  502. return concatenated;
  503. }
  504. metadata.prompt = extractAndCleanCaptions(comment, 'v4_prompt');
  505. metadata.negativePrompt = extractAndCleanCaptions(comment, 'v4_negative_prompt');
  506. console.log(comment.v4_prompt);
  507. console.log(comment.v4_negative_prompt);
  508. }
  509.  
  510. return metadata;
  511. } else if (exif['sd-metadata']) {
  512. metadata.rawMetadata = exif['sd-metadata'];
  513. const parameters = JSON.parse(exif['sd-metadata']);
  514. const rowPrompt = parameters.image.prompt[0].prompt;
  515. const PromptRegex = /[^[\]]+(?=\[|$)/g;
  516. const negativePromptRegex = /\[.*?\]/g;
  517. const promptArray = rowPrompt.match(PromptRegex);
  518. const negativePromptArray = rowPrompt.match(negativePromptRegex);
  519. const prompt = promptArray.map((prompt) => prompt.replace(/^\,|\,$/g, ''));
  520. const negativePrompt = negativePromptArray.map((prompt) =>
  521. prompt.replace(/^\[|\]$/g, '').replace(/^\,|\,$/g, ''),
  522. );
  523.  
  524. metadata.prompt = prompt.join(', ');
  525. metadata.negativePrompt = negativePrompt.join(', ');
  526. metadata['Steps'] = parameters?.image.steps;
  527. metadata['Model'] = parameters?.model;
  528. metadata['Model hash'] = parameters?.model_hash;
  529. metadata['Sampler'] = parameters?.image.sampler;
  530. metadata['CFG scale'] = parameters?.image.cfg_scale;
  531. metadata['Seed'] = parameters?.image.seed;
  532. metadata['Size'] = `${parameters?.image.width}x${parameters?.image.height}`;
  533. metadata['Software'] = 'InvokeAI';
  534.  
  535. return metadata;
  536. }
  537. } catch (error) {
  538. console.log(error);
  539. Swal.fire({
  540. icon: 'error',
  541. confirmButtonColor: `${colorClose}`,
  542. confirmButtonText: '닫기',
  543. title: '분석 오류',
  544. html: `
  545. ${error}<br>
  546. 오류내용과 이미지를 댓글로 알려주세요`,
  547. });
  548. }
  549. }
  550.  
  551. function infer(metadata) {
  552. if (metadata?.Software) return [metadata.Software];
  553. const inferList = [];
  554. const denoising = metadata?.['Denoising strength'];
  555. const hires = metadata?.['Hires upscaler'];
  556.  
  557. inferList.push('T2I');
  558. if (denoising && !hires) {
  559. inferList[0] = 'I2I';
  560. } else if (hires) {
  561. inferList.push('Hires. fix');
  562. }
  563. (metadata?.['AddNet Enabled'] ||
  564. metadata?.prompt?.includes('lora:') ||
  565. metadata?.negativePrompt?.includes('lora:')) &&
  566. inferList.push('LoRa');
  567. (metadata?.prompt?.includes('lyco:') || metadata?.negativePrompt?.includes('lyco:')) &&
  568. inferList.push('LyCORIS');
  569. (metadata?.['Hypernet'] ||
  570. metadata?.prompt?.includes('hypernet:') ||
  571. metadata?.negativePrompt?.includes('hypernet:')) &&
  572. inferList.push('Hypernet');
  573.  
  574. const controlNetRegex = /(ControlNet)/;
  575. for (const key in metadata) {
  576. if (controlNetRegex.test(key)) {
  577. inferList.push('ControlNet');
  578. break;
  579. }
  580. }
  581. metadata?.['SD upscale upscaler'] && inferList.push('SD upscale');
  582. metadata?.['Ultimate SD upscale upscaler'] && inferList.push('Ultimate SD upscale');
  583. metadata?.['Latent Couple'] && inferList.push('Latent Couple');
  584. metadata?.['Dynamic thresholding enabled'] && inferList.push('Dynamic thresholding');
  585. metadata?.['LLuL Enabled'] && inferList.push('LLuL');
  586. metadata?.['Cutoff enabled'] && inferList.push('Cutoff');
  587. metadata?.['Tiled Diffusion'] && inferList.push('Tiled Diffusion');
  588. metadata?.['DDetailer model a'] && inferList.push('DDetailer');
  589. metadata?.['ADetailer version'] && inferList.push('ADetailer'); // DDetailer/ADetailer를 DINO 하나로 묶는 게 나을까?
  590.  
  591. return inferList;
  592. }
  593.  
  594. function showAndHide(elementSelector) {
  595. const contentEls = document.querySelectorAll(elementSelector);
  596.  
  597. contentEls.forEach((contentEl) => {
  598. const containerEl = contentEl.parentElement;
  599. const showMoreEl = containerEl.nextElementSibling;
  600.  
  601. if (contentEl.offsetHeight > containerEl.offsetHeight) {
  602. showMoreEl.style.display = 'block';
  603. containerEl.classList.add('md-hidden');
  604. } else {
  605. showMoreEl.style.display = 'none';
  606. containerEl.classList.remove('md-hidden');
  607. containerEl.classList.add('md-visible');
  608. }
  609.  
  610. showMoreEl.addEventListener('click', () => {
  611. const isMore = showMoreEl.textContent === '더 보기';
  612. showMoreEl.textContent = isMore ? '숨기기' : '더 보기';
  613. containerEl.classList.toggle('md-hidden', !isMore);
  614. containerEl.classList.toggle('md-visible', isMore);
  615. });
  616. });
  617. }
  618.  
  619. function showMetadataModal(metadata, url) {
  620. metadata = parseMetadata(metadata);
  621. const inferList = infer(metadata);
  622. const showMeta = Swal.mixin({
  623. title: '메타데이터 요약',
  624. html: /*html*/ `
  625. <div class="md-grid">
  626. <div class="md-grid-item">
  627. <div class="md-title">Prompt <span class="md-copy md-button" data-clipboard-target="#prompt"></span></div>
  628. <div class="md-info" id="prompt">
  629. ${metadata.prompt ?? '정보 없음'}
  630. </div>
  631. </div>
  632. <div class="md-grid-item">
  633. <div class="md-title">Negative Prompt
  634. <span class="md-copy md-button" data-clipboard-target="#negative-prompt"></span>
  635. </div>
  636. <div class="md-info">
  637. <div class="md-hidden">
  638. <div class="md-show-and-hide" id="negative-prompt">
  639. ${metadata.negativePrompt ?? '정보 없음'}
  640. </div>
  641. </div>
  642. <div class="md-show-more">더 보기</div>
  643. </div>
  644. </div>
  645. <div class="md-grid-item">
  646. <div class="md-nested-grid">
  647. <div>
  648. <div class="md-title">Sampler <span class="md-copy md-button" data-clipboard-target="#sampler"></span></div>
  649. <div class="md-info" id="sampler">${metadata['Sampler'] ?? '정보 없음'}</div>
  650. </div>
  651. <div>
  652. <div class="md-title">Seed <span class="md-copy md-button" data-clipboard-target="#seed"></span></div>
  653. <div class="md-info" id="seed">${metadata['Seed'] ?? '정보 없음'}</div>
  654. </div>
  655. <div>
  656. <div class="md-title">Steps <span class="md-copy md-button" data-clipboard-target="#steps"></span></div>
  657. <div class="md-info" id="steps">${metadata['Steps'] ?? '정보 없음'}</div>
  658. </div>
  659. <div>
  660. <div class="md-title">Size <span class="md-copy md-button" data-clipboard-target="#size"></span></div>
  661. <div class="md-info" id="size">${metadata['Size'] ?? '정보 없음'}</div>
  662. </div>
  663. <div>
  664. <div class="md-title">CFG scale <span class="md-copy md-button" data-clipboard-target="#cfg-scale"></span></div>
  665. <div class="md-info" id="cfg-scale">${metadata['CFG scale'] ?? '정보 없음'}</div>
  666. </div>
  667. <div>
  668. <div class="md-title">Denoising strength <span class="md-copy md-button" data-clipboard-target="#denoising-strength"></span></div>
  669. <div class="md-info" id="denoising-strength">${metadata['Denoising strength'] ?? '정보 없음'}</div>
  670. </div>
  671. <div class="md-model">
  672. <div class="md-title">Model
  673. <span class="md-copy md-button" data-clipboard-target="#model"></span>
  674. <a href='https://civitai.com/?query=${
  675. metadata['Model hash']
  676. }' target='_blank'><span class="md-civitai md-button"></span></a>
  677. </div>
  678. <div class="md-info" id="model">${
  679. metadata['Model']
  680. ? `${metadata['Model']} [${metadata['Model hash']}]`
  681. : (metadata['Model hash'] ?? '정보 없음')
  682. }</div>
  683. </div>
  684. <div>
  685. <div class="md-title">Infer...</div>
  686. <div class="md-info">${inferList.join(', ')}</div>
  687. </div>
  688. </div>
  689. </div>
  690. `,
  691. footer: /*html*/ `
  692. <div class="md-grid-item">
  693. <div class="md-title">Raw Metadata <span class="md-copy md-button" data-clipboard-target="#raw-metadata"></span>
  694. </div>
  695. <div class="md-info">
  696. <div class="md-hidden">
  697. <pre class="md-show-and-hide" id="raw-metadata">
  698. ${metadata.rawMetadata ?? '정보 없음'}
  699. </pre>
  700. </div>
  701. <div class="md-show-more">더 보기</div>
  702. </div>
  703. ${footerString}
  704. </div>
  705. `,
  706. width: '50em',
  707. showDenyButton: true,
  708. showCancelButton: true,
  709. focusCancel: true,
  710. confirmButtonColor: `${colorOption1}`,
  711. denyButtonColor: `${colorOption2}`,
  712. cancelButtonColor: `${colorClose}`,
  713. confirmButtonText: '이미지 열기',
  714. denyButtonText: '이미지 저장',
  715. cancelButtonText: '닫기',
  716. });
  717.  
  718. // if image has URL, options are available to open in new tab or download
  719. if (url != null) {
  720. showMeta.fire().then((result) => {
  721. if (result.isConfirmed) {
  722. window.open(url, '_blank');
  723. } else if (result.isDenied) {
  724. GM_download(url, getFileName(url));
  725. }
  726. });
  727. } else {
  728. // if image has no URL, then it must have been dragged and dropped, hence no open in new tab or download options
  729. showMeta.fire({
  730. showDenyButton: false,
  731. showCancelButton: false,
  732. focusCancel: false,
  733. focusConfirm: true,
  734. confirmButtonColor: `${colorClose}`,
  735. confirmButtonText: '닫기',
  736. });
  737. }
  738. showAndHide('.md-show-and-hide');
  739. }
  740.  
  741. function showTagExtractionModal(url, blob) {
  742. let noMeta = Swal.mixin({
  743. footer: `
  744. <div style="width: 100%;">
  745. <div class="md-info" style="text-align: center;">
  746. <a href="${url}" target="_blank">Open image...</a>
  747. </div>
  748. ${footerString}
  749. </div>
  750. `,
  751. });
  752. if (url == null) {
  753. noMeta = Swal.mixin({
  754. footer: `
  755. <div style="width: 100%;">
  756. ${footerString}
  757. </div>
  758. `,
  759. });
  760. }
  761.  
  762. function getOptimizedImageURL(url) {
  763. if (isArca) {
  764. return url.replace('ac.namu.la', 'ac-o.namu.la').replace('&type=orig', '');
  765. }
  766. if (isPixiv) {
  767. const extension = url.substring(url.lastIndexOf('.') + 1);
  768. return url
  769. .replace('/img-original/', '/c/600x1200_90_webp/img-master/')
  770. .replace(`.${extension}`, '_master1200.jpg');
  771. }
  772. }
  773. noMeta
  774. .fire({
  775. icon: 'error',
  776. title: '메타데이터 없음!',
  777. text: '찾아볼까요?',
  778. showCancelButton: true,
  779. showDenyButton: true,
  780. confirmButtonText: 'Danbooru Autotagger',
  781. denyButtonText: 'WD 1.4 Tagger',
  782. cancelButtonText: '아니오',
  783. showLoaderOnConfirm: true,
  784. showLoaderOnDeny: true,
  785. focusCancel: true,
  786. confirmButtonColor: `${colorOption1}`,
  787. denyButtonColor: `${colorOption2}`,
  788. cancelButtonColor: `${colorClose}`,
  789. backdrop: true,
  790. preConfirm: async () => {
  791. if (url != null) {
  792. const res = await GM_fetch(getOptimizedImageURL(url), {
  793. headers: {
  794. Referer: `${location.protocol}//${location.hostname}`,
  795. },
  796. });
  797. blob = await res.blob();
  798. }
  799. let formData = new FormData();
  800. formData.append('threshold', '0.4');
  801. formData.append('format', 'json');
  802. formData.append('file', blob);
  803.  
  804. return GM_fetch('https://autotagger.donmai.us/evaluate', {
  805. method: 'POST',
  806. body: formData,
  807. })
  808. .then((res) => {
  809. if (!res.status === 200) {
  810. Swal.showValidationMessage(`https://autotagger.donmai.us 접속되는지 확인!`);
  811. }
  812. return res.json();
  813. })
  814. .catch((error) => {
  815. console.log(error);
  816. Swal.showValidationMessage(`https://autotagger.donmai.us 접속되는지 확인!`);
  817. });
  818. },
  819. preDeny: async () => {
  820. if (url != null) {
  821. const res = await GM_fetch(getOptimizedImageURL(url), {
  822. headers: {
  823. Referer: `${location.protocol}//${location.hostname}`,
  824. },
  825. });
  826. blob = await res.blob();
  827. }
  828. const optimizedBase64 = await blobToBase64(blob);
  829.  
  830. return fetch('https://smilingwolf-wd-v1-4-tags.hf.space/run/predict', {
  831. method: 'POST',
  832. headers: {
  833. 'Content-Type': 'application/json',
  834. },
  835. body: JSON.stringify({
  836. data: [optimizedBase64, 'SwinV2', 0.35, 0.85],
  837. }),
  838. })
  839. .then((res) => res.json())
  840. .catch((error) => {
  841. Swal.showValidationMessage(error);
  842. });
  843. },
  844. allowOutsideClick: () => !Swal.isLoading(),
  845. })
  846. .then((result) => {
  847. if (result.isDismissed) return;
  848. let tags;
  849. if (result.isConfirmed) {
  850. tags = Object.keys(result.value[0].tags).join(', ').replaceAll('_', ' ');
  851. } else if (result.isDenied) {
  852. tags = result.value.data[3]?.label
  853. ? `${result.value.data[3]?.label}, ${result.value.data[0]}`
  854. : result.value.data[0];
  855. }
  856.  
  857. Swal.fire({
  858. confirmButtonColor: `${colorClose}`,
  859. confirmButtonText: '닫기',
  860. html: /*html*/ `
  861. <div class="md-title">Output
  862. <span class="md-copy md-button" data-clipboard-target="#md-tags"></span>
  863. </div>
  864. <div class="md-info" id="md-tags">${tags}</div>
  865. `,
  866. });
  867. });
  868. }
  869.  
  870. function fileToBlob(file) {
  871. return new Promise((resolve) => {
  872. const reader = new FileReader();
  873. reader.onload = () =>
  874. resolve(
  875. new Blob([reader.result], {
  876. type: file.type,
  877. }),
  878. );
  879. reader.readAsArrayBuffer(file);
  880. });
  881. }
  882.  
  883. function blobToBase64(blob) {
  884. return new Promise((resolve) => {
  885. const reader = new FileReader();
  886. reader.onloadend = () => resolve(reader.result);
  887. reader.readAsDataURL(blob);
  888. });
  889. }
  890.  
  891. function notSupportedFormat() {
  892. toastmix.fire({
  893. position: 'top-end',
  894. icon: 'error',
  895. title: '지원하지 않는 파일 형식입니다.',
  896. });
  897. }
  898.  
  899. function isSupportedImageFormat(url) {
  900. const supportedExtensions = /\.(png|jpe?g|webp)|image\/(jpeg|webp|png)/;
  901. return supportedExtensions.test(url);
  902. }
  903.  
  904. function handleUploadable(MIME) {
  905. const uploadableSubtypes = /(jpe?g|jfif|pjp|png|gif|web[pm]|mov|mp4|m4[ab])/;
  906. const [type, subtype] = MIME.split('/');
  907. if (uploadableSubtypes.test(subtype)) {
  908. return type;
  909. } else {
  910. return null;
  911. }
  912. }
  913.  
  914. async function getStealthExif(src) {
  915. let canvas = document.createElement('canvas');
  916. let ctx = canvas.getContext('2d', { willReadFrequently: true, alpha: true });
  917. let img = new Image();
  918. img.src = src;
  919. img.crossOrigin = 'Anonymous';
  920.  
  921. await img.decode();
  922.  
  923. canvas.width = img.width;
  924. canvas.height = img.height;
  925. ctx.drawImage(img, 0, 0);
  926.  
  927. let binary = '';
  928. const signature = 'stealth_pngcomp';
  929. let index = 0;
  930. let reading = 'signature';
  931. let length = 0;
  932.  
  933. for (let x = 0; x < img.width; x++) {
  934. for (let y = 0; y < img.height; y++) {
  935. let data = ctx.getImageData(x, y, 1, 1).data;
  936. let a = data[3];
  937. binary += String(a & 1);
  938. index++;
  939.  
  940. if (reading == 'signature') {
  941. if (index == signature.length * 8) {
  942. let str = '';
  943. for (let i = 0; i < binary.length / 8; i++) {
  944. str += String.fromCharCode(parseInt(binary.substring(i * 8, i * 8 + 8), 2));
  945. }
  946.  
  947. if (str == signature) {
  948. reading = 'length';
  949. binary = '';
  950. index = 0;
  951. } else {
  952. return null;
  953. }
  954. }
  955. } else if (reading == 'length') {
  956. if (index == 32) {
  957. length = parseInt(binary, 2);
  958. reading = 'data';
  959. binary = '';
  960. index = 0;
  961. }
  962. } else if (reading == 'data') {
  963. if (index == length) {
  964. let array = new Uint8Array(length);
  965. for (let i = 0; i < binary.length / 8; i++) {
  966. array[i] = parseInt(binary.substring(i * 8, i * 8 + 8), 2);
  967. }
  968.  
  969. let temp = pako.ungzip(array);
  970. let prompt = new TextDecoder('utf-8').decode(temp);
  971. return JSON.parse(prompt);
  972. }
  973. }
  974. }
  975. }
  976.  
  977. return null;
  978. }
  979.  
  980. async function extractImageMetadata(blob, type) {
  981. try {
  982. switch (type) {
  983. case 'image/jpeg':
  984. case 'image/webp': {
  985. const exif = exifLib.load(await blobToBase64(blob));
  986. const parameters = exif.Exif[37510].replace('UNICODE', '').replaceAll('\u0000', '');
  987. return {
  988. parameters,
  989. };
  990. }
  991. case 'image/png': {
  992. const chunks = UPNG.decode(await blob.arrayBuffer());
  993. let parameters = chunks.tabs.tEXt?.parameters || chunks.tabs.iTXt?.parameters;
  994. const description = chunks.tabs.tEXt?.Description || chunks.tabs.iTXt?.Description;
  995. if (parameters) {
  996. return {
  997. parameters,
  998. };
  999. } else if (description) {
  1000. return chunks.tabs?.tEXt || chunks.tabs?.iTXt;
  1001. } else {
  1002. return null;
  1003. }
  1004. }
  1005. }
  1006. } catch (error) {
  1007. console.log(error);
  1008. return null;
  1009. }
  1010. }
  1011.  
  1012. async function fetchAndDecode(url) {
  1013. try {
  1014. let response, contentType, reader;
  1015. const Referer = `${location.protocol}//${location.hostname}`;
  1016. if (isArca) {
  1017. response = await fetch(url.replace('ac.namu.la', 'ac-o.namu.la'));
  1018. contentType = response.headers.get('content-type');
  1019. reader = response.body.getReader();
  1020. } else if (useTampermonkey) {
  1021. response = await new Promise((resolve) => {
  1022. GM_xmlhttpRequest({
  1023. url,
  1024. responseType: 'stream',
  1025. headers: {
  1026. Referer,
  1027. },
  1028. onreadystatechange: (data) => {
  1029. resolve(data);
  1030. },
  1031. });
  1032. });
  1033. const headers = Object.fromEntries(
  1034. response.responseHeaders.split('\n').map((line) => {
  1035. const [key, value] = line.split(':').map((part) => part.trim());
  1036. return [key, value];
  1037. }),
  1038. );
  1039. contentType = headers['content-type'];
  1040. reader = response.response.getReader();
  1041. } else {
  1042. response = await GM_fetch(url, {
  1043. headers: {
  1044. Referer,
  1045. },
  1046. });
  1047. contentType = response.headers.get('content-type');
  1048. reader = response.body.getReader();
  1049. }
  1050. if (
  1051. (isPixiv && !url.includes('.jpg') && contentType === 'text/html') ||
  1052. (isPixiv && url.includes('.jpg'))
  1053. ) {
  1054. url = url.replace('.png', '.jpg');
  1055. showTagExtractionModal(url);
  1056. return;
  1057. }
  1058.  
  1059. let metadata;
  1060. let chunks = [];
  1061. while (true) {
  1062. const { done, value } = await reader.read();
  1063. if (done || metadata || metadata === null) {
  1064. reader.cancel();
  1065. break;
  1066. }
  1067. switch (contentType) {
  1068. case 'image/jpeg':
  1069. metadata = getMetadataJPEGChunk(value);
  1070. break;
  1071. case 'image/png':
  1072. metadata = getMetadataPNGChunk(value);
  1073. metadata?.IDAT && reader.cancel();
  1074. break;
  1075. case 'image/webp':
  1076. chunks.push(value);
  1077. break;
  1078. default:
  1079. notSupportedFormat();
  1080. reader.cancel();
  1081. break;
  1082. }
  1083. }
  1084. if (contentType === 'image/webp') {
  1085. const blob = new Blob(chunks, {
  1086. type: 'image/webp',
  1087. });
  1088. const base64 = await blobToBase64(blob);
  1089. const exif = exifLib.load(base64);
  1090. const parameters = exif.Exif[37510].replace('UNICODE', '').replaceAll('\u0000', '');
  1091. metadata = {
  1092. parameters,
  1093. };
  1094. }
  1095. const stealthData = await getStealthExif(url);
  1096. if ((await stealthData) != null) {
  1097. console.log('stealth data:', await stealthData);
  1098. return await stealthData;
  1099. }
  1100. console.log('EXIF data:', metadata);
  1101. return metadata;
  1102. } catch (error) {
  1103. console.log(error);
  1104. return null;
  1105. }
  1106. }
  1107.  
  1108. async function extract(url) {
  1109. if (!isSupportedImageFormat(url)) {
  1110. notSupportedFormat();
  1111. return;
  1112. }
  1113.  
  1114. Swal.fire({
  1115. title: '로드 중!',
  1116. width: '15rem',
  1117. didOpen: () => {
  1118. Swal.showLoading();
  1119. },
  1120. });
  1121.  
  1122. console.time('modal open');
  1123. console.time('fetch');
  1124. const metadata = await fetchAndDecode(url);
  1125. console.timeEnd('fetch');
  1126. console.log(metadata);
  1127.  
  1128. if (metadata?.Description || metadata?.parameters || metadata?.['sd-metadata']) {
  1129. showMetadataModal(metadata, url);
  1130. } else {
  1131. showTagExtractionModal(url);
  1132. }
  1133. console.timeEnd('modal open');
  1134. }
  1135.  
  1136. function getCSRFToken() {
  1137. return new Promise((resolve) => {
  1138. const csrf = document.querySelector('input[name=_csrf]');
  1139. const token = document.querySelector('input[name=token]');
  1140. if (csrf && token) {
  1141. resolve([csrf.value, token.value]);
  1142. }
  1143. });
  1144. }
  1145.  
  1146. function uploadArca(blob, type, saveEXIF = true, token = null) {
  1147. return new Promise(async (resolve, reject) => {
  1148. let swalText = '비디오는 EXIF 보존 설정에 영향을 받지 않습니다.';
  1149. if (type == 'image') {
  1150. swalText = 'EXIF 보존: ' + saveEXIF;
  1151. }
  1152. let xhr = new XMLHttpRequest();
  1153. xhr.upload.addEventListener('progress', null, false);
  1154. let formData = new FormData();
  1155. if (!document.querySelector('#article_write_form > input[name=token]')) {
  1156. await getCSRFToken().then((tokenList) => {
  1157. token = tokenList[1];
  1158. });
  1159. }
  1160.  
  1161. formData.append('upload', blob);
  1162. formData.append('token', token || document.querySelector('#article_write_form > input[name=token]').value);
  1163. formData.append('saveExif', saveEXIF);
  1164. formData.append('saveFilename', false);
  1165.  
  1166. xhr.onload = function () {
  1167. let response = JSON.parse(xhr.responseText);
  1168. if (response.uploaded === true) {
  1169. resolve(response.url);
  1170. } else {
  1171. Swal.close();
  1172. console.error(xhr.responseText);
  1173. toastmix.fire({
  1174. icon: 'error',
  1175. title: `업로드 오류`,
  1176. });
  1177. }
  1178. };
  1179. xhr.open('POST', 'https://arca.live/b/upload');
  1180. xhr.send(formData);
  1181. Swal.fire({
  1182. title: '파일 업로드중',
  1183. text: swalText,
  1184. showConfirmButton: false,
  1185. allowOutsideClick: false,
  1186. didOpen: () => {
  1187. Swal.showLoading();
  1188. },
  1189. });
  1190. });
  1191. }
  1192.  
  1193. const { hostname, href, pathname } = location;
  1194. const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
  1195. const isPixiv = hostname === 'www.pixiv.net';
  1196. const isArca = hostname === 'arca.live';
  1197. const isArcaViewer = /(arca.live)(\/)(b\/.*)(\/)(\d*)/.test(href);
  1198. const isArcaEditor = /(arca.live\/b\/.*\/)(edit|write)/.test(href);
  1199. const useTampermonkey = GM_xmlhttpRequest?.RESPONSE_TYPE_STREAM && true;
  1200. const isPixivDragUpload = pathname === '/illustration/create' || pathname === '/upload.php';
  1201.  
  1202. if (GM_getValue('usePixiv', false) && isPixiv) {
  1203. function getOriginalUrl(url) {
  1204. const extension = url.substring(url.lastIndexOf('.') + 1);
  1205. const originalUrl = url
  1206. .replace('/c/600x1200_90_webp/img-master/', '/img-original/')
  1207. .replace('/c/100x100/img-master/', '/img-original/')
  1208. .replace('_master1200', '')
  1209. .replace(`.${extension}`, '.png');
  1210. return originalUrl;
  1211. }
  1212.  
  1213. let isAi = false;
  1214. if (!isMobile) {
  1215. document.arrive('footer > ul > li > span > a', function () {
  1216. if (this.href === 'https://www.pixiv.help/hc/articles/11866167926809') isAi = true;
  1217. });
  1218. document.arrive('div[role=presentation]:last-child > div > div', function () {
  1219. isAi && this.click();
  1220. });
  1221. } else {
  1222. document.arrive('a.ai-generated', () => {
  1223. isAi = true;
  1224. });
  1225. document.arrive('button.nav-back', function () {
  1226. isAi && this.click();
  1227. });
  1228. }
  1229.  
  1230. document.arrive('a > img', function () {
  1231. if (this.alt === 'pixiv') return;
  1232.  
  1233. if (isAi) {
  1234. let src;
  1235. if (!isMobile) {
  1236. src = this.parentNode.href;
  1237. } else {
  1238. src = getOriginalUrl(this.src);
  1239. }
  1240.  
  1241. this.onclick = function () {
  1242. extract(src);
  1243. };
  1244. }
  1245. });
  1246. }
  1247.  
  1248. if (isArcaViewer) {
  1249. document.arrive(
  1250. 'a[href$="type=orig"] > img',
  1251. {
  1252. existing: true,
  1253. },
  1254. function () {
  1255. if (this.classList.contains('channel-icon')) return;
  1256.  
  1257. this.parentNode.onclick = (event) => {
  1258. if (event.button === 0) {
  1259. event.preventDefault();
  1260. }
  1261. };
  1262. this.onclick = function () {
  1263. const src = `${this.src}&type=orig`;
  1264. extract(src);
  1265. };
  1266. },
  1267. );
  1268. }
  1269.  
  1270. let ArcaDragUpload = true;
  1271. if (isArcaEditor) {
  1272. if (GM_getValue('saveExifDefault', true)) {
  1273. document.arrive(
  1274. '.images-multi-upload',
  1275. {
  1276. onceOnly: true,
  1277. },
  1278. () => {
  1279. document.getElementById('saveExif').checked = true;
  1280. },
  1281. );
  1282. }
  1283. if (!GM_getValue('useDragdropUpload', true)) ArcaDragUpload = false;
  1284. }
  1285.  
  1286. !isMobile && !isPixivDragUpload && ArcaDragUpload && new DropZone();
  1287. GM_addStyle(modalCSS);
  1288. new ClipboardJS('.md-copy');
  1289. registerMenu();
  1290. })();