Greasy Fork is available in English.

Commit: Interactive image diff using Pixelmatch

Adds an image diff control in place of a regular image comparison renderer on GitHub

La data de 07-10-2020. Vezi ultima versiune.

  1. // ==UserScript==
  2. // @name Commit: Interactive image diff using Pixelmatch
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.4
  5. // @description Adds an image diff control in place of a regular image comparison renderer on GitHub
  6. // @author You
  7. // @match https://render.githubusercontent.com/diff/img?*
  8. // @match https://github.com/*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. if (window.location.hostname === 'github.com') {
  16. if (window) {
  17. window.addEventListener('keyup', (ev) => {
  18. const iframes = Array.from(document.querySelectorAll('iframe'));
  19. iframes.forEach(x => {
  20. if (x.hasAttribute('sandbox')) {
  21. x.removeAttribute('sandbox')
  22. }
  23. x.contentWindow.postMessage({name: 'keyup-image-diff', key: ev.key}, '*');
  24. });
  25. });
  26. }
  27.  
  28. console.log("Commit: Interactive image diff");
  29. let styleApplied = false;
  30. const styleNode = document.createElement("style");
  31. styleNode.innerHTML = `
  32. /*body.hasIframe .container-lg.new-discussion-timeline { max-width: initial; }*/
  33. .render-container[data-type='img'] { height: 700px !important; }
  34. `;
  35. document.head.appendChild(styleNode);
  36.  
  37. document.body.addEventListener('click', (ev) => {
  38. let target = ev.target.closest('button');
  39. if (!target || styleApplied) {
  40. return;
  41. }
  42. if (target.getAttribute('aria-label') == 'Display the rich diff') { // are we on milestones page
  43. styleApplied = true;
  44. 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
  45.  
  46. // click on all rich previews
  47. const buttons = document.querySelectorAll("[aria-label='Display the rich diff']");
  48. Array.from(buttons).forEach(button => {
  49. const mockedEvent = new MouseEvent('click', {
  50. bubbles: true,
  51. cancelable: true
  52. });
  53. button.dispatchEvent(mockedEvent);
  54. });
  55. }
  56. });
  57.  
  58.  
  59. return;
  60. }
  61.  
  62. window.addEventListener('message', (ev) => {
  63. const input = document.querySelector('#thisPluginsInput');
  64. if(ev.data.name === 'keyup-image-diff') {
  65. switch (ev.data.key) {
  66. case '1':
  67. input.setAttribute('value', '1');
  68. break;
  69. case '2':
  70.  
  71. input.setAttribute('value', '2');
  72. break;
  73. case '3':
  74.  
  75. input.setAttribute('value', '3');
  76. break;
  77. }
  78. var event = new Event('input', {
  79. bubbles: true,
  80. cancelable: true,
  81. });
  82.  
  83. input.dispatchEvent(event);
  84. }
  85. });
  86.  
  87. //pixelmatch
  88. !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) });
  89.  
  90.  
  91.  
  92. function setMode(input, span) {
  93. const pictures = document.querySelectorAll('.warpech-slider > *');
  94.  
  95. switch (parseInt(input.value, 10)) {
  96. case 3:
  97. span.innerHTML = 'Diff';
  98. pictures[0].style.display = 'none';
  99. pictures[1].style.display = 'none';
  100. pictures[2].style.display = 'block';
  101. break;
  102.  
  103. case 2:
  104. span.innerHTML = 'Changed file';
  105. pictures[0].style.display = 'none';
  106. pictures[1].style.display = 'block';
  107. pictures[2].style.display = 'none';
  108. break;
  109.  
  110. case 1:
  111. span.innerHTML = 'Original file';
  112. pictures[0].style.display = 'block';
  113. pictures[1].style.display = 'none';
  114. pictures[2].style.display = 'none';
  115. break;
  116. }
  117. }
  118.  
  119. async function compareImages() {
  120. const imgsMeta = document.querySelector("div[data-type='diff']");
  121. const imgs = [
  122. imgsMeta.getAttribute('data-file1'),
  123. imgsMeta.getAttribute('data-file2')
  124. ];
  125. console.log("imgs", imgs);
  126.  
  127. const label = document.createElement('label');
  128. label.classList.add('warpech-sliderControl');
  129. const input = document.createElement('input');
  130. input.id = 'thisPluginsInput';
  131. input.setAttribute('type', 'range');
  132. input.setAttribute('min', '1');
  133. input.setAttribute('max', '3');
  134. input.setAttribute('value', '3');
  135. input.addEventListener('input', (ev) => {
  136. setMode(input, span);
  137. });
  138. const span = document.createElement('span');
  139. label.appendChild(input);
  140. label.appendChild(span);
  141. document.body.appendChild(label);
  142.  
  143. const sliderElem = document.createElement('div');
  144. sliderElem.classList.add('warpech-slider');
  145. document.body.appendChild(sliderElem);
  146.  
  147. if (!imgs[0]) {
  148. throw new Error("Too early! The image does not have the src attribute");
  149. }
  150.  
  151. const img1clone = document.createElement('img');
  152. img1clone.setAttribute('src',imgs[0]);
  153. sliderElem.appendChild(img1clone);
  154.  
  155. const img2clone = document.createElement('img');
  156. img2clone.setAttribute('src',imgs[1]);
  157. sliderElem.appendChild(img2clone);
  158.  
  159. const img1 = await fetchImage(imgs[0]);
  160. const img2 = await fetchImage(imgs[1]);
  161.  
  162. const { width: w, height: h } = img1;
  163.  
  164. const ctx = context2d(w, h, 1);
  165. ctx.drawImage(img1, 0, 0);
  166. const data1 = ctx.getImageData(0, 0, w, h).data;
  167. ctx.drawImage(img2, 0, 0);
  168. const data2 = ctx.getImageData(0, 0, w, h).data;
  169. const diff = ctx.createImageData(w, h);
  170.  
  171. pixelmatch(data1, data2, diff.data, w, h, {});
  172. ctx.putImageData(diff, 0, 0);
  173.  
  174. sliderElem.appendChild(ctx.canvas)
  175.  
  176. setMode(input, span);
  177. }
  178. function fetchImage(src) {
  179. return new Promise((resolve, reject) => {
  180. const image = new Image;
  181. image.crossOrigin = "anonymous";
  182. image.src = src;
  183. image.onload = () => resolve(image);
  184. image.onerror = reject;
  185. });
  186. }
  187.  
  188. function context2d(width, height, dpi) {
  189. if (dpi == null) dpi = devicePixelRatio;
  190. var canvas = document.createElement("canvas");
  191. canvas.width = width * dpi;
  192. canvas.height = height * dpi;
  193. // canvas.style.width = width + "px";
  194. var context = canvas.getContext("2d");
  195. context.scale(dpi, dpi);
  196. return context;
  197. }
  198. const styleNode = document.createElement("style");
  199. styleNode.innerHTML = `
  200. .render-shell {
  201. visibility: hidden;
  202. }
  203.  
  204. .warpech-sliderControl {
  205. display: flex;
  206. align-items: center;
  207. position: absolute;
  208. right: 0;
  209. z-index: 9;
  210. background: #eaf5ff;
  211. padding: 5px;
  212. border-radius: 0 0 0 5px;
  213. border-left: 1px solid rgba(27,31,35,.15);
  214. border-bottom: 1px solid rgba(27,31,35,.15);
  215. }
  216.  
  217. .warpech-sliderControl input {
  218. width: 100px;
  219. margin-right: 10px;
  220. }
  221.  
  222. .warpech-sliderControl span {
  223. width: 100px;
  224. overflow: hidden;
  225. }
  226.  
  227. .warpech-slider {
  228. position: relative;
  229. /*overflow: scroll;*/
  230. }
  231.  
  232. .warpech-slider img,
  233. .warpech-slider canvas {
  234. position: absolute;
  235. width: initial;
  236. max-height: 700px;
  237. }`;
  238. document.head.appendChild(styleNode);
  239.  
  240. setTimeout(() => {
  241. compareImages();
  242. }, 500);
  243.  
  244. })();