Easy Compare

Compare images

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name               Easy Compare
// @description        Compare images
// @version            0.9.5
// @author             Secant (TYT@NexusHD)
// @license            GPL-3.0-or-later
// @supportURL         [email protected]
// @contributionURL    https://i.loli.net/2020/02/28/JPGgHc3UMwXedhv.jpg
// @contributionAmount 10
// @include            *
// @require            https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js
// @require            https://greasyfork.org/scripts/401377-pixelmatch/code/pixelmatch.js
// @resource           PixelMatchCore https://greasyfork.org/scripts/401377-pixelmatch/code/pixelmatch.js
// @namespace          https://greasyfork.org/users/152136
// @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
// @grant              GM_xmlhttpRequest
// @grant              GM_download
// @grant              GM_getValue
// @grant              GM_setValue
// @grant              GM_getResourceText
// @grant              unsafewindow
// @connect            hdbits.org
// @connect            awesome-hd.me
// @connect            ptpimg.me
// @connect            imgbox.com
// @connect            malzo.com
// @connect            imagebam.com
// @connect            pixhost.to
// @connect            loli.net
// @connect            funkyimg.com
// @connect            ilikeshots.club
// @connect            z4a.net
// @connect            picgd.com
// @connect            tu.totheglory.im
// @connect            tpimg.ccache.org
// @connect            pterclub.com
// @connect            catbox.moe
// @connect            sm.ms
// @connect            broadcasthe.net
// @connect            *
// ==/UserScript==
// jshint esversion:8, -W054
(async function ($, Mousetrap, pixelmatch, URL) {
  'use strict';

  /*--- Preparation ---*/
  // Mousetrap Pause Plugin
  if (Mousetrap) {
    let target = Mousetrap.prototype || Mousetrap;
    const _originalStopCallback = target.stopCallback;
    target.stopCallback = function (e, element, combo) {
      var self = this;
      if (self.paused) {
        return true;
      }
      return _originalStopCallback.call(self, e, element, combo);
    };
    target.pause = function () {
      var self = this;
      self.paused = true;
    };
    target.unpause = function () {
      var self = this;
      self.paused = false;
    };
    try {
      Mousetrap.init();
    } catch (_) { }
  }

  /*--- Global Contexts ---*/
  // A global timeout ID holder
  let timeout;
  // A global scale factor
  let scale = 10;
  // Regex replacement array that converts thumbs to originals
  const t2oLib = [
    [/\.thumb\.jpe?g$/, ''], // nexusphp
    [/\.md\.png$/, '.png'], // m-team
    [/\.th\.png$/, '.png'], // pterclub
    [/_thumb\.png$/, '.png'], // totheglory
    [/img\.awesome\-hd\.me\/t(\/\d+)?\//, 'img.awesome-hd.me/images/'], // awesome-hd
    [/thumbs((?:\d+)?\.imgbox\.com\/.+_)t\.png$/, 'images$1o.png'], // imgbox
    [/t((?:\d+)?\.pixhost\.to\/)thumbs\//, 'img$1images/'], // pixhost
    [/t(\.hdbits\.org\/.+)\.jpg$/, 'i$1.png'], // hdbits
    [/^.*?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']
  ];
  // Skip redirections
  const skipRedirLib = [
    [/^https?:\/\/anonym\.to\/\?(.*)$/, (_, p1) => decodeURIComponent(p1)],
    [/^https?:\/\/www\.dereferer\.org\/\?(.*)$/, (_, p1) => decodeURIComponent(p1)],
    [/^(?:https?:\/\/pterclub\.com)?\/link\.php\?sign=.+?&target=(.*)$/, (_, p1) => decodeURIComponent(p1.replace(/\+/g, ' ')).replace(/ /g, '%20')],
    [/^.*?imagecache\.php\?url=(.*)$/, (_, p1) => decodeURIComponent(p1.replace(/\+/g, ' ')).replace(/ /g, '%20')]
  ];
  // Probable original image selectors on a view page
  const guessSelectorLib = [
    '#image-viewer-container>img',
    '.image-container img',
    'div.img.big>img',
    'img.mainimage',
    'img.main-image',
    'img#img'
  ];
  // Filter function mapping
  const filterImage = {
    'solar': img => rgbImage(img, solarWorker || rgbSolarCurve),
    's2lar': img => rgbImage(img, s2larWorker || rgbS2larCurve)
  };

  /*--- Workers Initialization ---*/
  // Solar Curve
  function solarCurve(x, t = 5, k = 5.5) {
    const m = (k * Math.PI - 128 / t);
    const A = -1 / 4194304 * m;
    const B = 3 / 32768 * m;
    const C = 1 / t;
    return Math.round(
      127.9999 * Math.sin(
        A * x ** 3 + B * x ** 2 + C * x - Math.PI / 2
      ) + 127.5
    ) || 0;
  }
  let rgbSolarCurve = GM_getValue('solarCurve');
  let rgbS2larCurve = GM_getValue('s2larCurve');
  if (!rgbSolarCurve) {
    rgbSolarCurve = [
      Array.from({ length: 256 }, (_, x) => solarCurve(x)),
      Array.from({ length: 256 }, (_, x) => solarCurve(x - 5)),
      Array.from({ length: 256 }, (_, x) => solarCurve(x + 5))
    ];
    GM_setValue('solarCurve', JSON.stringify(rgbSolarCurve));
    rgbS2larCurve = [
      Array.from({ length: 256 }, (_, x) => rgbSolarCurve[0][[rgbSolarCurve[0][x]]]),
      Array.from({ length: 256 }, (_, x) => rgbSolarCurve[1][[rgbSolarCurve[1][x]]]),
      Array.from({ length: 256 }, (_, x) => rgbSolarCurve[2][[rgbSolarCurve[2][x]]])
    ];
    GM_setValue('s2larCurve', JSON.stringify(rgbS2larCurve));
  } else {
    rgbSolarCurve = JSON.parse(rgbSolarCurve);
    rgbS2larCurve = JSON.parse(rgbS2larCurve);
  }
  rgbSolarCurve = rgbSolarCurve.map(e => new Uint8Array(e));
  rgbS2larCurve = rgbS2larCurve.map(e => new Uint8Array(e));
  async function loadBuffer(worker, [R, G, B]) {
    return new Promise((resolve) => {
      worker.onmessage = (e) => {
        resolve(e.data.result);
      };
      worker.postMessage({
        R: R.buffer,
        G: G.buffer,
        B: B.buffer
      }, [R.buffer, G.buffer, B.buffer]);
    });
  }
  // Diff, Solar, S2lar Worker Initialization
  function diffWork(f) {
    f.apply(self);
    const u = Uint8ClampedArray;
    self.onmessage = ({ data: { key, img1, img2, width, height, init } }) => {
      img1 = new u(img1);
      img2 = new u(img2);
      const diff = new u(img1);
      try {
        self.pixelmatch(img1, img2, diff, width, height, init);
        self.postMessage({
          diff: diff.buffer,
          width: width,
          height: height,
          key: key
        }, [diff.buffer]);
      } catch (err) {
        console.warn(err);
        self.postMessage({
          diff: null,
          key: key
        });
      }
    };
  }
  function rgbWork(f) {
    const u = Uint8ClampedArray;
    self.onmessage = ({ data: { key, R, G, B, img, width, height } }) => {
      if (R && G && B) {
        self.RGB = [new u(R), new u(G), new u(B)];
        self.postMessage({ result: true });
      } else {
        img = new u(img);
        const filter = new u(img);
        try {
          f.apply(self, [img, filter, width, height, self.RGB]);
          self.postMessage({
            filter: filter.buffer,
            width: width,
            height: height,
            key: key
          }, [filter.buffer]);
        } catch (err) {
          console.warn(err);
          self.postMessage({
            filter: null,
            key: key
          });
        }
      }
    };
  }
  function stringifyWork(workFun, arg) {
    return `(${workFun.toString()})(${arg})`;
  }
  let diffWorker, solarWorker, s2larWorker;
  let loadBufferPromise;
  try {
    const diffWorkerBlob = new Blob([
      stringifyWork(diffWork, new Function(
        GM_getResourceText('PixelMatchCore')
      ))
    ], { type: 'application/javascript' });
    diffWorker = new Worker(URL.createObjectURL(diffWorkerBlob));
    diffWorker.keyPool = {};
    URL.revokeObjectURL(diffWorkerBlob);
    const rgbWorkerBlob = new Blob([stringifyWork(rgbWork, rgbRemap)], { type: 'application/javascript' });
    const rgbWorkerURL = URL.createObjectURL(rgbWorkerBlob);
    solarWorker = new Worker(rgbWorkerURL);
    solarWorker.keyPool = {};
    const transSo = loadBuffer(solarWorker, rgbSolarCurve);
    s2larWorker = new Worker(rgbWorkerURL);
    s2larWorker.keyPool = {};
    const transS2 = loadBuffer(s2larWorker, rgbS2larCurve);
    URL.revokeObjectURL(rgbWorkerURL);
    loadBufferPromise = Promise.all([transSo, transS2]);
  } catch (e) {
    try {
      const diffWorkerDataURI = `data:application/javascript,${
        encodeURIComponent(
          stringifyWork(diffWork, new Function(
            GM_getResourceText('PixelMatchCore')
          ))
        )}`;
      diffWorker = new Worker(diffWorkerDataURI);
      diffWorker.keyPool = {};
      const rgbWorkerDataURI = `data:application/javascript,${
        encodeURIComponent(
          stringifyWork(rgbWork, rgbRemap)
        )}`;
      solarWorker = new Worker(rgbWorkerDataURI);
      solarWorker.keyPool = {};
      const transSo = loadBuffer(solarWorker, rgbSolarCurve);
      s2larWorker = new Worker(rgbWorkerDataURI);
      s2larWorker.keyPool = {};
      const transS2 = loadBuffer(s2larWorker, rgbS2larCurve);
      loadBufferPromise = Promise.all([transSo, transS2]);
    } catch (e) {
      diffWorker = null;
      solarWorker = null;
    }
  }

  /*--- Helper Functions ---*/
  // Virtual DOM for selection without fetching images
  function $$(htmlString) {
    return $(htmlString, document.implementation.createHTMLDocument('virtual'));
  }
  // Function to make an <canvas/> element
  function makeCanvas(outlineColor = 'red') {
    const $figure = $('<figure/>').css({
      'width': 'fit-content',
      'position': 'fixed',
      'top': '50%',
      'left': '50%',
      'margin': '0',
      'vertical-align': 'middle'
    });
    const $canvas = $(`<canvas/>`).css({
      'display': 'none',
      'transform': 'translate(-50%, -50%)',
      'opacity': '1',
      'outline': '3px solid ' + outlineColor,
      'outline-offset': '2px',
    });
    $figure.append($canvas);
    return $canvas[0];
  }
  // Draw text on canvas
  function drawText(canvas, text, font = '16px sans serif', fillStyle = 'rgba(255,255,255,255)') {
    const context = canvas.getContext('2d');
    context.font = font;
    canvas.width = context.measureText(text).width;
    canvas.height = 20;
    context.font = font;
    context.fillStyle = fillStyle;
    context.fillText(text, 0, 15);
  }
  // Draw image on canvas
  function drawImage(canvas, imageData) {
    canvas.width = imageData.width;
    canvas.height = imageData.height;
    canvas.getContext('2d').putImageData(imageData, 0, 0);
  }
  // Guess original image src from view page
  function guessOriginalImage(url) {
    return new Promise((resolve) => {
      GM_xmlhttpRequest({
        url: url,
        method: 'GET',
        timeout: 6000,
        onload: (x) => {
          if (x.status === 200) {
            try {
              const $e = $$(x.responseText);
              const src = $e.find(guessSelectorLib.join(','))[0].src;
              let realSrc = src;
              for (let pairs of t2oLib) {
                realSrc = realSrc.replace(pairs[0], pairs[1]);
                if (realSrc !== src) {
                  break;
                }
              }
              resolve(realSrc);
            }
            catch (e) {
              console.warn(e);
              resolve(null);
            }
          }
          else {
            console.warn(x);
            resolve(null);
          }
        },
        ontimeout: (e) => {
          console.warn(e);
          resolve(null);
        }
      });
    });
  }
  // RGB channel remap function (lowlevel)
  function rgbRemap(raw, filter, width, height, rgb) {
    const [R, G, B] = rgb;
    for (let row = 0; row < height; ++row) {
      for (let col = 0; col < width; ++col) {
        let ind = col * 4 + row * width * 4;
        filter[ind] = R[raw[ind]];
        filter[ind + 1] = G[raw[ind + 1]];
        filter[ind + 2] = B[raw[ind + 2]];
        filter[ind + 3] = raw[ind + 3];
      }
    }
  }
  // Get ImageData from src with an optional update hook
  // Cross origin is supported
  async function GM_getImageData(src, fn) {
    return new Promise((resolve) => {
      GM_xmlhttpRequest({
        url: src,
        method: 'GET',
        // Blob or Arraybuffer responseType will slow down the page noticeably,
        // so we text type with x-user-defined charset to get raw binaries
        overrideMimeType: 'text/plain; charset=x-user-defined',
        // Progress update hook
        onprogress: (e) => {
          if (typeof (fn) == 'function') {
            if (e.total !== -1) {
              fn(e.loaded / e.total);
            }
            else {
              fn(-e.loaded);
            }
          }
        },
        onload: (e) => {
          if (e.status === 200) {
            // Get binary from text
            const imageResponseText = e.responseText;
            const l = imageResponseText.length;
            const bytes = new Uint8Array(l);
            for (let i = 0; i < l; i++) {
              bytes[i] = imageResponseText.charCodeAt(i) & 0xff;
            }
            // Decode png binary and resolve the image data arraybuffer,
            // createImageBitmap is a multi-thread operation,
            // and won't complain about CSP img-src errors when using Image object
            const type = (e.responseHeaders.match(/content\-type: *(.+)$/m) || ['', 'image/png'])[1];
            let ext;
            switch (type) {
              case 'image/apng':
                ext = '.apng';
                break;
              case 'image/bmp':
                ext = '.bmp';
                break;
              case 'image/gif':
                ext = '.gif';
                break;
              case 'image/x-icon':
                ext = '.ico';
                break;
              case 'image/jpeg':
                ext = '.jpg';
                break;
              case 'image/png':
                ext = '.png';
                break;
              case 'image/svg+xml':
                ext = '.svg';
                break;
              case 'image/tiff':
                ext = '.tiff';
                break;
              case 'image/webp':
                ext = '.webp';
                break;
              default:
                if (type.slice(0, 5) === 'image') {
                  let temp = type.match(/\/(.*)/);
                  if (temp) {
                    ext = '.' + temp;
                  } else {
                    ext = '';
                  }
                } else {
                  ext = (src.match(/\.[^\.]+$/) || [''])[0];
                }
                break;
            }
            createImageBitmap(new Blob([bytes], { type: type }))
              .then((e) => {
                const [width, height] = [e.width, e.height];
                const canvas = document.createElement('canvas');
                canvas.width = width;
                canvas.height = height;
                const context = canvas.getContext('2d');
                context.drawImage(e, 0, 0);
                e.close();
                resolve({
                  imageData: new ImageData(
                    context.getImageData(0, 0, width, height).data,
                    width,
                    height
                  ),
                  extension: ext
                });
              });
          }
          else {
            console.warn(e);
            resolve(null);
          }
        },
        onerror: (e) => {
          console.warn(e);
          resolve(null);
        }
      });
    });
  }

  /*--- Diff and Filter Core Function ---*/
  // Diff images
  async function diffImage(img1, img2, init = { alpha: 0.5, threshold: 0.007 }, worker = diffWorker) {
    if (
      img1 && img2 &&
      img1.width === img2.width &&
      img1.height === img2.height
    ) {
      if (worker) {// async diff
        const [
          raw1,
          raw2,
          width,
          height
        ] = [
            img1.data.buffer,
            img2.data.buffer,
            img1.width,
            img1.height
          ];
        const key = '' + Date.now();
        worker.onmessage = (e) => {
          const returnKey = e.data.key;
          const resolve = worker.keyPool[returnKey];
          if (resolve) {
            resolve(
              new ImageData(
                new Uint8ClampedArray(e.data.diff),
                e.data.width,
                e.data.height
              )
            );
          }
        };
        worker.postMessage({
          img1: raw1,
          img2: raw2,
          width: width,
          height: height,
          init: init,
          key: key
        }, [raw1, raw2]);
        return new Promise((res) => {
          worker.keyPool[key] = res;
        });
      } else {// sync diff
        const [data1, data2, width, height] = [
          img1.data,
          img2.data,
          img1.width,
          img1.height
        ];
        const res = new Uint8ClampedArray(data1);
        pixelmatch(data1, data2, res, width, height, init);
        return (
          new ImageData(
            res,
            width,
            height
          )
        );
      }
    } else {
      return null;
    }
  }
  // RGB channel remap filter image
  async function rgbImage(img, argument) {
    if (img) {
      if (argument instanceof Worker) {
        const worker = argument;
        const [raw, width, height] = [img.data.buffer, img.width, img.height];
        const key = '' + Date.now();
        worker.onmessage = (e) => {
          const returnKey = e.data.key;
          const resolve = worker.keyPool[returnKey];
          if (resolve) {
            resolve(
              new ImageData(
                new Uint8ClampedArray(e.data.filter),
                e.data.width,
                e.data.height
              )
            );
          }
        };
        await loadBufferPromise;
        worker.postMessage({
          img: raw,
          width: width,
          height: height,
          key: key
        }, [raw]);
        return new Promise((res) => {
          worker.keyPool[key] = res;
        });
      } else {
        const rgb = argument;
        const [data, width, height] = [img.data, img.width, img.height];
        const res = new Uint8ClampedArray(data);
        rgbRemap(data, res, width, height, rgb);
        return (
          new ImageData(
            res,
            width,
            height
          )
        );
      }
    } else {
      return null;
    }
  }

  function reRenderImage(image, scale) {
    if (scale > 10) {
      image.style['image-rendering'] = 'pixelated';
    } else {
      image.style['image-rendering'] = 'auto';
    }
  }

  /*--- Get Images: Original, Diffed or Filtered ---*/
  // Get original image function
  function getOriginalImage(target, $overlay) {
    if (target.easyCompare && target.easyCompare.originalImage) {
      const originalImage = target.easyCompare.originalImage;
      if (originalImage.ready) {
        originalImage.style.width = `${scale * 10}%`;
        reRenderImage(originalImage, scale);
      }
      return originalImage;
    } else {
      const originalCanvas = makeCanvas();
      const updateProgress = (p) => {
        if (p !== null && p >= 0) {
          drawText(originalCanvas, `Loading ${(p * 100).toFixed(1)}%`);
        } else if (p < 0) {
          drawText(originalCanvas, `Loading...`);
        }
      };
      const resolveOriginal = (src, onprogress, resolve) => {
        GM_getImageData(src, onprogress).then(({ imageData: originalImageData, extension }) => {
          resolve(originalImageData);
          originalCanvas.src = src;
          originalCanvas.ext = extension;
          drawImage(originalCanvas, originalImageData);
          originalCanvas.style.width = `${scale * 10}%`;
          reRenderImage(originalCanvas, scale);
          originalCanvas.ready = true;
        });
      };
      drawText(originalCanvas, `Loading...`);
      originalCanvas.ready = false;
      originalCanvas.targetImage = target;
      $overlay.append(originalCanvas.parentElement);
      if (!target.easyCompare) {
        target.easyCompare = {};
      }
      target.easyCompare.originalImage = originalCanvas;
      target.easyCompare.originalImagePromise = onprogress => new Promise(async (resolve) => {
        let realSrc = target.src;
        // Parse original src from thumb src
        for (let pairs of t2oLib) {
          realSrc = realSrc.replace(pairs[0], pairs[1]);
          if (realSrc !== target.src) {
            resolveOriginal(realSrc, onprogress, resolve);
            return;
          }
        }
        // Guess original src from hyper link
        let href, hrefOriginal;
        if ((hrefOriginal = target.parentElement.href, href = hrefOriginal)) {
          for (let pairs of skipRedirLib) {
            href = href.replace(pairs[0], pairs[1]);
            if (href !== hrefOriginal) {
              break;
            }
          }
          if (href.match(/\.png$|\.jpe?g$|\.webp|\.gif|\.bmp|\.svg$/)) {
            resolveOriginal(href, onprogress, resolve);
            return;
          } else {
            guessOriginalImage(href).then(src => {
              resolveOriginal(src || realSrc, onprogress, resolve);
              return;
            });
          }
        } else {
          resolveOriginal(realSrc, onprogress, resolve);
          return;
        }
      });
      target.easyCompare.originalImagePromise(updateProgress);
      return originalCanvas;
    }
  }
  // Get diffed image function
  function getDiffedImage(target, base, $overlay) {
    if (target.src === base.src) {
      return getOriginalImage(target);
    }
    if (target.easyCompare && target.easyCompare[base.src]) {
      target.easyCompare[base.src].targetImage = target;
      target.easyCompare[base.src].baseImage = base;
      const diffedCanvas = target.easyCompare[base.src];
      if (diffedCanvas.ready) {
        diffedCanvas.style.width = `${scale * 10}%`;
        reRenderImage(diffedCanvas, scale);
      }
      return diffedCanvas;
    } else {
      const diffedCanvas = makeCanvas();
      drawText(diffedCanvas, 'Loading...');
      diffedCanvas.ready = false;
      diffedCanvas.targetImage = target;
      diffedCanvas.baseImage = base;
      diffedCanvas.threshold = -1;
      diffedCanvas.step = 0.001;
      $overlay.append(diffedCanvas.parentElement);
      if (!target.easyCompare) {
        target.easyCompare = {};
      }
      target.easyCompare[base.src] = diffedCanvas;
      if (!base.easyCompare) {
        base.easyCompare = {};
      }
      base.easyCompare[target.src] = diffedCanvas;

      let progress = [0, 0];
      // Progress update function
      const updateProgress = (p, ind) => {
        if (p !== null && p >= 0 && ind !== null) {
          progress[ind] = p;
          drawText(diffedCanvas, `Loading ${((progress[0] + progress[1]) * 50).toFixed(1)}%`);
        }
        else if (p < 0) {
          drawText(diffedCanvas, 'Loading...');
        }
        else {
          drawText(diffedCanvas, 'Diffing...');
        }
      };
      getOriginalImage(target, $overlay);
      getOriginalImage(base, $overlay);
      Promise.all([
        target.easyCompare.originalImagePromise((p) => updateProgress(p, 0)),
        base.easyCompare.originalImagePromise((p) => updateProgress(p, 1))
      ]).then(imageData => {
        updateProgress(null, null);
        return diffImage(...imageData, {
          alpha: 0.5,
          threshold: 0.007
        });
      }).then((diffedImageData) => {
        if (diffedImageData === null) {
          drawText(diffedCanvas, 'Sizes Not Match');
        } else {
          drawImage(diffedCanvas, diffedImageData);
          diffedCanvas.ext = '.png';
          diffedCanvas.threshold = 0.007;
          diffedCanvas.style.width = `${scale * 10}%`;
          reRenderImage(diffedCanvas, scale);
          diffedCanvas.ready = true;
        }
      }).catch((err) => {
        console.warn(err);
        drawText(diffedCanvas, 'Something Went Wrong');
      });
      return diffedCanvas;
    }
  }
  // Get filtered image function
  function getFilteredImage(target, ftType, $overlay) {
    if (target.easyCompare && target.easyCompare[ftType]) {
      const filteredCanvas = target.easyCompare[ftType];
      if (filteredCanvas.ready) {
        filteredCanvas.style.width = `${scale * 10}%`;
        reRenderImage(filteredCanvas, scale);
      }
      return filteredCanvas;
    } else {
      const filteredCanvas = makeCanvas();
      drawText(filteredCanvas, 'Loading...');
      filteredCanvas.ready = false;
      filteredCanvas.targetImage = target;
      $overlay.append(filteredCanvas.parentElement);
      if (!target.easyCompare) {
        target.easyCompare = {};
      }
      target.easyCompare[ftType] = filteredCanvas;
      // Progress Update Function
      const updateProgress = (p) => {
        if (p !== null && p >= 0) {
          drawText(filteredCanvas, `Loading ${(p * 100).toFixed(1)}%`);
        } else if (p < 0) {
          drawText(filteredCanvas, 'Loading...');
        } else {
          drawText(filteredCanvas, 'Filtering...');
        }
      };
      // Wait original image and filter the original image
      getOriginalImage(target, $overlay);
      target.easyCompare
        .originalImagePromise(updateProgress).then((imageData) => {
          updateProgress(null);
          return filterImage[ftType](imageData);
        }).then(filterdImageData => {
          drawImage(filteredCanvas, filterdImageData);
          filteredCanvas.ext = '.png';
          filteredCanvas.style.width = `${scale * 10}%`;
          reRenderImage(filteredCanvas, scale);
          filteredCanvas.ready = true;
        });
      return filteredCanvas;
    }
  }

  /*--- UI Response Functions ---*/
  // Function to acquire active image
  function getActive($overlay) {
    return $overlay.find('canvas:visible');
  }
  // Function fired when compare button is activated
  function activateCompare($target) {
    $target.attr({
      'fill': '#008000'
    }).css({
      'cursor': 'pointer',
      'opacity': '1'
    })[0].state = true;
  }
  // Function fired when leaving image
  function leaveImage($overlay, target = undefined) {
    const original = getActive($overlay).hide()[0];
    if (((original && (target = original.targetImage)) || target) &&
      target.easyCompare && target.easyCompare.boxShadow !== undefined) {
      $(target).css('box-shadow', target.easyCompare.boxShadow);
    }
  }
  // Function fired when compare button is clicked and toggled on
  // (Main UI Logic)
  function enterCompare($overlay, $images, $message) {
    if (Mousetrap) {
      Mousetrap.pause();
    }
    $overlay.show()[0].state = true;
    let colors = ['red', 'blue'];
    let step = 1, baseImage;
    let ftType = 'none';
    let fadingTime = 300;
    // Mouse enter event
    $images.on('mouseenter.compare', (e, triggeredShiftKey) => {
      const target = e.currentTarget;
      clearTimeout(timeout);
      leaveImage($overlay);
      if (!target.easyCompare) {
        target.easyCompare = {};
        target.easyCompare.boxShadow = target.style['box-shadow'];
      }
      $(target).css({
        'box-shadow': '0px 0px 8px ' + colors[0]
      });
      let displayedImage;
      if ((e.shiftKey || triggeredShiftKey) && baseImage) {
        displayedImage = $(getDiffedImage(target, baseImage, $overlay))
          .css('outline-color', colors[0])
          .show();
      } else {
        switch (ftType) {
          case 'none':
            displayedImage = $(getOriginalImage(target, $overlay))
              .css('outline-color', colors[0])
              .show();
            break;
          default:
            displayedImage = $(getFilteredImage(target, ftType, $overlay))
              .css('outline-color', colors[0])
              .show();
            break;
        }
      }
      colors.push(colors.shift());
      //Mouse leave event
    }).on('mouseleave.compare', (e) => {
      const target = e.currentTarget;
      timeout = setTimeout(() => {
        leaveImage($overlay, target);
      }, 200);
    });

    // KeyBoard functions
    function setBaseImage() {
      try {
        baseImage = getActive($overlay)[0].targetImage;
      } catch (err) {
        baseImage = undefined;
        if (!(err instanceof TypeError)) {
          console.warn(err);
        }
      }
    }
    function downloadImage(name = 'easycompare') {
      try {
        const target = getActive($overlay)[0];
        const url = target.src || target.toDataURL('image/png').replace(/^data:image\/[^;]/, 'data:application/octet-stream');
        const ext = target.ext || '';
        GM_download({
          url: url,
          name: name + ext
        });
      } catch (err) {
        if (!(err instanceof TypeError)) {
          console.warn(err);
        }
      }
    }
    function toggleFilter(filter) {
      ftType = (ftType === filter ? 'none' : filter);
      try {
        const target = getActive($overlay).hide()[0];
        let $displayImage;
        if (ftType === 'none') {
          $displayImage = $(getOriginalImage(target.targetImage, $overlay));
        } else {
          $displayImage = $(getFilteredImage(target.targetImage, ftType, $overlay));
        }
        $displayImage
          .css('outline-color', target.style['outline-color'])
          .show();
      } catch (err) {
        if (!(err instanceof TypeError)) {
          console.warn(err);
        }
      }
    }
    function adjustView(up) {
      try {
        if (up && scale < 10) {
          scale = scale + 1;
        } else if (up && scale < 30) {
          scale = scale + 2;
        } else if (!up && scale > 10) {
          scale = scale - 2;
        } else if (!up && scale > 1) {
          scale = scale - 1;
        }
        const target = getActive($overlay)[0];
        if (target.ready) {
          target.style.width = `${scale * 10}%`;
          reRenderImage(target, scale);
        }
        $message.text(`Zoom: ${parseInt(scale * 10)}%`).css('opacity', '1');
        setTimeout(() => {
          $message.css('opacity', '0');
        }, fadingTime);
      } catch (err) {
        if (!(err instanceof TypeError)) {
          console.warn(err);
        }
      }
    }
    function setView(scl) {
      try {
        if (scale !== scl) {
          scale = scl;
          const target = getActive($overlay)[0];
          if (target.ready) {
            target.style.width = `${scale * 10}%`;
            reRenderImage(target, scale);
          }
          $message.text(`Zoom: ${parseInt(scale * 10)}%`).css('opacity', '1');
          setTimeout(() => {
            $message.css('opacity', '0');
          }, fadingTime);
        }
      } catch (err) {
        if (!(err instanceof TypeError)) {
          console.warn(err);
        }
      }
    }
    function adjustThreshold(up) {
      try {
        const target = getActive($overlay)[0];
        let threshold = target.threshold;
        if (threshold !== undefined && threshold >= 0) {
          const thresholdPrev = threshold;
          $message.text(`Threshold: ${thresholdPrev.toFixed(4)}`).css('opacity', '1');
          if (up) {
            threshold += target.step;
            if (threshold > 1) {
              threshold = 1;
            }
          } else {
            threshold -= target.step;
            if (threshold < 0) {
              threshold = 0;
            }
          }
          target.threshold = -1;
          const [
            baseCanvas,
            targetCanvas
          ] = [
              target.baseImage.easyCompare.originalImage,
              target.targetImage.easyCompare.originalImage
            ];
          diffImage(
            baseCanvas.getContext('2d').getImageData(0, 0, baseCanvas.width, baseCanvas.height),
            targetCanvas.getContext('2d').getImageData(0, 0, targetCanvas.width, targetCanvas.height),
            {
              alpha: 0.5,
              threshold: threshold
            }
          ).then((imageData) => {
            target.getContext('2d').putImageData(imageData, 0, 0);
            $message.text(`Threshold: ${threshold.toFixed(4)}`).css('opacity', '1');
            setTimeout(() => {
              target.threshold = threshold;
              $message.css('opacity', '0');
            }, fadingTime);
          });
        }
      } catch (err) {
        if (!(err instanceof TypeError)) {
          console.warn(err);
        }
      }
    }
    function adjustStep(left) {
      try {
        const target = getActive($overlay)[0];
        let step = target.step;
        if (step) {
          if (left && step <= 0.1) {
            target.step = step * 10;
          } else if (left) {
            target.step = 1.0;
          } else if (!left && step >= 0.001) {
            target.step = step / 10;
          } else {
            target.step = 0.0001;
          }
          $message.text(`Step: ${target.step.toFixed(4)}`).css('opacity', '1');
          setTimeout(() => $message.css('opacity', '0'), fadingTime);
        }
      } catch (err) {
        if (!(err instanceof TypeError)) {
          console.warn(err);
        }
      }
    }
    function clearCache() {
      try {
        leaveImage($overlay, getActive($overlay)[0].targetImage);
      } catch (err) {
        if (!(err instanceof TypeError)) {
          console.warn(err);
        }
      }
      $overlay.find('canvas').toArray().forEach(e => {
        const target = e.targetImage;
        delete target.easyCompare;
        e.parentElement.remove();
      });
    }
    function switchImage(left, shiftKey) {
      try {
        const targetImage = getActive($overlay)[0].targetImage;
        const index = $images.index(targetImage);
        leaveImage($overlay, targetImage);
        const nextElem = $images[left ? index - step : index + step] || $images[index];
        $(nextElem).trigger('mouseenter', [shiftKey]);
      } catch (err) {
        if (!(err instanceof TypeError)) {
          console.warn(err);
        }
      }
    }

    // Scroll and Keyboard event
    $(document).on('scroll.compare', (e) => {
      const temp = getActive($overlay)[0];
      if (temp) {
        const $prev = $(temp.targetImage);
        if (!$prev.is(':hover')) {
          leaveImage($overlay, $prev[0]);
          $images.find('img:hover').trigger('mousenter');
        }
      }// Hot-Keys
    }).on('keydown.compare', (e) => {
      e.preventDefault();
      e.stopImmediatePropagation();
      switch (e.key) {
        case 'Escape':
          exitCompare($overlay, $images);
          break;
        case 'Shift':
          setBaseImage();
          break;
        case '+': case '=':
          if (e.ctrlKey) {
            adjustView(true);
          }
          break;
        case '-': case '_':
          if (e.ctrlKey) {
            adjustView(false);
          }
          break;
        case 'O': case 'o':
          if (e.ctrlKey) {
            setView(10);
          }
          break;
        case 'P': case 'p':
          if (e.ctrlKey) {
            setView(30);
          }
          break;
        case 'S': case 's':
          if (e.ctrlKey) {
            downloadImage();
          } else {
            toggleFilter('solar');
          }
          break;
        case 'A': case 'a':
          toggleFilter('s2lar');
          break;
        case 'I': case 'i':
          if (e.ctrlKey) {
            setView(1);
          } else {
            adjustThreshold(true);
          }
          break;
        case 'ArrowUp':
          adjustThreshold(true);
          break;
        case 'K': case 'k': case 'ArrowDown':
          adjustThreshold(false);
          break;
        case 'J': case 'j': case 'ArrowLeft':
          adjustStep(true);
          break;
        case 'L': case 'l': case 'ArrowRight':
          if (e.ctrlKey) {
            clearCache();
          } else {
            adjustStep(false);
          }
          break;
        case 'Q':
        case 'q':
          $overlay.css('opacity', 0.5);
          break;
        case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
          step = parseInt(e.key);
          break;
        case '0':
          step = 10;
          break;
        case 'W': case 'w':
          switchImage(true, e.shiftKey);
          break;
        case 'E': case 'e':
          switchImage(false, e.shiftKey);
          break;
      }
      return false;
    }).on('keyup.compare', (e) => {
      e.preventDefault();
      e.stopImmediatePropagation();
      switch (e.key) {
        case 'Q':
        case 'q':
          $overlay.css('opacity', '');
          break;
      }
      return false;
    });
  }
  // Function fired when compare button is clicked and toggled off
  // or quit via keyboard 'esc'
  function exitCompare($overlay, $images) {
    if (Mousetrap) {
      Mousetrap.unpause();
    }
    leaveImage($overlay);
    $overlay.hide()[0].state = false;
    $images
      .off('mouseenter.compare')
      .off('mouseleave.compare');
    $(document)
      .off('scroll.compare')
      .off('keydown.compare');
  }

  /*--- Building Blocks ---*/
  // A message on the whole page
  const $message = $('<div>').css({
    'top': '50%',
    'left': '50%',
    'z-index': 2147483647,
    'position': 'fixed',
    'transform': 'translate(-50%, -50%)',
    'opacity': '0',
    'vertical-align': 'middle',
    'pointer-events': 'none',
    'transition': 'all 0.1s',
    'font-size': '500%',
    'color': 'yellow',
    'font-weight': 'bold'
  });
  // An overlay on the whole page
  const $overlay = $('<div/>').css({
    'id': 'easy-compare-overlay',
    'position': 'fixed',
    'top': 0,
    'right': 0,
    'bottom': 0,
    'left': 0,
    'z-index': 2147483646,
    'background-color': 'rgba(0, 0, 0, 0.75)',
    'pointer-events': 'none',
    'display': 'none'
  }).append($message);
  // The compare button
  const $compareButton = $(`<svg xmlns="http://www.w3.org/2000/svg">
<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"/>
</svg>`).attr({
    'width': '30',
    'height': '30',
    'viewBox': '0 0 48 48',
    'stroke': 'white',
    'stroke-width': '5px',
    'fill': 'gray'
  }).css({
    'position': 'fixed',
    'top': '0px',
    'right': '0px',
    'padding': '15px',
    'box-sizing': 'content-box',
    'z-index': 2147483647,
    'paint-order': 'stroke',
    'opacity': 0,
    'transition': 'all 0.2s',
    'cursor': 'auto'
  }).on('mouseenter', (e) => {
    const $target = $(e.currentTarget);
    if ($target[0].manualFlag) {
      $target.attr({
        'fill': 'gray'
      }).css({
        'opacity': 0.2,
        'pointer-events': 'none'
      });
      $target[0].manualFlag = false;
      const clientWidth = document.documentElement.clientWidth;
      $(document).on('mousemove.compare', ({ clientX, clientY }) => {
        if (clientX < clientWidth - 61 || clientY > 61) {
          $target[0].insideFlag = 0;
          clearTimeout(timeout);
          $target.attr({
            'fill': 'gray'
          }).css({
            'cursor': 'auto',
            'opacity': 0,
            'pointer-events': 'auto'
          })[0].state = false;
          $(document).off('mousemove.compare');
          $target[0].manualFlag = true;
        } else if (clientX >= clientWidth - 45 && clientX <= clientWidth - 15 && clientY >= 15 && clientY <= 45) {
          if (!$target[0].insideFlag) {
            $target[0].insideFlag = 1;
            timeout = setTimeout(() => {
              activateCompare($target);
              $target.css({
                'pointer-events': 'auto'
              });
            }, $overlay[0].state ? 0 : 1000);
          }
        } else if (clientX < clientWidth - 45 || clientX > clientWidth - 15 || clientY < 15 || clientY > 45) {
          $target[0].insideFlag = 0;
          clearTimeout(timeout);
          $target.attr({
            'fill': 'gray'
          }).css({
            'cursor': 'auto',
            'opacity': 0.2,
            'pointer-events': 'none'
          })[0].state = false;
        }
      });
    }
  }).click((e) => {
    if (e.currentTarget.state) {
      switch ($overlay[0].state) {
        case false:
          enterCompare($overlay, $(':not("#easy-compare-overlay") img:visible'), $message);
          break;
        case true:
          exitCompare($overlay, $(':not("#easy-compare-overlay") img:visible'));
          break;
      }
    }
    else {
      let x = e.clientX;
      let y = e.clientY;
      const lowerElement = document
        .elementsFromPoint(x, y)
        .find(e => !['svg', 'path'].includes(e.tagName));
      lowerElement.click();
    }
  }).mousedown((e) => {
    if (e.currentTarget.state) {
      $(e.currentTarget).attr({
        'fill': '#006000'
      });
    }
  }).mouseup((e) => {
    if (e.currentTarget.state) {
      $(e.currentTarget).attr({
        'fill': '#008000'
      });
    }
  });
  $compareButton[0].manualFlag = true;
  $compareButton[0].insideFlag = false;

  /*--- Insert to Document ---*/
  $overlay[0].state = false;
  $compareButton[0].state = false;
  $('body').append($compareButton).append($overlay);

})(window.$.noConflict(true),
  unsafeWindow.Mousetrap,
  window.pixelmatch,
  unsafeWindow.URL.createObjectURL ?
    unsafeWindow.URL :
    unsafeWindow.webkitURL);