- // ==UserScript==
- // @name Commit: Interactive image diff
- // @namespace http://tampermonkey.net/
- // @version 0.2
- // @description Adds an image diff control in place of a regular image comparison renderer on GitHub
- // @author You
- // @match https://render.githubusercontent.com/diff/img?*
- // @match https://github.com/*
- // @grant none
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- if (window.location.hostname === 'github.com') {
- console.log("Commit: Interactive image diff");
- let styleApplied = false;
- const styleNode = document.createElement("style");
- styleNode.innerHTML = `
- /*body.hasIframe .container-lg.new-discussion-timeline { max-width: initial; }*/
- .render-container[data-type='img'] { height: 700px !important; }
- `;
- document.head.appendChild(styleNode);
-
- document.body.addEventListener('click', (ev) => {
- let target = ev.target.closest('button');
- if (!target || styleApplied) {
- return;
- }
- if (target.getAttribute('aria-label') == 'Display the rich diff') { // are we on milestones page
- styleApplied = true;
- document.body.classList.add('full-width'); // this is a built-in class on GH, but it is only on in PR review, not in commits
-
- // click on all rich previews
- const buttons = document.querySelectorAll("[aria-label='Display the rich diff']");
- Array.from(buttons).forEach(button => {
- const mockedEvent = new MouseEvent('click', {
- bubbles: true,
- cancelable: true
- });
- button.dispatchEvent(mockedEvent);
- });
- }
- });
-
-
- return;
- }
-
- //pixelmatch
- !function (t) { if ("object" == typeof exports && "undefined" != typeof module) module.exports = t(); else if ("function" == typeof define && define.amd) define([], t); else { ("undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof self ? self : this).pixelmatch = t() } }(function () { return function () { return function t(e, n, r) { function o(i, u) { if (!n[i]) { if (!e[i]) { var a = "function" == typeof require && require; if (!u && a) return a(i, !0); if (f) return f(i, !0); var c = new Error("Cannot find module '" + i + "'"); throw c.code = "MODULE_NOT_FOUND", c } var l = n[i] = { exports: {} }; e[i][0].call(l.exports, function (t) { return o(e[i][1][t] || t) }, l, l.exports, t, e, n, r) } return n[i].exports } for (var f = "function" == typeof require && require, i = 0; i < r.length; i++)o(r[i]); return o } }()({ 1: [function (t, e, n) { "use strict"; e.exports = function (t, e, n, i, a, c) { if (!o(t) || !o(e) || n && !o(n)) throw new Error("Image data: Uint8Array, Uint8ClampedArray or Buffer expected."); if (t.length !== e.length || n && n.length !== t.length) throw new Error("Image sizes do not match."); if (t.length !== i * a * 4) throw new Error("Image data size does not match width/height."); c = Object.assign({}, r, c); const l = i * a, s = new Uint32Array(t.buffer, t.byteOffset, l), p = new Uint32Array(e.buffer, e.byteOffset, l); let m = !0; for (let t = 0; t < l; t++)if (s[t] !== p[t]) { m = !1; break } if (m) { if (n && !c.diffMask) for (let e = 0; e < l; e++)h(t, 4 * e, c.alpha, n); return 0 } const w = 35215 * c.threshold * c.threshold; let y = 0; const [M, g, x] = c.aaColor, [E, b, A] = c.diffColor; for (let r = 0; r < a; r++)for (let o = 0; o < i; o++) { const l = 4 * (r * i + o), s = u(t, e, l, l); s > w ? c.includeAA || !f(t, o, r, i, a, e) && !f(e, o, r, i, a, t) ? (n && d(n, l, E, b, A), y++) : n && !c.diffMask && d(n, l, M, g, x) : n && (c.diffMask || h(t, l, c.alpha, n)) } return y }; const r = { threshold: .1, includeAA: !1, alpha: .1, aaColor: [255, 255, 0], diffColor: [255, 0, 0], diffMask: !1 }; function o(t) { return ArrayBuffer.isView(t) && 1 === t.constructor.BYTES_PER_ELEMENT } function f(t, e, n, r, o, f) { const a = Math.max(e - 1, 0), c = Math.max(n - 1, 0), l = Math.min(e + 1, r - 1), s = Math.min(n + 1, o - 1), d = 4 * (n * r + e); let h, p, m, w, y = e === a || e === l || n === c || n === s ? 1 : 0, M = 0, g = 0; for (let o = a; o <= l; o++)for (let f = c; f <= s; f++) { if (o === e && f === n) continue; const i = u(t, t, d, 4 * (f * r + o), !0); if (0 === i) { if (++y > 2) return !1 } else i < M ? (M = i, h = o, p = f) : i > g && (g = i, m = o, w = f) } return 0 !== M && 0 !== g && (i(t, h, p, r, o) && i(f, h, p, r, o) || i(t, m, w, r, o) && i(f, m, w, r, o)) } function i(t, e, n, r, o) { const f = Math.max(e - 1, 0), i = Math.max(n - 1, 0), u = Math.min(e + 1, r - 1), a = Math.min(n + 1, o - 1), c = 4 * (n * r + e); let l = e === f || e === u || n === i || n === a ? 1 : 0; for (let o = f; o <= u; o++)for (let f = i; f <= a; f++) { if (o === e && f === n) continue; const i = 4 * (f * r + o); if (t[c] === t[i] && t[c + 1] === t[i + 1] && t[c + 2] === t[i + 2] && t[c + 3] === t[i + 3] && l++ , l > 2) return !0 } return !1 } function u(t, e, n, r, o) { let f = t[n + 0], i = t[n + 1], u = t[n + 2], d = t[n + 3], h = e[r + 0], p = e[r + 1], m = e[r + 2], w = e[r + 3]; if (d === w && f === h && i === p && u === m) return 0; d < 255 && (f = s(f, d /= 255), i = s(i, d), u = s(u, d)), w < 255 && (h = s(h, w /= 255), p = s(p, w), m = s(m, w)); const y = a(f, i, u) - a(h, p, m); if (o) return y; const M = c(f, i, u) - c(h, p, m), g = l(f, i, u) - l(h, p, m); return .5053 * y * y + .299 * M * M + .1957 * g * g } function a(t, e, n) { return .29889531 * t + .58662247 * e + .11448223 * n } function c(t, e, n) { return .59597799 * t - .2741761 * e - .32180189 * n } function l(t, e, n) { return .21147017 * t - .52261711 * e + .31114694 * n } function s(t, e) { return 255 + (t - 255) * e } function d(t, e, n, r, o) { t[e + 0] = n, t[e + 1] = r, t[e + 2] = o, t[e + 3] = 255 } function h(t, e, n, r) { const o = s(a(t[e + 0], t[e + 1], t[e + 2]), n * t[e + 3] / 255); d(r, e, o, o, o) } }, {}] }, {}, [1])(1) });
-
-
-
- function setMode(input, span) {
- const pictures = document.querySelectorAll('.warpech-slider > *');
-
- switch (parseInt(input.value, 10)) {
- case 3:
- span.innerHTML = 'Diff';
- pictures[0].style.display = 'none';
- pictures[1].style.display = 'none';
- pictures[2].style.display = 'block';
- break;
-
- case 2:
- span.innerHTML = 'Changed file';
- pictures[0].style.display = 'none';
- pictures[1].style.display = 'block';
- pictures[2].style.display = 'none';
- break;
-
- case 1:
- span.innerHTML = 'Original file';
- pictures[0].style.display = 'block';
- pictures[1].style.display = 'none';
- pictures[2].style.display = 'none';
- break;
- }
- }
-
- async function compareImages() {
- const imgsMeta = document.querySelector("div[data-type='diff']");
- const imgs = [
- imgsMeta.getAttribute('data-file1'),
- imgsMeta.getAttribute('data-file2')
- ];
- console.log("imgs", imgs);
-
- const label = document.createElement('label');
- label.classList.add('warpech-sliderControl');
- const input = document.createElement('input');
- input.setAttribute('type', 'range');
- input.setAttribute('min', '1');
- input.setAttribute('max', '3');
- input.setAttribute('value', '3');
- input.addEventListener('input', (ev) => {
- setMode(input, span);
- });
- const span = document.createElement('span');
- label.appendChild(input);
- label.appendChild(span);
- document.body.appendChild(label);
-
- const sliderElem = document.createElement('div');
- sliderElem.classList.add('warpech-slider');
- document.body.appendChild(sliderElem);
-
- if (!imgs[0]) {
- throw new Error("Too early! The image does not have the src attribute");
- }
-
- const img1clone = document.createElement('img');
- img1clone.setAttribute('src',imgs[0]);
- sliderElem.appendChild(img1clone);
-
- const img2clone = document.createElement('img');
- img2clone.setAttribute('src',imgs[1]);
- sliderElem.appendChild(img2clone);
-
- const img1 = await fetchImage(imgs[0]);
- const img2 = await fetchImage(imgs[1]);
-
- const { width: w, height: h } = img1;
-
- const ctx = context2d(w, h, 1);
- ctx.drawImage(img1, 0, 0);
- const data1 = ctx.getImageData(0, 0, w, h).data;
- ctx.drawImage(img2, 0, 0);
- const data2 = ctx.getImageData(0, 0, w, h).data;
- const diff = ctx.createImageData(w, h);
-
- pixelmatch(data1, data2, diff.data, w, h, {});
- ctx.putImageData(diff, 0, 0);
-
- sliderElem.appendChild(ctx.canvas)
-
- setMode(input, span);
- }
- function fetchImage(src) {
- return new Promise((resolve, reject) => {
- const image = new Image;
- image.crossOrigin = "anonymous";
- image.src = src;
- image.onload = () => resolve(image);
- image.onerror = reject;
- });
- }
-
- function context2d(width, height, dpi) {
- if (dpi == null) dpi = devicePixelRatio;
- var canvas = document.createElement("canvas");
- canvas.width = width * dpi;
- canvas.height = height * dpi;
- // canvas.style.width = width + "px";
- var context = canvas.getContext("2d");
- context.scale(dpi, dpi);
- return context;
- }
- const styleNode = document.createElement("style");
- styleNode.innerHTML = `
- .render-shell {
- visibility: hidden;
- }
-
- .warpech-sliderControl {
- display: flex;
- align-items: center;
- position: absolute;
- right: 0;
- z-index: 9;
- background: #eaf5ff;
- padding: 5px;
- border-radius: 0 0 0 5px;
- border-left: 1px solid rgba(27,31,35,.15);
- border-bottom: 1px solid rgba(27,31,35,.15);
- }
-
- .warpech-sliderControl input {
- width: 100px;
- margin-right: 10px;
- }
-
- .warpech-sliderControl span {
- width: 100px;
- overflow: hidden;
- }
-
- .warpech-slider {
- position: relative;
- /*overflow: scroll;*/
- }
-
- .warpech-slider img,
- .warpech-slider canvas {
- position: absolute;
- width: initial;
- max-height: 700px;
- }`;
- document.head.appendChild(styleNode);
-
- setTimeout(() => {
- compareImages();
- }, 500);
-
- })();