// ==UserScript==
// @name Easy Compare
// @description Compare images
// @version 0.7
// @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://bundle.run/[email protected]
// /require https://cdn.staticfile.org/pako/1.0.10/pako.min.js
// /require https://cdn.staticfile.org/upng-js/2.1.0/UPNG.min.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 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 *
// ==/UserScript==
// # TODO List
// ☑ guess original images from hyper link
// ☑ redirect url chopper: deferer, anonymouse
// ☑ image diff by hold "Shift" and switch to another image: https://bundle.run/[email protected]
// ☑ solar curve filter toggled by "s": see bandings clearly
// ☑ save current active image by "ctrl + s"
// ☑ clear caches by "ctrl + l"
// ☐ canvasless (async/sync)
// ☐ more sites support
// ☐ other filters?
// ☐ webgl acceleration (webgl in worker?)
// jshint esversion:8
(async function ($, Mousetrap, pixelmatch, UPNG, URL) {
'use strict';
// Solar Curve;
const [Rc, Gc, Bc] = [
new Uint8Array([0, 2, 9, 21, 37, 56, 78, 101, 125, 149, 172, 193, 212, 228, 241, 250, 254, 255, 252, 246, 235, 222, 206, 188, 168, 148, 127, 106, 86, 67, 49, 34, 22, 12, 5, 1, 0, 1, 6, 14, 24, 36, 50, 66, 82, 100, 118, 136, 154, 171, 188, 203, 216, 228, 238, 245, 251, 254, 255, 254, 251, 246, 239, 230, 219, 207, 194, 180, 165, 149, 134, 118, 103, 88, 73, 60, 48, 36, 26, 18, 11, 6, 2, 0, 0, 1, 4, 8, 14, 22, 30, 40, 51, 63, 75, 88, 102, 115, 129, 143, 156, 170, 182, 194, 205, 216, 225, 233, 240, 246, 250, 253, 255, 255, 254, 252, 248, 243, 237, 230, 221, 212, 201, 190, 179, 166, 154, 141, 128, 114, 101, 89, 76, 65, 54, 43, 34, 25, 18, 12, 7, 3, 1, 0, 0, 2, 5, 9, 15, 22, 30, 39, 50, 61, 73, 85, 99, 112, 126, 140, 153, 167, 180, 192, 204, 215, 225, 233, 241, 247, 251, 254, 255, 255, 253, 249, 244, 237, 229, 219, 207, 195, 182, 167, 152, 137, 121, 106, 90, 75, 61, 48, 36, 25, 16, 9, 4, 1, 0, 1, 4, 10, 17, 27, 39, 52, 67, 84, 101, 119, 137, 155, 173, 189, 205, 219, 231, 241, 249, 254, 255, 254, 250, 243, 233, 221, 206, 188, 169, 149, 128, 107, 87, 67, 49, 33, 20, 9, 3, 0, 1, 5, 14, 27, 43, 62, 83, 106, 130, 154, 177, 199, 218, 234, 246, 253]),
new Uint8Array([60, 39, 22, 10, 2, 0, 2, 9, 21, 37, 56, 78, 101, 125, 149, 172, 193, 212, 228, 241, 250, 254, 255, 252, 246, 235, 222, 206, 188, 168, 148, 127, 106, 86, 67, 49, 34, 22, 12, 5, 1, 0, 1, 6, 14, 24, 36, 50, 66, 82, 100, 118, 136, 154, 171, 188, 203, 216, 228, 238, 245, 251, 254, 255, 254, 251, 246, 239, 230, 219, 207, 194, 180, 165, 149, 134, 118, 103, 88, 73, 60, 48, 36, 26, 18, 11, 6, 2, 0, 0, 1, 4, 8, 14, 22, 30, 40, 51, 63, 75, 88, 102, 115, 129, 143, 156, 170, 182, 194, 205, 216, 225, 233, 240, 246, 250, 253, 255, 255, 254, 252, 248, 243, 237, 230, 221, 212, 201, 190, 179, 166, 154, 141, 128, 114, 101, 89, 76, 65, 54, 43, 34, 25, 18, 12, 7, 3, 1, 0, 0, 2, 5, 9, 15, 22, 30, 39, 50, 61, 73, 85, 99, 112, 126, 140, 153, 167, 180, 192, 204, 215, 225, 233, 241, 247, 251, 254, 255, 255, 253, 249, 244, 237, 229, 219, 207, 195, 182, 167, 152, 137, 121, 106, 90, 75, 61, 48, 36, 25, 16, 9, 4, 1, 0, 1, 4, 10, 17, 27, 39, 52, 67, 84, 101, 119, 137, 155, 173, 189, 205, 219, 231, 241, 249, 254, 255, 254, 250, 243, 233, 221, 206, 188, 169, 149, 128, 107, 87, 67, 49, 33, 20, 9, 3, 0, 1, 5, 14, 27, 43, 62, 83, 106, 130, 154, 177]),
new Uint8Array([56, 78, 101, 125, 149, 172, 193, 212, 228, 241, 250, 254, 255, 252, 246, 235, 222, 206, 188, 168, 148, 127, 106, 86, 67, 49, 34, 22, 12, 5, 1, 0, 1, 6, 14, 24, 36, 50, 66, 82, 100, 118, 136, 154, 171, 188, 203, 216, 228, 238, 245, 251, 254, 255, 254, 251, 246, 239, 230, 219, 207, 194, 180, 165, 149, 134, 118, 103, 88, 73, 60, 48, 36, 26, 18, 11, 6, 2, 0, 0, 1, 4, 8, 14, 22, 30, 40, 51, 63, 75, 88, 102, 115, 129, 143, 156, 170, 182, 194, 205, 216, 225, 233, 240, 246, 250, 253, 255, 255, 254, 252, 248, 243, 237, 230, 221, 212, 201, 190, 179, 166, 154, 141, 128, 114, 101, 89, 76, 65, 54, 43, 34, 25, 18, 12, 7, 3, 1, 0, 0, 2, 5, 9, 15, 22, 30, 39, 50, 61, 73, 85, 99, 112, 126, 140, 153, 167, 180, 192, 204, 215, 225, 233, 241, 247, 251, 254, 255, 255, 253, 249, 244, 237, 229, 219, 207, 195, 182, 167, 152, 137, 121, 106, 90, 75, 61, 48, 36, 25, 16, 9, 4, 1, 0, 1, 4, 10, 17, 27, 39, 52, 67, 84, 101, 119, 137, 155, 173, 189, 205, 219, 231, 241, 249, 254, 255, 254, 250, 243, 233, 221, 206, 188, 169, 149, 128, 107, 87, 67, 49, 33, 20, 9, 3, 0, 1, 5, 14, 27, 43, 62, 83, 106, 130, 154, 177, 199, 218, 234, 246, 253, 255, 253, 245, 233, 216])
];
async function loadBuffer(worker) {
return new Promise((resolve) => {
rainbowWorker.onmessage = (e) => { resolve(e.data.result); };
rainbowWorker.postMessage({
Rc: Rc.buffer,
Gc: Gc.buffer,
Bc: Bc.buffer
}, [Rc.buffer, Gc.buffer, Bc.buffer]);
});
}
// Diff, Rainbow Worker Initialization
let diffWorker, rainbowWorker;
const diffWorkerScript = `const defaultOptions={threshold:.1,includeAA:!1,alpha:.1,aaColor:[255,255,0],diffColor:[255,0,0],diffMask:!1};function pixelmatch(a,b,c,d,e,f){if(!isPixelData(a)||!isPixelData(b)||c&&!isPixelData(c))throw new Error("Image data: Uint8Array, Uint8ClampedArray or Buffer expected.");if(a.length!==b.length||c&&c.length!==a.length)throw new Error("Image sizes do not match.");if(a.length!==4*(d*e))throw new Error("Image data size does not match width/height.");f=Object.assign({},defaultOptions,f);const g=d*e,h=new Uint32Array(a.buffer,a.byteOffset,g),j=new Uint32Array(b.buffer,b.byteOffset,g);let k=!0;for(let l=0;l<g;l++)if(h[l]!==j[l]){k=!1;break}if(k){if(c&&!f.diffMask)for(let b=0;b<g;b++)drawGrayPixel(a,4*b,f.alpha,c);return 0}const l=35215*f.threshold*f.threshold;let m=0;const[n,o,p]=f.aaColor,[q,r,s]=f.diffColor;for(let g=0;g<e;g++)for(let h=0;h<d;h++){const i=4*(g*d+h),j=colorDelta(a,b,i,i);j>l?!f.includeAA&&(antialiased(a,h,g,d,e,b)||antialiased(b,h,g,d,e,a))?c&&!f.diffMask&&drawPixel(c,i,n,o,p):(c&&drawPixel(c,i,q,r,s),m++):c&&!f.diffMask&&drawGrayPixel(a,i,f.alpha,c)}return m}function isPixelData(a){return ArrayBuffer.isView(a)&&1===a.constructor.BYTES_PER_ELEMENT}function antialiased(a,b,c,d,e,f){const g=Math.max(b-1,0),h=Math.max(c-1,0),i=Math.min(b+1,d-1),j=Math.min(c+1,e-1);let k,l,m,n,o=b===g||b===i||c===h||c===j?1:0,p=0,q=0;for(let r=g;r<=i;r++)for(let e=h;e<=j;e++){if(r===b&&e===c)continue;const f=colorDelta(a,a,4*(c*d+b),4*(e*d+r),!0);if(0!==f)f<p?(p=f,k=r,l=e):f>q&&(q=f,m=r,n=e);else if(o++,2<o)return!1}return 0!==p&&0!==q&&(hasManySiblings(a,k,l,d,e)&&hasManySiblings(f,k,l,d,e)||hasManySiblings(a,m,n,d,e)&&hasManySiblings(f,m,n,d,e))}function hasManySiblings(a,b,c,d,e){const f=Math.max(b-1,0),g=Math.max(c-1,0),h=Math.min(b+1,d-1),i=Math.min(c+1,e-1),j=4*(c*d+b);let k=b===f||b===h||c===g||c===i?1:0;for(let l=f;l<=h;l++)for(let e=g;e<=i;e++){if(l===b&&e===c)continue;const f=4*(e*d+l);if(a[j]===a[f]&&a[j+1]===a[f+1]&&a[j+2]===a[f+2]&&a[j+3]===a[f+3]&&k++,2<k)return!0}return!1}function colorDelta(a,b,c,d,e){let f=a[c+0],g=a[c+1],h=a[c+2],j=a[c+3],k=b[d+0],l=b[d+1],m=b[d+2],n=b[d+3];if(j===n&&f===k&&g===l&&h===m)return 0;255>j&&(j/=255,f=blend(f,j),g=blend(g,j),h=blend(h,j)),255>n&&(n/=255,k=blend(k,n),l=blend(l,n),m=blend(m,n));const o=rgb2y(f,g,h)-rgb2y(k,l,m);if(e)return o;const p=rgb2i(f,g,h)-rgb2i(k,l,m),i=rgb2q(f,g,h)-rgb2q(k,l,m);return .5053*o*o+.299*p*p+.1957*i*i}function rgb2y(a,c,d){return .29889531*a+.58662247*c+.11448223*d}function rgb2i(a,c,d){return .59597799*a-.2741761*c-.32180189*d}function rgb2q(a,c,d){return .21147017*a-.52261711*c+.31114694*d}function blend(b,c){return 255+(b-255)*c}function drawPixel(a,c,d,e,f){a[c+0]=d,a[c+1]=e,a[c+2]=f,a[c+3]=255}function drawGrayPixel(a,c,d,e){const f=a[c+0],h=a[c+1],g=a[c+2],b=blend(rgb2y(f,h,g),d*a[c+3]/255);drawPixel(e,c,b,b,b)}self.onmessage=a=>{img1=new Uint8ClampedArray(a.data.img1),img2=new Uint8ClampedArray(a.data.img2),diff=new Uint8ClampedArray(img1),width=a.data.width,height=a.data.height,init=a.data.init,key=a.data.key;try{pixelmatch(img1,img2,diff,width,height,init),self.postMessage({diff:diff.buffer,width:width,height:height,key:key},[diff.buffer])}catch(a){console.warn(a),self.postMessage({diff:null,key:key})}};`;
const rainbowWorkerScript = `let Rc,Gc,Bc;self.onmessage=a=>{const b=a.data.key;if(a.data.Rc&&a.data.Gc&&a.data.Bc)Rc=new Uint8ClampedArray(a.data.Rc),Gc=new Uint8ClampedArray(a.data.Gc),Bc=new Uint8ClampedArray(a.data.Bc),self.postMessage({result:!0});else{const c=new Uint8ClampedArray(a.data.img),d=new Uint8ClampedArray(c),e=a.data.width,f=a.data.height;try{for(let a=0;a<f;++a)for(let b,f=0;f<e;++f)b=4*f+4*(a*e),d[b]=Rc[c[b]],d[b+1]=Gc[c[b+1]],d[b+2]=Bc[c[b+2]],d[b+3]=c[b+3];self.postMessage({filter:d.buffer,width:e,height:f,key:b},[d.buffer])}catch(a){console.warn(a),self.postMessage({filter:null,key:b})}}};`;
try {
const diffWorkerBlob = new Blob([diffWorkerScript], { type: 'application/javascript' });
diffWorker = new Worker(URL.createObjectURL(diffWorkerBlob));
diffWorker.keyPool = {};
URL.revokeObjectURL(diffWorkerBlob);
const rainbowWorkerBlob = new Blob([rainbowWorkerScript], { type: 'application/javascript' });
rainbowWorker = new Worker(URL.createObjectURL(rainbowWorkerBlob));
rainbowWorker.keyPool = {};
URL.revokeObjectURL(rainbowWorkerBlob);
await loadBuffer(rainbowWorker);
} catch (e) {
try {
const diffWorkerDataURI = `data:application/javascript,${encodeURIComponent(diffWorkerScript)}`;
diffWorker = new Worker(diffWorkerDataURI);
diffWorker.keyPool = {};
const rainbowWorkerDataURI = `data:application/javascript,${encodeURIComponent(rainbowWorkerScript)}`;
rainbowWorker = new Worker(rainbowWorkerDataURI);
rainbowWorker.keyPool = {};
await loadBuffer(rainbowWorker);
} catch (e) {
diffWorker = null;
rainbowWorker = null;
}
}
// Title: Mousetrap Pause Plugin
// Reference: https://github.com/ccampbell/mousetrap/tree/master/plugins/pause
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;
};
}
// A global timeout ID holder
let timeout;
// A global scale factor
let scale = 1;
// 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#img'
];
// 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);
}
});
});
}
// Get image uint8 array buffer
async function getImageBytesBuffer(src, fn) {
const imageArrayBuffer = await new Promise((resolve) => {
GM_xmlhttpRequest({
url: src,
method: 'GET',
responseType: 'arraybuffer',
onprogress: (e) => {
if (e.total !== -1) {
fn(e.loaded / e.total);
}
else {
fn(-e.loaded);
}
},
onload: (e) => {
if (e.status === 200) {
resolve(e.response);
}
else {
console.warn(e);
resolve(null);
}
},
onerror: (e) => {
console.warn(e);
resolve(null);
}
});
});
if (imageArrayBuffer) {
/*
const upngObj = UPNG.decode(imageArrayBuffer);
console.log(upngObj);
if(upngObj.data) {
return {
raw: upngObj.data,
width: upngObj.width,
height: upngObj.height
};
}
else {
return null;
}
*/
return new Promise(async (resolve) => {
const url = await new Promise((resolve) => {
const fr = new FileReader();
fr.onload = e => resolve(fr.result);
fr.readAsDataURL(new Blob([new Uint8Array(imageArrayBuffer)], { type: 'image/png' }));
});
const img = new Image();
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
img.onload = function () {
const [width, height] = [this.width, this.height];
canvas.width = width;
canvas.height = height;
context.drawImage(this, 0, 0, width, height);
resolve({
raw: context.getImageData(0, 0, width, height).data.buffer,
width: width,
height: height
});
};
img.onerror = function () {
resolve(null);
};
img.src = url;
});
}
else {
return null;
}
}
function solarCurve(raw, filter, width, height) {
for (let row = 0; row < height; ++row) {
for (let col = 0; col < width; ++col) {
let ind = col * 4 + row * width * 4;
filter[ind] = Rc[raw[ind]];
filter[ind + 1] = Gc[raw[ind + 1]];
filter[ind + 2] = Bc[raw[ind + 2]];
filter[ind + 3] = raw[ind + 3];
}
}
}
async function rainbowImage(src, onprogress, worker = rainbowWorker) {
const img = await getImageBytesBuffer(src, onprogress);
if (img) {
onprogress(null);
if (worker) {
const [raw, width, height] = [img.raw, img.width, img.height];
const key = '' + Date.now();
worker.onmessage = (e) => {
const returnKey = e.data.key;
const resolve = worker.keyPool[returnKey];
if (resolve) {
const canvas = document.createElement('canvas');
const [width, height] = [e.data.width, e.data.height];
[canvas.width, canvas.height] = [width, height];
const context = canvas.getContext('2d');
context.putImageData(new ImageData(
new Uint8ClampedArray(e.data.filter),
width,
height
), 0, 0);
canvas.toBlob((blob) => {
resolve(URL.createObjectURL(blob));
delete worker.keyPool[returnKey];
}, 'image/png', 1);
}
};
worker.postMessage({
img: raw,
width: width,
height: height,
key: key
}, [raw]);
return new Promise((res) => {
worker.keyPool[key] = res;
});
} else {
const [raw, width, height] = [new Uint8ClampedArray(img.raw), img.width, img.height];
const canvas = document.createElement('canvas');
[canvas.width, canvas.height] = [width, height];
const context = canvas.getContext('2d');
const rainbow = context.createImageData(width, height);
solarCurve(raw, rainbow.data, width, height);
context.putImageData(rainbow, 0, 0);
return new Promise((resolve) => {
canvas.toBlob((blob) => {
resolve(URL.createObjectURL(blob));
}, 'image/png', 1);
});
}
}
}
// Diff images (async or sync)
// input: src1, src2, on progress function, pixelmatch initilization object, web worker
// ouput: diffsrc (dataURL)
async function diffImage(src1, src2, onprogress, init = { alpha: 0.5, threshold: 0.007 }, worker = diffWorker) {
const [img1, img2] = await Promise.all([
getImageBytesBuffer(src1, (p) => onprogress(p, 0)),
getImageBytesBuffer(src2, (p) => onprogress(p, 1))
]);
if (
img1 && img2 &&
img1.width === img2.width &&
img1.height === img2.height
) {
onprogress(null, null);
if (worker) {// async diff
const [raw1, raw2, width, height] = [img1.raw, img2.raw, img1.width, img1.height];
const key = '' + Date.now();
worker.onmessage = (e) => {
const returnKey = e.data.key;
const resolve = worker.keyPool[returnKey];
if (resolve) {
const canvas = document.createElement('canvas');
const [width, height] = [e.data.width, e.data.height];
[canvas.width, canvas.height] = [width, height];
const context = canvas.getContext('2d');
context.putImageData(new ImageData(
new Uint8ClampedArray(e.data.diff),
width,
height
), 0, 0);
canvas.toBlob((blob) => {
resolve(URL.createObjectURL(blob));
delete worker.keyPool[returnKey];
}, 'image/png', 1);
}
};
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 [raw1, raw2, width, height] = [
new Uint8ClampedArray(img1.raw),
new Uint8ClampedArray(img2.raw),
img1.width,
img1.height
];
const canvas = document.createElement('canvas');
[canvas.width, canvas.height] = [width, height];
const context = canvas.getContext('2d');
const diff = context.createImageData(width, height);
pixelmatch(raw1, raw2, diff.data, width, height, init);
context.putImageData(diff, 0, 0);
return new Promise((resolve) => {
canvas.toBlob((blob) => {
resolve(URL.createObjectURL(blob));
}, 'image/png', 1);
});
}
}
else {
return null;
}
}
// Virtual DOM for selection without fetching images
function $$(htmlString) {
return $(htmlString, document.implementation.createHTMLDocument('virtual'));
}
// Convert text to SVG image
function text2SVGDataURL(text, width, height = 20) {
return `data:image/svg+xml,${
encodeURIComponent(
`<svg xmlns='http://www.w3.org/2000/svg' height="${height}" width="${width}"><text x="0" y="15" fill="white">${text}</text></svg>`
)}`;
}
// Function to make an <img/> element
function makeImage(src, outlineColor = 'red') {
const $figure = $('<figure/>').css({
'width': 'fit-content',
'position': 'fixed',
'top': '50%',
'left': '50%',
'margin': '0',
'vertical-align': 'middle'
});
const $image = $(`<img src="${src}"/>`).css({
'display': 'none',
'transform': 'translate(-50%, -50%)',
'opacity': '1',
'outline': '3px solid ' + outlineColor,
'outline-offset': '2px',
});
$figure.append($image);
return $image;
}
// 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 = $overlay.find('img:visible').hide()[0];
if (((original && (target = original.targetImage)) || target) &&
target.easyCompare && target.easyCompare.boxShadow !== undefined) {
$(target).css('box-shadow', target.easyCompare.boxShadow);
}
}
// Filter function mapping
const filterImage = {
'rainbow': rainbowImage
};
// 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 * 100}%`;
}
return originalImage;
} else {
const originalImage = makeImage(text2SVGDataURL(`Loading...`, 80))[0];
originalImage.ready = false;
originalImage.targetImage = target;
$overlay.append(originalImage.parentElement);
if (!target.easyCompare) {
target.easyCompare = {};
}
target.easyCompare.originalImage = originalImage;
target.easyCompare.originalImagePromise = new Promise((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) {
originalImage.src = realSrc;
originalImage.style.width = `${scale * 100}%`;
originalImage.ready = true;
resolve(originalImage);
}
}
// 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$/)) {
originalImage.src = href;
originalImage.style.width = `${scale * 100}%`;
originalImage.ready = true;
resolve(originalImage);
} else {
guessOriginalImage(href).then(src => {
originalImage.src = src || realSrc;
originalImage.style.width = `${scale * 100}%`;
originalImage.ready = true;
resolve(originalImage);
});
}
} else {
originalImage.src = realSrc;
originalImage.style.width = `${scale * 100}%`;
originalImage.ready = true;
resolve(originalImage);
}
});
return originalImage;
}
}
// 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 diffedImage = target.easyCompare[base.src];
if (diffedImage.ready) {
diffedImage.style.width = `${scale * 100}%`;
}
return diffedImage;
} else {
const diffedImage = makeImage(text2SVGDataURL(`Loading...`, 80))[0];
diffedImage.ready = false;
diffedImage.baseImage = base;
diffedImage.threshold = -1;
diffedImage.step = 0.001;
$overlay.append(diffedImage.parentElement);
if (!target.easyCompare) {
target.easyCompare = {};
}
target.easyCompare[base.src] = diffedImage;
if (!base.easyCompare) {
base.easyCompare = {};
}
base.easyCompare[target.src] = diffedImage;
let progress = [0, 0];
// Progress update function
const updateProgress = (p, ind) => {
if (p !== null && p >= 0 && ind !== null) {
progress[ind] = p;
diffedImage.src = text2SVGDataURL(`Loading ${((progress[0] + progress[1]) * 50).toFixed(1)}%`, 120);
}
else if (p < 0) {
diffedImage.src = text2SVGDataURL(`Loading...`, 80);
}
else {
diffedImage.src = text2SVGDataURL(`Diffing...`, 80);
}
};
getOriginalImage(target, $overlay);
getOriginalImage(base, $overlay);
Promise.all([
target.easyCompare.originalImagePromise,
base.easyCompare.originalImagePromise
]).then(([{ src: src1 }, { src: src2 }]) => diffImage(src1, src2, updateProgress, {
alpha: 0.5,
threshold: 0.007
})).then((diffedSrc) => {
if (diffedSrc === null) {
diffedImage.src = text2SVGDataURL(`Sizes Not Match`, 120);
} else {
diffedImage.onload = () => {
diffedImage.style.width = `${scale * 100}%`;
diffedImage.ready = true;
};
diffedImage.src = diffedSrc;
diffedImage.threshold = 0.007;
}
}).catch((err) => {
console.warn(err);
diffedImage.src = text2SVGDataURL(`Sth. Went Wrong`, 120);
});
return diffedImage;
}
}
// Get filtered image function
function getFilteredImage(target, ftType, $overlay) {
if (target.easyCompare && target.easyCompare[ftType]) {
const filteredImage = target.easyCompare[ftType];
if (filteredImage.ready) {
filteredImage.style.width = `${scale * 100}%`;
}
return filteredImage;
} else {
const filteredImage = makeImage(text2SVGDataURL(`Loading...`, 80))[0];
filteredImage.ready = false;
filteredImage.targetImage = target;
$overlay.append(filteredImage.parentElement);
if (!target.easyCompare) {
target.easyCompare = {};
}
target.easyCompare[ftType] = filteredImage;
// Progress Update Function
const updateProgress = (p) => {
if (p !== null && p >= 0) {
filteredImage.src = text2SVGDataURL(`Loading ${(p * 100).toFixed(1)}%`, 120);
} else if (p < 0) {
filteredImage.src = text2SVGDataURL(`Loading...`, 80);
} else {
filteredImage.src = text2SVGDataURL(`Filtering...`, 80);
}
};
// Wait original image and filter the original image
getOriginalImage(target, $overlay);
target.easyCompare.originalImagePromise.then(originalImage => {
filterImage[ftType](originalImage.src, updateProgress).then(filterdSrc => {
filteredImage.onload = () => {
filteredImage.style.width = `${scale * 100}%`;
filteredImage.ready = true;
};
filteredImage.src = filterdSrc;
});
});
return filteredImage;
}
}
// Function fired when compare button is clicked and toggled on
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';
// 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);
});
// Scroll event
$(document).on('scroll.compare', (e) => {
const temp = $overlay.find('img:visible')[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':
try {
const index = $images.index($overlay.find('img:visible')[0].targetImage);
baseImage = $images[index];
} catch (err) {
baseImage = undefined;
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
break;
case '+': case '=':
if (e.ctrlKey) {
try {
if (scale <= 0.9) {
scale = scale + 0.1;
} else {
scale = 1;
}
const target = $overlay.find('img:visible')[0];
if (target.ready) {
target.style.width = `${scale * 100}%`;
}
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
}
break;
case '-': case '_':
if (e.ctrlKey) {
try {
if (scale >= 0.2) {
scale = scale - 0.1;
} else {
scale = 0.1;
}
const target = $overlay.find('img:visible')[0];
if (target.ready) {
target.style.width = `${scale * 100}%`;
}
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
}
break;
case 'O': case 'o':
if (e.ctrlKey) {
try {
if (scale !== 1) {
scale = 1;
const target = $overlay.find('img:visible')[0];
if (target.ready) {
target.style.width = `${scale * 100}%`;
}
}
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
}
break;
case 'S': case 's':
if (e.ctrlKey) {
try {
const target = $overlay.find('img:visible')[0];
GM_download({
url: target.src,
name: 'easycompare.png',
onerror: (e) => {
if (e.error === 'Invalid scheme') {
const a = document.createElement('a');
a.href = target.src;
a.download = 'easycompare.png';
a.click();
}
}
});
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
} else {
ftType = (ftType === 'rainbow' ? 'none' : 'rainbow');
try {
const target = $overlay.find('img:visible').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);
}
}
}
break;
case 'I': case 'i': case 'ArrowUp':
try {
const target = $overlay.find('img:visible')[0];
let threshold = target.threshold;
if (threshold !== undefined && threshold >= 0) {
const thresholdPrev = threshold;
$message.text(`Threshold: ${thresholdPrev.toFixed(4)}`).css('opacity', '1');
threshold += target.step;
if (threshold > 1) {
threshold = 1;
}
target.threshold = -1;
diffImage(target.baseImage.easyCompare.originalImage.src,
target.targetImage.easyCompare.originalImage.src,
(a, b) => { },
{ alpha: 0.5, threshold: threshold })
.then((diffSrc) => {
let temp;
if (diffSrc === null) {
target.src = text2SVGDataURL(`Sizes Not Match`, 120);
temp = thresholdPrev;
setTimeout(() => { target.threshold = thresholdPrev; }, 300);
} else {
target.src = diffSrc;
temp = threshold;
setTimeout(() => { target.threshold = threshold; }, 300);
}
$message.text(`Threshold: ${temp.toFixed(4)}`).css('opacity', '1');
setTimeout(() => $message.css('opacity', '0'), 300);
});
}
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
break;
case 'K': case 'k': case 'ArrowDown':
try {
const target = $overlay.find('img:visible')[0];
let threshold = target.threshold;
if (threshold !== undefined && threshold >= 0) {
const thresholdPrev = threshold;
$message.text(`Threshold: ${thresholdPrev.toFixed(4)}`).css('opacity', '1');
threshold -= target.step;
if (threshold < 0) {
threshold = 0;
}
target.threshold = -1;
diffImage(target.baseImage.easyCompare.originalImage.src,
target.targetImage.easyCompare.originalImage.src,
(a, b) => { },
{ alpha: 0.5, threshold: threshold })
.then((diffSrc) => {
let temp;
if (diffSrc === null) {
target.src = text2SVGDataURL(`Sizes Not Match`, 120);
temp = thresholdPrev;
setTimeout(() => { target.threshold = thresholdPrev; }, 300);
} else {
target.src = diffSrc;
temp = threshold;
setTimeout(() => { target.threshold = threshold; }, 300);
}
$message.text(`Threshold: ${temp.toFixed(4)}`).css('opacity', '1');
setTimeout(() => $message.css('opacity', '0'), 300);
});
}
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
break;
case 'J': case 'j': case 'ArrowLeft':
try {
const target = $overlay.find('img:visible')[0];
switch (target.step) {
case 0.0001:
target.step = 0.001;
break;
case 0.001:
target.step = 0.01;
break;
case 0.01:
target.step = 0.1;
break;
case 0.1:
target.step = 1.0;
break;
default:
break;
}
if (target.step) {
$message.text(`Step: ${target.step.toFixed(4)}`).css('opacity', '1');
setTimeout(() => $message.css('opacity', '0'), 300);
}
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
break;
case 'L': case 'l': case 'ArrowRight':
if (e.ctrlKey) {
try {
leaveImage($overlay, $overlay.find('img:visible')[0].targetImage);
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
$overlay.find('img').toArray().forEach(e => {
const target = e.targetImage;
delete target.easyCompare;
URL.revokeObjectURL(e.src);
e.parentElement.remove();
});
} else {
try {
const target = $overlay.find('img:visible')[0];
switch (target.step) {
case 1.0:
target.step = 0.1;
break;
case 0.1:
target.step = 0.01;
break;
case 0.01:
target.step = 0.001;
break;
case 0.001:
target.step = 0.0001;
break;
default:
break;
}
if (target.step) {
$message.text(`Step: ${target.step.toFixed(4)}`).css('opacity', '1');
setTimeout(() => $message.css('opacity', '0'), 300);
}
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
}
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 'E': case 'e':
try {
const targetImage = $overlay.find('img:visible')[0].targetImage;
const index = $images.index(targetImage);
leaveImage($overlay, targetImage);
const nextElem = $images[index + step] || $images[index];
$(nextElem).trigger('mouseenter', [e.shiftKey]);
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
break;
case 'W': case 'w':
try {
const targetImage = $overlay.find('img:visible')[0].targetImage;
const index = $images.index(targetImage);
leaveImage($overlay, targetImage);
const nextElem = $images[index - step] || $images[index];
$(nextElem).trigger('mouseenter', [e.shiftKey]);
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
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');
}
// An overlay on the whole page
const $overlay = $('<div/>').css({
'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'
});
// 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'
});
$overlay.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': '15px',
'right': '15px',
'z-index': 2147483647,
'paint-order': 'stroke',
'opacity': 0,
'transition': 'all 0.2s',
'cursor': 'auto'
}).on('mouseenter', (e) => {
$(e.currentTarget).attr({
'fill': 'gray'
}).css({
'opacity': 0.2
});
timeout = setTimeout(() => activateCompare($(e.currentTarget)), $overlay[0].state ? 0 : 1000);
}).on('mouseleave', (e) => {
clearTimeout(timeout);
$(e.currentTarget).attr({
'fill': 'gray'
}).css({
'cursor': 'auto',
'opacity': 0
})[0].state = false;
}).click((e) => {
if (e.currentTarget.state) {
switch ($overlay[0].state) {
case false:
enterCompare($overlay, $('img:visible:not(.easy-compare-image)'), $message);
break;
case true:
exitCompare($overlay, $('img:visible:not(.easy-compare-image)'));
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'
});
}
});
$overlay[0].state = false;
$compareButton[0].state = false;
$('body').append($compareButton).append($overlay);
})(window.$.noConflict(true),
unsafeWindow.Mousetrap,
window.pixelmatch,
window.UPNG,
unsafeWindow.URL.createObjectURL ?
unsafeWindow.URL :
unsafeWindow.webkitURL);