Easy Compare

Compare images

  1. // ==UserScript==
  2. // @name Easy Compare
  3. // @description Compare images
  4. // @version 0.9.5
  5. // @author Secant (TYT@NexusHD)
  6. // @license GPL-3.0-or-later
  7. // @supportURL zzwu@zju.edu.cn
  8. // @contributionURL https://i.loli.net/2020/02/28/JPGgHc3UMwXedhv.jpg
  9. // @contributionAmount 10
  10. // @include *
  11. // @require https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js
  12. // @require https://greasyfork.org/scripts/401377-pixelmatch/code/pixelmatch.js
  13. // @resource PixelMatchCore https://greasyfork.org/scripts/401377-pixelmatch/code/pixelmatch.js
  14. // @namespace https://greasyfork.org/users/152136
  15. // @icon data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23008000'%3E%3Cpath id='ld' d='M20 6H10c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h10v4h4V2h-4v4zm0 30H10l10-12v12zM38 6H28v4h10v26L28 24v18h10c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4z'/%3E%3C/svg%3E
  16. // @grant GM_xmlhttpRequest
  17. // @grant GM_download
  18. // @grant GM_getValue
  19. // @grant GM_setValue
  20. // @grant GM_getResourceText
  21. // @grant unsafewindow
  22. // @connect hdbits.org
  23. // @connect awesome-hd.me
  24. // @connect ptpimg.me
  25. // @connect imgbox.com
  26. // @connect malzo.com
  27. // @connect imagebam.com
  28. // @connect pixhost.to
  29. // @connect loli.net
  30. // @connect funkyimg.com
  31. // @connect ilikeshots.club
  32. // @connect z4a.net
  33. // @connect picgd.com
  34. // @connect tu.totheglory.im
  35. // @connect tpimg.ccache.org
  36. // @connect pterclub.com
  37. // @connect catbox.moe
  38. // @connect sm.ms
  39. // @connect broadcasthe.net
  40. // @connect *
  41. // ==/UserScript==
  42. // jshint esversion:8, -W054
  43. (async function ($, Mousetrap, pixelmatch, URL) {
  44. 'use strict';
  45.  
  46. /*--- Preparation ---*/
  47. // Mousetrap Pause Plugin
  48. if (Mousetrap) {
  49. let target = Mousetrap.prototype || Mousetrap;
  50. const _originalStopCallback = target.stopCallback;
  51. target.stopCallback = function (e, element, combo) {
  52. var self = this;
  53. if (self.paused) {
  54. return true;
  55. }
  56. return _originalStopCallback.call(self, e, element, combo);
  57. };
  58. target.pause = function () {
  59. var self = this;
  60. self.paused = true;
  61. };
  62. target.unpause = function () {
  63. var self = this;
  64. self.paused = false;
  65. };
  66. try {
  67. Mousetrap.init();
  68. } catch (_) { }
  69. }
  70.  
  71. /*--- Global Contexts ---*/
  72. // A global timeout ID holder
  73. let timeout;
  74. // A global scale factor
  75. let scale = 10;
  76. // Regex replacement array that converts thumbs to originals
  77. const t2oLib = [
  78. [/\.thumb\.jpe?g$/, ''], // nexusphp
  79. [/\.md\.png$/, '.png'], // m-team
  80. [/\.th\.png$/, '.png'], // pterclub
  81. [/_thumb\.png$/, '.png'], // totheglory
  82. [/img\.awesome\-hd\.me\/t(\/\d+)?\//, 'img.awesome-hd.me/images/'], // awesome-hd
  83. [/thumbs((?:\d+)?\.imgbox\.com\/.+_)t\.png$/, 'images$1o.png'], // imgbox
  84. [/t((?:\d+)?\.pixhost\.to\/)thumbs\//, 'img$1images/'], // pixhost
  85. [/t(\.hdbits\.org\/.+)\.jpg$/, 'i$1.png'], // hdbits
  86. [/^.*?imagecache\.php\?url=(https?)%3A%2F%2Fthumbs(\d+)?\.imgbox\.com%2F(\w+)%2F(\w+)%2F(\w+)_t\.png/, '$1://images$2.imgbox.com/$3/$4/$5_o.png']
  87. ];
  88. // Skip redirections
  89. const skipRedirLib = [
  90. [/^https?:\/\/anonym\.to\/\?(.*)$/, (_, p1) => decodeURIComponent(p1)],
  91. [/^https?:\/\/www\.dereferer\.org\/\?(.*)$/, (_, p1) => decodeURIComponent(p1)],
  92. [/^(?:https?:\/\/pterclub\.com)?\/link\.php\?sign=.+?&target=(.*)$/, (_, p1) => decodeURIComponent(p1.replace(/\+/g, ' ')).replace(/ /g, '%20')],
  93. [/^.*?imagecache\.php\?url=(.*)$/, (_, p1) => decodeURIComponent(p1.replace(/\+/g, ' ')).replace(/ /g, '%20')]
  94. ];
  95. // Probable original image selectors on a view page
  96. const guessSelectorLib = [
  97. '#image-viewer-container>img',
  98. '.image-container img',
  99. 'div.img.big>img',
  100. 'img.mainimage',
  101. 'img.main-image',
  102. 'img#img'
  103. ];
  104. // Filter function mapping
  105. const filterImage = {
  106. 'solar': img => rgbImage(img, solarWorker || rgbSolarCurve),
  107. 's2lar': img => rgbImage(img, s2larWorker || rgbS2larCurve)
  108. };
  109.  
  110. /*--- Workers Initialization ---*/
  111. // Solar Curve
  112. function solarCurve(x, t = 5, k = 5.5) {
  113. const m = (k * Math.PI - 128 / t);
  114. const A = -1 / 4194304 * m;
  115. const B = 3 / 32768 * m;
  116. const C = 1 / t;
  117. return Math.round(
  118. 127.9999 * Math.sin(
  119. A * x ** 3 + B * x ** 2 + C * x - Math.PI / 2
  120. ) + 127.5
  121. ) || 0;
  122. }
  123. let rgbSolarCurve = GM_getValue('solarCurve');
  124. let rgbS2larCurve = GM_getValue('s2larCurve');
  125. if (!rgbSolarCurve) {
  126. rgbSolarCurve = [
  127. Array.from({ length: 256 }, (_, x) => solarCurve(x)),
  128. Array.from({ length: 256 }, (_, x) => solarCurve(x - 5)),
  129. Array.from({ length: 256 }, (_, x) => solarCurve(x + 5))
  130. ];
  131. GM_setValue('solarCurve', JSON.stringify(rgbSolarCurve));
  132. rgbS2larCurve = [
  133. Array.from({ length: 256 }, (_, x) => rgbSolarCurve[0][[rgbSolarCurve[0][x]]]),
  134. Array.from({ length: 256 }, (_, x) => rgbSolarCurve[1][[rgbSolarCurve[1][x]]]),
  135. Array.from({ length: 256 }, (_, x) => rgbSolarCurve[2][[rgbSolarCurve[2][x]]])
  136. ];
  137. GM_setValue('s2larCurve', JSON.stringify(rgbS2larCurve));
  138. } else {
  139. rgbSolarCurve = JSON.parse(rgbSolarCurve);
  140. rgbS2larCurve = JSON.parse(rgbS2larCurve);
  141. }
  142. rgbSolarCurve = rgbSolarCurve.map(e => new Uint8Array(e));
  143. rgbS2larCurve = rgbS2larCurve.map(e => new Uint8Array(e));
  144. async function loadBuffer(worker, [R, G, B]) {
  145. return new Promise((resolve) => {
  146. worker.onmessage = (e) => {
  147. resolve(e.data.result);
  148. };
  149. worker.postMessage({
  150. R: R.buffer,
  151. G: G.buffer,
  152. B: B.buffer
  153. }, [R.buffer, G.buffer, B.buffer]);
  154. });
  155. }
  156. // Diff, Solar, S2lar Worker Initialization
  157. function diffWork(f) {
  158. f.apply(self);
  159. const u = Uint8ClampedArray;
  160. self.onmessage = ({ data: { key, img1, img2, width, height, init } }) => {
  161. img1 = new u(img1);
  162. img2 = new u(img2);
  163. const diff = new u(img1);
  164. try {
  165. self.pixelmatch(img1, img2, diff, width, height, init);
  166. self.postMessage({
  167. diff: diff.buffer,
  168. width: width,
  169. height: height,
  170. key: key
  171. }, [diff.buffer]);
  172. } catch (err) {
  173. console.warn(err);
  174. self.postMessage({
  175. diff: null,
  176. key: key
  177. });
  178. }
  179. };
  180. }
  181. function rgbWork(f) {
  182. const u = Uint8ClampedArray;
  183. self.onmessage = ({ data: { key, R, G, B, img, width, height } }) => {
  184. if (R && G && B) {
  185. self.RGB = [new u(R), new u(G), new u(B)];
  186. self.postMessage({ result: true });
  187. } else {
  188. img = new u(img);
  189. const filter = new u(img);
  190. try {
  191. f.apply(self, [img, filter, width, height, self.RGB]);
  192. self.postMessage({
  193. filter: filter.buffer,
  194. width: width,
  195. height: height,
  196. key: key
  197. }, [filter.buffer]);
  198. } catch (err) {
  199. console.warn(err);
  200. self.postMessage({
  201. filter: null,
  202. key: key
  203. });
  204. }
  205. }
  206. };
  207. }
  208. function stringifyWork(workFun, arg) {
  209. return `(${workFun.toString()})(${arg})`;
  210. }
  211. let diffWorker, solarWorker, s2larWorker;
  212. let loadBufferPromise;
  213. try {
  214. const diffWorkerBlob = new Blob([
  215. stringifyWork(diffWork, new Function(
  216. GM_getResourceText('PixelMatchCore')
  217. ))
  218. ], { type: 'application/javascript' });
  219. diffWorker = new Worker(URL.createObjectURL(diffWorkerBlob));
  220. diffWorker.keyPool = {};
  221. URL.revokeObjectURL(diffWorkerBlob);
  222. const rgbWorkerBlob = new Blob([stringifyWork(rgbWork, rgbRemap)], { type: 'application/javascript' });
  223. const rgbWorkerURL = URL.createObjectURL(rgbWorkerBlob);
  224. solarWorker = new Worker(rgbWorkerURL);
  225. solarWorker.keyPool = {};
  226. const transSo = loadBuffer(solarWorker, rgbSolarCurve);
  227. s2larWorker = new Worker(rgbWorkerURL);
  228. s2larWorker.keyPool = {};
  229. const transS2 = loadBuffer(s2larWorker, rgbS2larCurve);
  230. URL.revokeObjectURL(rgbWorkerURL);
  231. loadBufferPromise = Promise.all([transSo, transS2]);
  232. } catch (e) {
  233. try {
  234. const diffWorkerDataURI = `data:application/javascript,${
  235. encodeURIComponent(
  236. stringifyWork(diffWork, new Function(
  237. GM_getResourceText('PixelMatchCore')
  238. ))
  239. )}`;
  240. diffWorker = new Worker(diffWorkerDataURI);
  241. diffWorker.keyPool = {};
  242. const rgbWorkerDataURI = `data:application/javascript,${
  243. encodeURIComponent(
  244. stringifyWork(rgbWork, rgbRemap)
  245. )}`;
  246. solarWorker = new Worker(rgbWorkerDataURI);
  247. solarWorker.keyPool = {};
  248. const transSo = loadBuffer(solarWorker, rgbSolarCurve);
  249. s2larWorker = new Worker(rgbWorkerDataURI);
  250. s2larWorker.keyPool = {};
  251. const transS2 = loadBuffer(s2larWorker, rgbS2larCurve);
  252. loadBufferPromise = Promise.all([transSo, transS2]);
  253. } catch (e) {
  254. diffWorker = null;
  255. solarWorker = null;
  256. }
  257. }
  258.  
  259. /*--- Helper Functions ---*/
  260. // Virtual DOM for selection without fetching images
  261. function $$(htmlString) {
  262. return $(htmlString, document.implementation.createHTMLDocument('virtual'));
  263. }
  264. // Function to make an <canvas/> element
  265. function makeCanvas(outlineColor = 'red') {
  266. const $figure = $('<figure/>').css({
  267. 'width': 'fit-content',
  268. 'position': 'fixed',
  269. 'top': '50%',
  270. 'left': '50%',
  271. 'margin': '0',
  272. 'vertical-align': 'middle'
  273. });
  274. const $canvas = $(`<canvas/>`).css({
  275. 'display': 'none',
  276. 'transform': 'translate(-50%, -50%)',
  277. 'opacity': '1',
  278. 'outline': '3px solid ' + outlineColor,
  279. 'outline-offset': '2px',
  280. });
  281. $figure.append($canvas);
  282. return $canvas[0];
  283. }
  284. // Draw text on canvas
  285. function drawText(canvas, text, font = '16px sans serif', fillStyle = 'rgba(255,255,255,255)') {
  286. const context = canvas.getContext('2d');
  287. context.font = font;
  288. canvas.width = context.measureText(text).width;
  289. canvas.height = 20;
  290. context.font = font;
  291. context.fillStyle = fillStyle;
  292. context.fillText(text, 0, 15);
  293. }
  294. // Draw image on canvas
  295. function drawImage(canvas, imageData) {
  296. canvas.width = imageData.width;
  297. canvas.height = imageData.height;
  298. canvas.getContext('2d').putImageData(imageData, 0, 0);
  299. }
  300. // Guess original image src from view page
  301. function guessOriginalImage(url) {
  302. return new Promise((resolve) => {
  303. GM_xmlhttpRequest({
  304. url: url,
  305. method: 'GET',
  306. timeout: 6000,
  307. onload: (x) => {
  308. if (x.status === 200) {
  309. try {
  310. const $e = $$(x.responseText);
  311. const src = $e.find(guessSelectorLib.join(','))[0].src;
  312. let realSrc = src;
  313. for (let pairs of t2oLib) {
  314. realSrc = realSrc.replace(pairs[0], pairs[1]);
  315. if (realSrc !== src) {
  316. break;
  317. }
  318. }
  319. resolve(realSrc);
  320. }
  321. catch (e) {
  322. console.warn(e);
  323. resolve(null);
  324. }
  325. }
  326. else {
  327. console.warn(x);
  328. resolve(null);
  329. }
  330. },
  331. ontimeout: (e) => {
  332. console.warn(e);
  333. resolve(null);
  334. }
  335. });
  336. });
  337. }
  338. // RGB channel remap function (lowlevel)
  339. function rgbRemap(raw, filter, width, height, rgb) {
  340. const [R, G, B] = rgb;
  341. for (let row = 0; row < height; ++row) {
  342. for (let col = 0; col < width; ++col) {
  343. let ind = col * 4 + row * width * 4;
  344. filter[ind] = R[raw[ind]];
  345. filter[ind + 1] = G[raw[ind + 1]];
  346. filter[ind + 2] = B[raw[ind + 2]];
  347. filter[ind + 3] = raw[ind + 3];
  348. }
  349. }
  350. }
  351. // Get ImageData from src with an optional update hook
  352. // Cross origin is supported
  353. async function GM_getImageData(src, fn) {
  354. return new Promise((resolve) => {
  355. GM_xmlhttpRequest({
  356. url: src,
  357. method: 'GET',
  358. // Blob or Arraybuffer responseType will slow down the page noticeably,
  359. // so we text type with x-user-defined charset to get raw binaries
  360. overrideMimeType: 'text/plain; charset=x-user-defined',
  361. // Progress update hook
  362. onprogress: (e) => {
  363. if (typeof (fn) == 'function') {
  364. if (e.total !== -1) {
  365. fn(e.loaded / e.total);
  366. }
  367. else {
  368. fn(-e.loaded);
  369. }
  370. }
  371. },
  372. onload: (e) => {
  373. if (e.status === 200) {
  374. // Get binary from text
  375. const imageResponseText = e.responseText;
  376. const l = imageResponseText.length;
  377. const bytes = new Uint8Array(l);
  378. for (let i = 0; i < l; i++) {
  379. bytes[i] = imageResponseText.charCodeAt(i) & 0xff;
  380. }
  381. // Decode png binary and resolve the image data arraybuffer,
  382. // createImageBitmap is a multi-thread operation,
  383. // and won't complain about CSP img-src errors when using Image object
  384. const type = (e.responseHeaders.match(/content\-type: *(.+)$/m) || ['', 'image/png'])[1];
  385. let ext;
  386. switch (type) {
  387. case 'image/apng':
  388. ext = '.apng';
  389. break;
  390. case 'image/bmp':
  391. ext = '.bmp';
  392. break;
  393. case 'image/gif':
  394. ext = '.gif';
  395. break;
  396. case 'image/x-icon':
  397. ext = '.ico';
  398. break;
  399. case 'image/jpeg':
  400. ext = '.jpg';
  401. break;
  402. case 'image/png':
  403. ext = '.png';
  404. break;
  405. case 'image/svg+xml':
  406. ext = '.svg';
  407. break;
  408. case 'image/tiff':
  409. ext = '.tiff';
  410. break;
  411. case 'image/webp':
  412. ext = '.webp';
  413. break;
  414. default:
  415. if (type.slice(0, 5) === 'image') {
  416. let temp = type.match(/\/(.*)/);
  417. if (temp) {
  418. ext = '.' + temp;
  419. } else {
  420. ext = '';
  421. }
  422. } else {
  423. ext = (src.match(/\.[^\.]+$/) || [''])[0];
  424. }
  425. break;
  426. }
  427. createImageBitmap(new Blob([bytes], { type: type }))
  428. .then((e) => {
  429. const [width, height] = [e.width, e.height];
  430. const canvas = document.createElement('canvas');
  431. canvas.width = width;
  432. canvas.height = height;
  433. const context = canvas.getContext('2d');
  434. context.drawImage(e, 0, 0);
  435. e.close();
  436. resolve({
  437. imageData: new ImageData(
  438. context.getImageData(0, 0, width, height).data,
  439. width,
  440. height
  441. ),
  442. extension: ext
  443. });
  444. });
  445. }
  446. else {
  447. console.warn(e);
  448. resolve(null);
  449. }
  450. },
  451. onerror: (e) => {
  452. console.warn(e);
  453. resolve(null);
  454. }
  455. });
  456. });
  457. }
  458.  
  459. /*--- Diff and Filter Core Function ---*/
  460. // Diff images
  461. async function diffImage(img1, img2, init = { alpha: 0.5, threshold: 0.007 }, worker = diffWorker) {
  462. if (
  463. img1 && img2 &&
  464. img1.width === img2.width &&
  465. img1.height === img2.height
  466. ) {
  467. if (worker) {// async diff
  468. const [
  469. raw1,
  470. raw2,
  471. width,
  472. height
  473. ] = [
  474. img1.data.buffer,
  475. img2.data.buffer,
  476. img1.width,
  477. img1.height
  478. ];
  479. const key = '' + Date.now();
  480. worker.onmessage = (e) => {
  481. const returnKey = e.data.key;
  482. const resolve = worker.keyPool[returnKey];
  483. if (resolve) {
  484. resolve(
  485. new ImageData(
  486. new Uint8ClampedArray(e.data.diff),
  487. e.data.width,
  488. e.data.height
  489. )
  490. );
  491. }
  492. };
  493. worker.postMessage({
  494. img1: raw1,
  495. img2: raw2,
  496. width: width,
  497. height: height,
  498. init: init,
  499. key: key
  500. }, [raw1, raw2]);
  501. return new Promise((res) => {
  502. worker.keyPool[key] = res;
  503. });
  504. } else {// sync diff
  505. const [data1, data2, width, height] = [
  506. img1.data,
  507. img2.data,
  508. img1.width,
  509. img1.height
  510. ];
  511. const res = new Uint8ClampedArray(data1);
  512. pixelmatch(data1, data2, res, width, height, init);
  513. return (
  514. new ImageData(
  515. res,
  516. width,
  517. height
  518. )
  519. );
  520. }
  521. } else {
  522. return null;
  523. }
  524. }
  525. // RGB channel remap filter image
  526. async function rgbImage(img, argument) {
  527. if (img) {
  528. if (argument instanceof Worker) {
  529. const worker = argument;
  530. const [raw, width, height] = [img.data.buffer, img.width, img.height];
  531. const key = '' + Date.now();
  532. worker.onmessage = (e) => {
  533. const returnKey = e.data.key;
  534. const resolve = worker.keyPool[returnKey];
  535. if (resolve) {
  536. resolve(
  537. new ImageData(
  538. new Uint8ClampedArray(e.data.filter),
  539. e.data.width,
  540. e.data.height
  541. )
  542. );
  543. }
  544. };
  545. await loadBufferPromise;
  546. worker.postMessage({
  547. img: raw,
  548. width: width,
  549. height: height,
  550. key: key
  551. }, [raw]);
  552. return new Promise((res) => {
  553. worker.keyPool[key] = res;
  554. });
  555. } else {
  556. const rgb = argument;
  557. const [data, width, height] = [img.data, img.width, img.height];
  558. const res = new Uint8ClampedArray(data);
  559. rgbRemap(data, res, width, height, rgb);
  560. return (
  561. new ImageData(
  562. res,
  563. width,
  564. height
  565. )
  566. );
  567. }
  568. } else {
  569. return null;
  570. }
  571. }
  572.  
  573. function reRenderImage(image, scale) {
  574. if (scale > 10) {
  575. image.style['image-rendering'] = 'pixelated';
  576. } else {
  577. image.style['image-rendering'] = 'auto';
  578. }
  579. }
  580.  
  581. /*--- Get Images: Original, Diffed or Filtered ---*/
  582. // Get original image function
  583. function getOriginalImage(target, $overlay) {
  584. if (target.easyCompare && target.easyCompare.originalImage) {
  585. const originalImage = target.easyCompare.originalImage;
  586. if (originalImage.ready) {
  587. originalImage.style.width = `${scale * 10}%`;
  588. reRenderImage(originalImage, scale);
  589. }
  590. return originalImage;
  591. } else {
  592. const originalCanvas = makeCanvas();
  593. const updateProgress = (p) => {
  594. if (p !== null && p >= 0) {
  595. drawText(originalCanvas, `Loading ${(p * 100).toFixed(1)}%`);
  596. } else if (p < 0) {
  597. drawText(originalCanvas, `Loading...`);
  598. }
  599. };
  600. const resolveOriginal = (src, onprogress, resolve) => {
  601. GM_getImageData(src, onprogress).then(({ imageData: originalImageData, extension }) => {
  602. resolve(originalImageData);
  603. originalCanvas.src = src;
  604. originalCanvas.ext = extension;
  605. drawImage(originalCanvas, originalImageData);
  606. originalCanvas.style.width = `${scale * 10}%`;
  607. reRenderImage(originalCanvas, scale);
  608. originalCanvas.ready = true;
  609. });
  610. };
  611. drawText(originalCanvas, `Loading...`);
  612. originalCanvas.ready = false;
  613. originalCanvas.targetImage = target;
  614. $overlay.append(originalCanvas.parentElement);
  615. if (!target.easyCompare) {
  616. target.easyCompare = {};
  617. }
  618. target.easyCompare.originalImage = originalCanvas;
  619. target.easyCompare.originalImagePromise = onprogress => new Promise(async (resolve) => {
  620. let realSrc = target.src;
  621. // Parse original src from thumb src
  622. for (let pairs of t2oLib) {
  623. realSrc = realSrc.replace(pairs[0], pairs[1]);
  624. if (realSrc !== target.src) {
  625. resolveOriginal(realSrc, onprogress, resolve);
  626. return;
  627. }
  628. }
  629. // Guess original src from hyper link
  630. let href, hrefOriginal;
  631. if ((hrefOriginal = target.parentElement.href, href = hrefOriginal)) {
  632. for (let pairs of skipRedirLib) {
  633. href = href.replace(pairs[0], pairs[1]);
  634. if (href !== hrefOriginal) {
  635. break;
  636. }
  637. }
  638. if (href.match(/\.png$|\.jpe?g$|\.webp|\.gif|\.bmp|\.svg$/)) {
  639. resolveOriginal(href, onprogress, resolve);
  640. return;
  641. } else {
  642. guessOriginalImage(href).then(src => {
  643. resolveOriginal(src || realSrc, onprogress, resolve);
  644. return;
  645. });
  646. }
  647. } else {
  648. resolveOriginal(realSrc, onprogress, resolve);
  649. return;
  650. }
  651. });
  652. target.easyCompare.originalImagePromise(updateProgress);
  653. return originalCanvas;
  654. }
  655. }
  656. // Get diffed image function
  657. function getDiffedImage(target, base, $overlay) {
  658. if (target.src === base.src) {
  659. return getOriginalImage(target);
  660. }
  661. if (target.easyCompare && target.easyCompare[base.src]) {
  662. target.easyCompare[base.src].targetImage = target;
  663. target.easyCompare[base.src].baseImage = base;
  664. const diffedCanvas = target.easyCompare[base.src];
  665. if (diffedCanvas.ready) {
  666. diffedCanvas.style.width = `${scale * 10}%`;
  667. reRenderImage(diffedCanvas, scale);
  668. }
  669. return diffedCanvas;
  670. } else {
  671. const diffedCanvas = makeCanvas();
  672. drawText(diffedCanvas, 'Loading...');
  673. diffedCanvas.ready = false;
  674. diffedCanvas.targetImage = target;
  675. diffedCanvas.baseImage = base;
  676. diffedCanvas.threshold = -1;
  677. diffedCanvas.step = 0.001;
  678. $overlay.append(diffedCanvas.parentElement);
  679. if (!target.easyCompare) {
  680. target.easyCompare = {};
  681. }
  682. target.easyCompare[base.src] = diffedCanvas;
  683. if (!base.easyCompare) {
  684. base.easyCompare = {};
  685. }
  686. base.easyCompare[target.src] = diffedCanvas;
  687.  
  688. let progress = [0, 0];
  689. // Progress update function
  690. const updateProgress = (p, ind) => {
  691. if (p !== null && p >= 0 && ind !== null) {
  692. progress[ind] = p;
  693. drawText(diffedCanvas, `Loading ${((progress[0] + progress[1]) * 50).toFixed(1)}%`);
  694. }
  695. else if (p < 0) {
  696. drawText(diffedCanvas, 'Loading...');
  697. }
  698. else {
  699. drawText(diffedCanvas, 'Diffing...');
  700. }
  701. };
  702. getOriginalImage(target, $overlay);
  703. getOriginalImage(base, $overlay);
  704. Promise.all([
  705. target.easyCompare.originalImagePromise((p) => updateProgress(p, 0)),
  706. base.easyCompare.originalImagePromise((p) => updateProgress(p, 1))
  707. ]).then(imageData => {
  708. updateProgress(null, null);
  709. return diffImage(...imageData, {
  710. alpha: 0.5,
  711. threshold: 0.007
  712. });
  713. }).then((diffedImageData) => {
  714. if (diffedImageData === null) {
  715. drawText(diffedCanvas, 'Sizes Not Match');
  716. } else {
  717. drawImage(diffedCanvas, diffedImageData);
  718. diffedCanvas.ext = '.png';
  719. diffedCanvas.threshold = 0.007;
  720. diffedCanvas.style.width = `${scale * 10}%`;
  721. reRenderImage(diffedCanvas, scale);
  722. diffedCanvas.ready = true;
  723. }
  724. }).catch((err) => {
  725. console.warn(err);
  726. drawText(diffedCanvas, 'Something Went Wrong');
  727. });
  728. return diffedCanvas;
  729. }
  730. }
  731. // Get filtered image function
  732. function getFilteredImage(target, ftType, $overlay) {
  733. if (target.easyCompare && target.easyCompare[ftType]) {
  734. const filteredCanvas = target.easyCompare[ftType];
  735. if (filteredCanvas.ready) {
  736. filteredCanvas.style.width = `${scale * 10}%`;
  737. reRenderImage(filteredCanvas, scale);
  738. }
  739. return filteredCanvas;
  740. } else {
  741. const filteredCanvas = makeCanvas();
  742. drawText(filteredCanvas, 'Loading...');
  743. filteredCanvas.ready = false;
  744. filteredCanvas.targetImage = target;
  745. $overlay.append(filteredCanvas.parentElement);
  746. if (!target.easyCompare) {
  747. target.easyCompare = {};
  748. }
  749. target.easyCompare[ftType] = filteredCanvas;
  750. // Progress Update Function
  751. const updateProgress = (p) => {
  752. if (p !== null && p >= 0) {
  753. drawText(filteredCanvas, `Loading ${(p * 100).toFixed(1)}%`);
  754. } else if (p < 0) {
  755. drawText(filteredCanvas, 'Loading...');
  756. } else {
  757. drawText(filteredCanvas, 'Filtering...');
  758. }
  759. };
  760. // Wait original image and filter the original image
  761. getOriginalImage(target, $overlay);
  762. target.easyCompare
  763. .originalImagePromise(updateProgress).then((imageData) => {
  764. updateProgress(null);
  765. return filterImage[ftType](imageData);
  766. }).then(filterdImageData => {
  767. drawImage(filteredCanvas, filterdImageData);
  768. filteredCanvas.ext = '.png';
  769. filteredCanvas.style.width = `${scale * 10}%`;
  770. reRenderImage(filteredCanvas, scale);
  771. filteredCanvas.ready = true;
  772. });
  773. return filteredCanvas;
  774. }
  775. }
  776.  
  777. /*--- UI Response Functions ---*/
  778. // Function to acquire active image
  779. function getActive($overlay) {
  780. return $overlay.find('canvas:visible');
  781. }
  782. // Function fired when compare button is activated
  783. function activateCompare($target) {
  784. $target.attr({
  785. 'fill': '#008000'
  786. }).css({
  787. 'cursor': 'pointer',
  788. 'opacity': '1'
  789. })[0].state = true;
  790. }
  791. // Function fired when leaving image
  792. function leaveImage($overlay, target = undefined) {
  793. const original = getActive($overlay).hide()[0];
  794. if (((original && (target = original.targetImage)) || target) &&
  795. target.easyCompare && target.easyCompare.boxShadow !== undefined) {
  796. $(target).css('box-shadow', target.easyCompare.boxShadow);
  797. }
  798. }
  799. // Function fired when compare button is clicked and toggled on
  800. // (Main UI Logic)
  801. function enterCompare($overlay, $images, $message) {
  802. if (Mousetrap) {
  803. Mousetrap.pause();
  804. }
  805. $overlay.show()[0].state = true;
  806. let colors = ['red', 'blue'];
  807. let step = 1, baseImage;
  808. let ftType = 'none';
  809. let fadingTime = 300;
  810. // Mouse enter event
  811. $images.on('mouseenter.compare', (e, triggeredShiftKey) => {
  812. const target = e.currentTarget;
  813. clearTimeout(timeout);
  814. leaveImage($overlay);
  815. if (!target.easyCompare) {
  816. target.easyCompare = {};
  817. target.easyCompare.boxShadow = target.style['box-shadow'];
  818. }
  819. $(target).css({
  820. 'box-shadow': '0px 0px 8px ' + colors[0]
  821. });
  822. let displayedImage;
  823. if ((e.shiftKey || triggeredShiftKey) && baseImage) {
  824. displayedImage = $(getDiffedImage(target, baseImage, $overlay))
  825. .css('outline-color', colors[0])
  826. .show();
  827. } else {
  828. switch (ftType) {
  829. case 'none':
  830. displayedImage = $(getOriginalImage(target, $overlay))
  831. .css('outline-color', colors[0])
  832. .show();
  833. break;
  834. default:
  835. displayedImage = $(getFilteredImage(target, ftType, $overlay))
  836. .css('outline-color', colors[0])
  837. .show();
  838. break;
  839. }
  840. }
  841. colors.push(colors.shift());
  842. //Mouse leave event
  843. }).on('mouseleave.compare', (e) => {
  844. const target = e.currentTarget;
  845. timeout = setTimeout(() => {
  846. leaveImage($overlay, target);
  847. }, 200);
  848. });
  849.  
  850. // KeyBoard functions
  851. function setBaseImage() {
  852. try {
  853. baseImage = getActive($overlay)[0].targetImage;
  854. } catch (err) {
  855. baseImage = undefined;
  856. if (!(err instanceof TypeError)) {
  857. console.warn(err);
  858. }
  859. }
  860. }
  861. function downloadImage(name = 'easycompare') {
  862. try {
  863. const target = getActive($overlay)[0];
  864. const url = target.src || target.toDataURL('image/png').replace(/^data:image\/[^;]/, 'data:application/octet-stream');
  865. const ext = target.ext || '';
  866. GM_download({
  867. url: url,
  868. name: name + ext
  869. });
  870. } catch (err) {
  871. if (!(err instanceof TypeError)) {
  872. console.warn(err);
  873. }
  874. }
  875. }
  876. function toggleFilter(filter) {
  877. ftType = (ftType === filter ? 'none' : filter);
  878. try {
  879. const target = getActive($overlay).hide()[0];
  880. let $displayImage;
  881. if (ftType === 'none') {
  882. $displayImage = $(getOriginalImage(target.targetImage, $overlay));
  883. } else {
  884. $displayImage = $(getFilteredImage(target.targetImage, ftType, $overlay));
  885. }
  886. $displayImage
  887. .css('outline-color', target.style['outline-color'])
  888. .show();
  889. } catch (err) {
  890. if (!(err instanceof TypeError)) {
  891. console.warn(err);
  892. }
  893. }
  894. }
  895. function adjustView(up) {
  896. try {
  897. if (up && scale < 10) {
  898. scale = scale + 1;
  899. } else if (up && scale < 30) {
  900. scale = scale + 2;
  901. } else if (!up && scale > 10) {
  902. scale = scale - 2;
  903. } else if (!up && scale > 1) {
  904. scale = scale - 1;
  905. }
  906. const target = getActive($overlay)[0];
  907. if (target.ready) {
  908. target.style.width = `${scale * 10}%`;
  909. reRenderImage(target, scale);
  910. }
  911. $message.text(`Zoom: ${parseInt(scale * 10)}%`).css('opacity', '1');
  912. setTimeout(() => {
  913. $message.css('opacity', '0');
  914. }, fadingTime);
  915. } catch (err) {
  916. if (!(err instanceof TypeError)) {
  917. console.warn(err);
  918. }
  919. }
  920. }
  921. function setView(scl) {
  922. try {
  923. if (scale !== scl) {
  924. scale = scl;
  925. const target = getActive($overlay)[0];
  926. if (target.ready) {
  927. target.style.width = `${scale * 10}%`;
  928. reRenderImage(target, scale);
  929. }
  930. $message.text(`Zoom: ${parseInt(scale * 10)}%`).css('opacity', '1');
  931. setTimeout(() => {
  932. $message.css('opacity', '0');
  933. }, fadingTime);
  934. }
  935. } catch (err) {
  936. if (!(err instanceof TypeError)) {
  937. console.warn(err);
  938. }
  939. }
  940. }
  941. function adjustThreshold(up) {
  942. try {
  943. const target = getActive($overlay)[0];
  944. let threshold = target.threshold;
  945. if (threshold !== undefined && threshold >= 0) {
  946. const thresholdPrev = threshold;
  947. $message.text(`Threshold: ${thresholdPrev.toFixed(4)}`).css('opacity', '1');
  948. if (up) {
  949. threshold += target.step;
  950. if (threshold > 1) {
  951. threshold = 1;
  952. }
  953. } else {
  954. threshold -= target.step;
  955. if (threshold < 0) {
  956. threshold = 0;
  957. }
  958. }
  959. target.threshold = -1;
  960. const [
  961. baseCanvas,
  962. targetCanvas
  963. ] = [
  964. target.baseImage.easyCompare.originalImage,
  965. target.targetImage.easyCompare.originalImage
  966. ];
  967. diffImage(
  968. baseCanvas.getContext('2d').getImageData(0, 0, baseCanvas.width, baseCanvas.height),
  969. targetCanvas.getContext('2d').getImageData(0, 0, targetCanvas.width, targetCanvas.height),
  970. {
  971. alpha: 0.5,
  972. threshold: threshold
  973. }
  974. ).then((imageData) => {
  975. target.getContext('2d').putImageData(imageData, 0, 0);
  976. $message.text(`Threshold: ${threshold.toFixed(4)}`).css('opacity', '1');
  977. setTimeout(() => {
  978. target.threshold = threshold;
  979. $message.css('opacity', '0');
  980. }, fadingTime);
  981. });
  982. }
  983. } catch (err) {
  984. if (!(err instanceof TypeError)) {
  985. console.warn(err);
  986. }
  987. }
  988. }
  989. function adjustStep(left) {
  990. try {
  991. const target = getActive($overlay)[0];
  992. let step = target.step;
  993. if (step) {
  994. if (left && step <= 0.1) {
  995. target.step = step * 10;
  996. } else if (left) {
  997. target.step = 1.0;
  998. } else if (!left && step >= 0.001) {
  999. target.step = step / 10;
  1000. } else {
  1001. target.step = 0.0001;
  1002. }
  1003. $message.text(`Step: ${target.step.toFixed(4)}`).css('opacity', '1');
  1004. setTimeout(() => $message.css('opacity', '0'), fadingTime);
  1005. }
  1006. } catch (err) {
  1007. if (!(err instanceof TypeError)) {
  1008. console.warn(err);
  1009. }
  1010. }
  1011. }
  1012. function clearCache() {
  1013. try {
  1014. leaveImage($overlay, getActive($overlay)[0].targetImage);
  1015. } catch (err) {
  1016. if (!(err instanceof TypeError)) {
  1017. console.warn(err);
  1018. }
  1019. }
  1020. $overlay.find('canvas').toArray().forEach(e => {
  1021. const target = e.targetImage;
  1022. delete target.easyCompare;
  1023. e.parentElement.remove();
  1024. });
  1025. }
  1026. function switchImage(left, shiftKey) {
  1027. try {
  1028. const targetImage = getActive($overlay)[0].targetImage;
  1029. const index = $images.index(targetImage);
  1030. leaveImage($overlay, targetImage);
  1031. const nextElem = $images[left ? index - step : index + step] || $images[index];
  1032. $(nextElem).trigger('mouseenter', [shiftKey]);
  1033. } catch (err) {
  1034. if (!(err instanceof TypeError)) {
  1035. console.warn(err);
  1036. }
  1037. }
  1038. }
  1039.  
  1040. // Scroll and Keyboard event
  1041. $(document).on('scroll.compare', (e) => {
  1042. const temp = getActive($overlay)[0];
  1043. if (temp) {
  1044. const $prev = $(temp.targetImage);
  1045. if (!$prev.is(':hover')) {
  1046. leaveImage($overlay, $prev[0]);
  1047. $images.find('img:hover').trigger('mousenter');
  1048. }
  1049. }// Hot-Keys
  1050. }).on('keydown.compare', (e) => {
  1051. e.preventDefault();
  1052. e.stopImmediatePropagation();
  1053. switch (e.key) {
  1054. case 'Escape':
  1055. exitCompare($overlay, $images);
  1056. break;
  1057. case 'Shift':
  1058. setBaseImage();
  1059. break;
  1060. case '+': case '=':
  1061. if (e.ctrlKey) {
  1062. adjustView(true);
  1063. }
  1064. break;
  1065. case '-': case '_':
  1066. if (e.ctrlKey) {
  1067. adjustView(false);
  1068. }
  1069. break;
  1070. case 'O': case 'o':
  1071. if (e.ctrlKey) {
  1072. setView(10);
  1073. }
  1074. break;
  1075. case 'P': case 'p':
  1076. if (e.ctrlKey) {
  1077. setView(30);
  1078. }
  1079. break;
  1080. case 'S': case 's':
  1081. if (e.ctrlKey) {
  1082. downloadImage();
  1083. } else {
  1084. toggleFilter('solar');
  1085. }
  1086. break;
  1087. case 'A': case 'a':
  1088. toggleFilter('s2lar');
  1089. break;
  1090. case 'I': case 'i':
  1091. if (e.ctrlKey) {
  1092. setView(1);
  1093. } else {
  1094. adjustThreshold(true);
  1095. }
  1096. break;
  1097. case 'ArrowUp':
  1098. adjustThreshold(true);
  1099. break;
  1100. case 'K': case 'k': case 'ArrowDown':
  1101. adjustThreshold(false);
  1102. break;
  1103. case 'J': case 'j': case 'ArrowLeft':
  1104. adjustStep(true);
  1105. break;
  1106. case 'L': case 'l': case 'ArrowRight':
  1107. if (e.ctrlKey) {
  1108. clearCache();
  1109. } else {
  1110. adjustStep(false);
  1111. }
  1112. break;
  1113. case 'Q':
  1114. case 'q':
  1115. $overlay.css('opacity', 0.5);
  1116. break;
  1117. case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
  1118. step = parseInt(e.key);
  1119. break;
  1120. case '0':
  1121. step = 10;
  1122. break;
  1123. case 'W': case 'w':
  1124. switchImage(true, e.shiftKey);
  1125. break;
  1126. case 'E': case 'e':
  1127. switchImage(false, e.shiftKey);
  1128. break;
  1129. }
  1130. return false;
  1131. }).on('keyup.compare', (e) => {
  1132. e.preventDefault();
  1133. e.stopImmediatePropagation();
  1134. switch (e.key) {
  1135. case 'Q':
  1136. case 'q':
  1137. $overlay.css('opacity', '');
  1138. break;
  1139. }
  1140. return false;
  1141. });
  1142. }
  1143. // Function fired when compare button is clicked and toggled off
  1144. // or quit via keyboard 'esc'
  1145. function exitCompare($overlay, $images) {
  1146. if (Mousetrap) {
  1147. Mousetrap.unpause();
  1148. }
  1149. leaveImage($overlay);
  1150. $overlay.hide()[0].state = false;
  1151. $images
  1152. .off('mouseenter.compare')
  1153. .off('mouseleave.compare');
  1154. $(document)
  1155. .off('scroll.compare')
  1156. .off('keydown.compare');
  1157. }
  1158.  
  1159. /*--- Building Blocks ---*/
  1160. // A message on the whole page
  1161. const $message = $('<div>').css({
  1162. 'top': '50%',
  1163. 'left': '50%',
  1164. 'z-index': 2147483647,
  1165. 'position': 'fixed',
  1166. 'transform': 'translate(-50%, -50%)',
  1167. 'opacity': '0',
  1168. 'vertical-align': 'middle',
  1169. 'pointer-events': 'none',
  1170. 'transition': 'all 0.1s',
  1171. 'font-size': '500%',
  1172. 'color': 'yellow',
  1173. 'font-weight': 'bold'
  1174. });
  1175. // An overlay on the whole page
  1176. const $overlay = $('<div/>').css({
  1177. 'id': 'easy-compare-overlay',
  1178. 'position': 'fixed',
  1179. 'top': 0,
  1180. 'right': 0,
  1181. 'bottom': 0,
  1182. 'left': 0,
  1183. 'z-index': 2147483646,
  1184. 'background-color': 'rgba(0, 0, 0, 0.75)',
  1185. 'pointer-events': 'none',
  1186. 'display': 'none'
  1187. }).append($message);
  1188. // The compare button
  1189. const $compareButton = $(`<svg xmlns="http://www.w3.org/2000/svg">
  1190. <path id="ld" d="M20 6H10c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h10v4h4V2h-4v4zm0 30H10l10-12v12zM38 6H28v4h10v26L28 24v18h10c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4z"/>
  1191. </svg>`).attr({
  1192. 'width': '30',
  1193. 'height': '30',
  1194. 'viewBox': '0 0 48 48',
  1195. 'stroke': 'white',
  1196. 'stroke-width': '5px',
  1197. 'fill': 'gray'
  1198. }).css({
  1199. 'position': 'fixed',
  1200. 'top': '0px',
  1201. 'right': '0px',
  1202. 'padding': '15px',
  1203. 'box-sizing': 'content-box',
  1204. 'z-index': 2147483647,
  1205. 'paint-order': 'stroke',
  1206. 'opacity': 0,
  1207. 'transition': 'all 0.2s',
  1208. 'cursor': 'auto'
  1209. }).on('mouseenter', (e) => {
  1210. const $target = $(e.currentTarget);
  1211. if ($target[0].manualFlag) {
  1212. $target.attr({
  1213. 'fill': 'gray'
  1214. }).css({
  1215. 'opacity': 0.2,
  1216. 'pointer-events': 'none'
  1217. });
  1218. $target[0].manualFlag = false;
  1219. const clientWidth = document.documentElement.clientWidth;
  1220. $(document).on('mousemove.compare', ({ clientX, clientY }) => {
  1221. if (clientX < clientWidth - 61 || clientY > 61) {
  1222. $target[0].insideFlag = 0;
  1223. clearTimeout(timeout);
  1224. $target.attr({
  1225. 'fill': 'gray'
  1226. }).css({
  1227. 'cursor': 'auto',
  1228. 'opacity': 0,
  1229. 'pointer-events': 'auto'
  1230. })[0].state = false;
  1231. $(document).off('mousemove.compare');
  1232. $target[0].manualFlag = true;
  1233. } else if (clientX >= clientWidth - 45 && clientX <= clientWidth - 15 && clientY >= 15 && clientY <= 45) {
  1234. if (!$target[0].insideFlag) {
  1235. $target[0].insideFlag = 1;
  1236. timeout = setTimeout(() => {
  1237. activateCompare($target);
  1238. $target.css({
  1239. 'pointer-events': 'auto'
  1240. });
  1241. }, $overlay[0].state ? 0 : 1000);
  1242. }
  1243. } else if (clientX < clientWidth - 45 || clientX > clientWidth - 15 || clientY < 15 || clientY > 45) {
  1244. $target[0].insideFlag = 0;
  1245. clearTimeout(timeout);
  1246. $target.attr({
  1247. 'fill': 'gray'
  1248. }).css({
  1249. 'cursor': 'auto',
  1250. 'opacity': 0.2,
  1251. 'pointer-events': 'none'
  1252. })[0].state = false;
  1253. }
  1254. });
  1255. }
  1256. }).click((e) => {
  1257. if (e.currentTarget.state) {
  1258. switch ($overlay[0].state) {
  1259. case false:
  1260. enterCompare($overlay, $(':not("#easy-compare-overlay") img:visible'), $message);
  1261. break;
  1262. case true:
  1263. exitCompare($overlay, $(':not("#easy-compare-overlay") img:visible'));
  1264. break;
  1265. }
  1266. }
  1267. else {
  1268. let x = e.clientX;
  1269. let y = e.clientY;
  1270. const lowerElement = document
  1271. .elementsFromPoint(x, y)
  1272. .find(e => !['svg', 'path'].includes(e.tagName));
  1273. lowerElement.click();
  1274. }
  1275. }).mousedown((e) => {
  1276. if (e.currentTarget.state) {
  1277. $(e.currentTarget).attr({
  1278. 'fill': '#006000'
  1279. });
  1280. }
  1281. }).mouseup((e) => {
  1282. if (e.currentTarget.state) {
  1283. $(e.currentTarget).attr({
  1284. 'fill': '#008000'
  1285. });
  1286. }
  1287. });
  1288. $compareButton[0].manualFlag = true;
  1289. $compareButton[0].insideFlag = false;
  1290.  
  1291. /*--- Insert to Document ---*/
  1292. $overlay[0].state = false;
  1293. $compareButton[0].state = false;
  1294. $('body').append($compareButton).append($overlay);
  1295.  
  1296. })(window.$.noConflict(true),
  1297. unsafeWindow.Mousetrap,
  1298. window.pixelmatch,
  1299. unsafeWindow.URL.createObjectURL ?
  1300. unsafeWindow.URL :
  1301. unsafeWindow.webkitURL);