From b6f47539cd1a1dafe05ffd6fdc40bce4547c479d Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 18 Nov 2014 00:25:41 +0100 Subject: [PATCH] Add a blink comparator and pixel difference to image diffs Pixel difference uses CSS mix-blend-mode, which is supported currently only on Firefox >= 32 and on Safari >= 7.1. Implementation is behind a Javascript feature test. For other browsers, there's a blink comparator. Code changes: * ImageDiffHandler now takes the page it's used on as argument. We need that to get labels. DOM generated is a little bit different (new controls). * Diff pages adapted to new constructor of ImageDiffHandler. * CSS and Javascript changes implementing the new controls, making use of two new static image resources. Since I felt that the new controls deserved tooltips, I also gave the opacity slider a tooltip: changed to , and slider handle changed from
to . CSS ensures everything still displays the same (basically display:inline-block). * Supplied messages for English, French, and German for the new tooltips. Tested on IE8, Safari 6.1.6 & 7.1, Chrome 38, FF 33.1 & FF 3.6.13 --- .../gitblit/wicket/GitBlitWebApp.properties | 3 + .../wicket/GitBlitWebApp_de.properties | 3 + .../wicket/GitBlitWebApp_fr.properties | 3 + .../gitblit/wicket/pages/BlobDiffPage.java | 4 +- .../gitblit/wicket/pages/CommitDiffPage.java | 2 +- .../com/gitblit/wicket/pages/ComparePage.java | 2 +- .../wicket/pages/ImageDiffHandler.java | 32 +++++++-- .../gitblit/wicket/pages/scripts/imgdiff.js | 63 ++++++++++++++++-- src/main/resources/blink32.png | Bin 0 -> 1749 bytes src/main/resources/gitblit.css | 19 +++++- src/main/resources/sub32.png | Bin 0 -> 954 bytes 11 files changed, 115 insertions(+), 16 deletions(-) create mode 100644 src/main/resources/blink32.png create mode 100644 src/main/resources/sub32.png diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties index c1b5a30e..648ac2a5 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties @@ -757,3 +757,6 @@ gb.diffDeletedFile = File was deleted gb.diffRenamedFile = File was renamed from {0} gb.diffCopiedFile = File was copied from {0} gb.diffTruncated = Diff truncated after the above file +gb.opacityAdjust = Adjust opacity +gb.blinkComparator = Blink comparator +gb.imgdiffSubtract = Subtract (black = identical) \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_de.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_de.properties index be36ecd1..eca3fd2a 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_de.properties +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_de.properties @@ -750,3 +750,6 @@ gb.diffDeletedFile = Datei wurde gel\u00f6scht gb.diffRenamedFile = Datei umbenannt von {0} gb.diffCopiedFile = Datei kopiert von {0} gb.diffTruncated = Diff nach obiger Datei abgeschnitten +gb.opacityAdjust = Transparenz +gb.blinkComparator = Blinkkomparator +gb.imgdiffSubtract = Pixeldifferenz (schwarz = identisch) \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties index 1318b1d9..d479b3d6 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties @@ -679,3 +679,6 @@ gb.diffDeletedFile = Fichier a \u00e9t\u00e9 effac\u00e9 gb.diffRenamedFile = Fichier renomm\u00e9 de {0} gb.diffCopiedFile = Fichier copi\u00e9 de {0} gb.diffTruncated = Affichage de diff\u00e9rences supprim\u00e9e apr\u00e8s le fichier ci-dessus +gb.opacityAdjust = ajuster l'opacit\u00e9 +gb.blinkComparator = Comparateur \u00e0 clignotement +gb.imgdiffSubtract = Diff\u00e9rence (noir = identique) \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/pages/BlobDiffPage.java b/src/main/java/com/gitblit/wicket/pages/BlobDiffPage.java index 71516ec8..ae737a53 100644 --- a/src/main/java/com/gitblit/wicket/pages/BlobDiffPage.java +++ b/src/main/java/com/gitblit/wicket/pages/BlobDiffPage.java @@ -52,7 +52,7 @@ public class BlobDiffPage extends RepositoryPage { if (StringUtils.isEmpty(baseObjectId)) { // use first parent RevCommit parent = commit.getParentCount() == 0 ? null : commit.getParent(0); - ImageDiffHandler handler = new ImageDiffHandler(getContextUrl(), repositoryName, + ImageDiffHandler handler = new ImageDiffHandler(this, repositoryName, parent.getName(), commit.getName(), imageExtensions); diff = DiffUtils.getDiff(r, commit, blobPath, DiffOutputType.HTML, handler).content; if (handler.getImgDiffCount() > 0) { @@ -63,7 +63,7 @@ public class BlobDiffPage extends RepositoryPage { } else { // base commit specified RevCommit baseCommit = JGitUtils.getCommit(r, baseObjectId); - ImageDiffHandler handler = new ImageDiffHandler(getContextUrl(), repositoryName, + ImageDiffHandler handler = new ImageDiffHandler(this, repositoryName, baseCommit.getName(), commit.getName(), imageExtensions); diff = DiffUtils.getDiff(r, baseCommit, commit, blobPath, DiffOutputType.HTML, handler).content; if (handler.getImgDiffCount() > 0) { diff --git a/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java b/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java index e40af515..c838dab5 100644 --- a/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java +++ b/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java @@ -82,7 +82,7 @@ public class CommitDiffPage extends RepositoryPage { add(new CommitHeaderPanel("commitHeader", repositoryName, commit)); final List imageExtensions = app().settings().getStrings(Keys.web.imageExtensions); - final ImageDiffHandler handler = new ImageDiffHandler(getContextUrl(), repositoryName, + final ImageDiffHandler handler = new ImageDiffHandler(this, repositoryName, parents.isEmpty() ? null : parents.get(0), commit.getName(), imageExtensions); final DiffOutput diff = DiffUtils.getCommitDiff(r, commit, DiffOutputType.HTML, handler); if (handler.getImgDiffCount() > 0) { diff --git a/src/main/java/com/gitblit/wicket/pages/ComparePage.java b/src/main/java/com/gitblit/wicket/pages/ComparePage.java index c0141eba..62ae7c25 100644 --- a/src/main/java/com/gitblit/wicket/pages/ComparePage.java +++ b/src/main/java/com/gitblit/wicket/pages/ComparePage.java @@ -113,7 +113,7 @@ public class ComparePage extends RepositoryPage { toCommitId.setObject(endId); final List imageExtensions = app().settings().getStrings(Keys.web.imageExtensions); - final ImageDiffHandler handler = new ImageDiffHandler(getContextUrl(), repositoryName, + final ImageDiffHandler handler = new ImageDiffHandler(this, repositoryName, fromCommit.getName(), toCommit.getName(), imageExtensions); final DiffOutput diff = DiffUtils.getDiff(r, fromCommit, toCommit, DiffOutputType.HTML, handler); diff --git a/src/main/java/com/gitblit/wicket/pages/ImageDiffHandler.java b/src/main/java/com/gitblit/wicket/pages/ImageDiffHandler.java index 52bf13b9..dc0c5ae8 100644 --- a/src/main/java/com/gitblit/wicket/pages/ImageDiffHandler.java +++ b/src/main/java/com/gitblit/wicket/pages/ImageDiffHandler.java @@ -18,6 +18,7 @@ package com.gitblit.wicket.pages; import java.nio.charset.StandardCharsets; import java.util.List; +import org.apache.wicket.protocol.http.WebApplication; import org.apache.wicket.protocol.http.WicketURLEncoder; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry.Side; @@ -37,14 +38,14 @@ public class ImageDiffHandler implements DiffUtils.BinaryDiffHandler { private final String oldCommitId; private final String newCommitId; private final String repositoryName; - private final String baseUrl; + private final BasePage page; private final List imageExtensions; private int imgDiffCount = 0; - public ImageDiffHandler(final String baseUrl, final String repositoryName, final String oldCommitId, - final String newCommitId, final List imageExtensions) { - this.baseUrl = baseUrl; + public ImageDiffHandler(final BasePage page, final String repositoryName, final String oldCommitId, final String newCommitId, + final List imageExtensions) { + this.page = page; this.repositoryName = repositoryName; this.oldCommitId = oldCommitId; this.newCommitId = newCommitId; @@ -81,7 +82,19 @@ public class ImageDiffHandler implements DiffUtils.BinaryDiffHandler { old.appendElement("img").attr("class", "imgdiff-old").attr("id", id).attr("style", "max-width:640px;").attr("src", oldUrl); container.appendElement("img").attr("class", "imgdiff").attr("style", "max-width:640px;").attr("src", newUrl); wrapper.appendElement("br"); - wrapper.appendElement("div").attr("class", "imgdiff-opa-container").appendElement("div").attr("class", "imgdiff-opa-slider"); + Element controls = wrapper.appendElement("div"); + // Opacity slider + controls.appendElement("div").attr("class", "imgdiff-opa-container").appendElement("a").attr("class", "imgdiff-opa-slider") + .attr("href", "#").attr("title", page.getString("gb.opacityAdjust")); + // Blink comparator: find Pluto! + controls.appendElement("a").attr("class", "imgdiff-link imgdiff-blink").attr("href", "#") + .attr("title", page.getString("gb.blinkComparator")) + .appendElement("img").attr("src", getStaticResourceUrl("blink32.png")).attr("width", "20"); + // Pixel subtraction, initially not displayed, will be shown by imgdiff.js depending on feature test. + // (Uses CSS mix-blend-mode, which isn't supported on all browsers yet). + controls.appendElement("a").attr("class", "imgdiff-link imgdiff-subtract").attr("href", "#") + .attr("title", page.getString("gb.imgdiffSubtract")).attr("style", "display:none;") + .appendElement("img").attr("src", getStaticResourceUrl("sub32.png")).attr("width", "20"); return builder.toString(); } break; @@ -118,7 +131,7 @@ public class ImageDiffHandler implements DiffUtils.BinaryDiffHandler { if (ext.equalsIgnoreCase(extension)) { String commitId = Side.NEW.equals(side) ? newCommitId : oldCommitId; if (commitId != null) { - return RawServlet.asLink(baseUrl, urlencode(repositoryName), commitId, urlencode(path)); + return RawServlet.asLink(page.getContextUrl(), urlencode(repositoryName), commitId, urlencode(path)); } else { return null; } @@ -128,6 +141,13 @@ public class ImageDiffHandler implements DiffUtils.BinaryDiffHandler { return null; } + /** + * Returns a URL that will fetch the designated static resource from within GitBlit. + */ + protected String getStaticResourceUrl(String contextRelativePath) { + return WebApplication.get().getRequestCycleProcessor().getRequestCodingStrategy().rewriteStaticRelativeUrl(contextRelativePath); + } + /** * Encode a URL component of a {@link RawServlet} URL in the special way that the servlet expects it. Note that * the %-encoding used does not encode '&' or '<'. Slashes are not encoded in the result. diff --git a/src/main/java/com/gitblit/wicket/pages/scripts/imgdiff.js b/src/main/java/com/gitblit/wicket/pages/scripts/imgdiff.js index c98a05a4..e993997a 100644 --- a/src/main/java/com/gitblit/wicket/pages/scripts/imgdiff.js +++ b/src/main/java/com/gitblit/wicket/pages/scripts/imgdiff.js @@ -22,7 +22,7 @@ * * The styling of the slider is to be done in CSS. Currently recognized options: * - initial: clipped to [0..1], default 0 - * - handleClass: to assign to the handle div element created. + * - handleClass: to assign to the handle span element created. * If no handleClass is specified, a very plain default style is assigned. */ function rangeSlider(elem, options) { @@ -30,7 +30,7 @@ function rangeSlider(elem, options) { options.initial = Math.min(1.0, Math.max(0, options.initial)); var $elem = $(elem); - var $handle = $('
').css({ position: 'absolute', left: 0, cursor: 'ew-resize' }); + var $handle = $('').css({ position: 'absolute', left: 0, cursor: 'ew-resize' }); var $root = $(document.documentElement); var $doc = $(document); var lastRatio = options.initial; @@ -144,6 +144,7 @@ function setup() { var opacityAccess = rangeSlider($opacitySlider, {handleClass: 'imgdiff-opa-handle'}); var $img = $('#' + this.id.substr(this.id.indexOf('-')+1)); // Here we change opacity var $div = $img.parent(); // This controls visibility: here we change width. + var blinking = false; $overlaySlider.on('slider:pos', function(e, data) { var pos = $(data.handle).offset().left; @@ -167,11 +168,10 @@ function setup() { } }); $opacitySlider.on('slider:pos', function(e, data) { - if ($div.width() <= 0) overlayAccess.moveAuto(1.0); // Make old image visible in a nice way + if ($div.width() <= 0 && !blinking) overlayAccess.moveAuto(1.0); // Make old image visible in a nice way $img.css('opacity', 1.0 - data.ratio); }); - $opacitySlider.css('cursor', 'pointer'); - $opacitySlider.on('mousedown', function(e) { + $opacitySlider.on('click', function(e) { var newRatio = (e.pageX - $opacitySlider.offset().left) / $opacitySlider.innerWidth(); var oldRatio = opacityAccess.getRatio(); if (newRatio !== oldRatio) { @@ -184,6 +184,59 @@ function setup() { e.preventDefault(); }); + // Blinking before and after images is a good way for the human eye to catch differences. + var $blinker = $this.find('.imgdiff-blink'); + var initialOpacity = null; + $blinker.on('click', function(e) { + if (blinking) { + window.clearTimeout(blinking); + $blinker.children('img').first().css('border', '1px solid transparent'); + opacityAccess.setRatio(initialOpacity); + blinking = null; + } else { + $blinker.children('img').first().css('border', '1px solid #AAA'); + initialOpacity = opacityAccess.getRatio(); + var currentOpacity = 1.0; + function blink() { + opacityAccess.setRatio(currentOpacity); + currentOpacity = 1.0 - currentOpacity; + // Keep frequeny below 2Hz (i.e., delay above 500ms) + blinking = window.setTimeout(blink, 600); + } + if ($div.width() <= 0) { + overlayAccess.moveRatio(1.0, 500, blink); + } else { + blink(); + } + } + e.preventDefault(); + }); + + // Subtracting before and after images is another good way to detect differences. Result will be + // black where identical. + if (typeof $img[0].style.mixBlendMode != 'undefined') { + // Feature test: does the browser support the mix-blend-mode CSS property from the Compositing + // and Blending Level 1 spec (http://dev.w3.org/fxtf/compositing-1/#mix-blend-mode )? + // As of 2014-11, only Firefox >= 32 and Safari >= 7.1 support this. Other browsers will have to + // make do with the blink comparator only. + var $sub = $this.find('.imgdiff-subtract'); + $sub.css('display', 'inline-block'); + $sub.on('click', function (e) { + var curr = $img.css('mix-blend-mode'); + if (curr != 'difference') { + curr = 'difference'; + $sub.children('img').first().css('border', '1px solid #AAA'); + if ($div.width() <= 0) overlayAccess.moveRatio(1.0, 500); + opacityAccess.setRatio(0); + } else { + curr = 'normal'; + $sub.children('img').first().css('border', '1px solid transparent'); + + } + $img.css('mix-blend-mode', curr); + e.preventDefault(); + }); + } }); } diff --git a/src/main/resources/blink32.png b/src/main/resources/blink32.png new file mode 100644 index 0000000000000000000000000000000000000000..da593505a43d81198465f849480de2b38df26a39 GIT binary patch literal 1749 zcmV;`1}gc9P)hHZ$M)Fs9@?4LNm~|H zt9?frd2G$h@0sWK$IlQU1b(Eo+wD>mMbkovN*u>I0HBoi2_bu$rfrqW<<5`nN4^I@ zyWK7YL2z#{7~JuF|5g}=^|R*by1ro;#(FlJeVIz7R=x+!Hvu#njZ7GZzq+pb(D(hH z2q7SZ001bZV2l9(IOmAt7yuxIfDi(PVf<;D=5t-wpVewL?}q`jTCJZC4-X%?u6th^ zPIkjEU|AM~5O5p^!{JaFPj=HZ|CpGV_-$%xYV9mL?5vN?X7iVBxBG;1UZ<3T5CTf+ zF=9f%7{kQGgaQBnolZv?1%Nn?A%wtSFt{HC!JH7{akX0g{VxGDo6TQr+kP5FQH@fn z3`-yYz!-yJ7@(9w2!V7ut;R_=06-K);GEZO+kOfF;~+)=v|6p7yWQ@SD2i%~G34`k z5JKR29ysTUehEezNirmjd!C2g-CZ!oP_0%0pw{hnpAbU!lI~Cd8jVKg=;-K?q&=U{ zqf{zE*L8TF2ivyc`#uOE&@@efksvfpgAf8y6d?!#yn6KtySuyaeIIx4-o^Fn*TFfj zA08e)isSe{GBBwGa1!nJr2zmi3(^5XUh% z=a`+HMXgpFndiFh!*;t}QUHP=xF_>m1|H`ej^m7g$Ye6GEDM^Zfpe~U7={=Qhgexz z!N-pu6~KDEj>W}AGyr4*)VA`C+S z$JMXb>nVfzd>)#n!7vP5y?PbP%gacmQUIW;WYUK3`?uQdc8Nt%G#!RvJsDi%Abj5+ zsdk+6ky8mFP%IYl;K2hpj)QW!j7%nTuJMzguSZcd&4dsYIYHAj6bc0xhHMQe%|TzI@r++d~ipf9V2A>G&^L2!X}L zMPxFWZ@NG>N@<@FLiXe)Q536>f*T!*-&CN{+AuuyDgN20! z#Bq!`juAzX8q0S8AcX8OP1Ck?UEheJsGdZdJQ*PbmSrK6$$(M{+qMw|0fxgNfa8+A zv$KP@Z{MQV>!DOCVQFaz#bWW)Uh-Yn^$ktawph7b?l_LK?z(RMEaQwZ6pKZerU~GG z0oUzz@!`Vy8mStgfc2?2*<;xeWt*xCBDsr67W?up) zr8t#Jtr&*!r}SkU$Ivtll}ZJvt$rf zt5qy4EMR(iT4l)o{ysi`{yeS|pG(E8>VU56&rH+&LvG4B2XHK@dc7XLe*Fs1^HfBS zHwo&}rAxSR;|8u=xdP5Pve~Q>D^jHDy8cYn0XeAEYF?|=`Yi~8InH_AbzL+X4Nywq zc^<+r9Qp5$Ga!90gg~WI!P3$as?{pe=`^OMrldI8I8n&nDF6U4H8r&+gm`S*_R}B; zYWw^9r*xn^tqjs+ekTFtayi_*c~iMZ)3ipRP-m2HR@$II&N-$W00000NkvXXu0mjfLZ>4= literal 0 HcmV?d00001 diff --git a/src/main/resources/gitblit.css b/src/main/resources/gitblit.css index e0570ceb..a6cc516c 100644 --- a/src/main/resources/gitblit.css +++ b/src/main/resources/gitblit.css @@ -1490,10 +1490,12 @@ img.imgdiff-old { user-select: none; border: 1px solid #F00; } + .imgdiff-opa-container { + display: inline-block; width: 200px; height: 4px; - margin: 12px 35px; + margin: 12px 35px 6px 35px; padding: 0; position: relative; border: 1px solid #888; @@ -1532,6 +1534,7 @@ img.imgdiff-old { } .imgdiff-opa-handle { + display: inline-block; width: 10px; height: 10px; position: absolute; @@ -1549,6 +1552,7 @@ img.imgdiff-old { } .imgdiff-ovr-handle { + display: inline-block; width : 1px; height: 100%; top: 0px; @@ -1578,6 +1582,19 @@ img.imgdiff-old { /* With CSS: background-image: radial-gradient(5px at 50% 50%, #444, #888, transparent 5px); */ } +.imgdiff-link { + margin: 0px 4px; + text-decoration: none; + border: none; +} + +.imgdiff-link > img { + border: 1px solid transparent; /* Avoid jumping when we change the border */ + width: 20px; + height: 20px; + margin-bottom: 10px; +} + /* End image diffs */ td.changeType { diff --git a/src/main/resources/sub32.png b/src/main/resources/sub32.png new file mode 100644 index 0000000000000000000000000000000000000000..ebcfe13e83760822c12d22ae6d4e9894766232da GIT binary patch literal 954 zcmV;r14aCaP)T_$k;e0V^RPWeQc74x zqA2Qc&Ud7g8$yT{0071~K?pq(Lhiyad|JjHT7*CpMV+E34znyf$n$)!EX$rv&vBf- z=Xp22?_ayFd$kCenFI!d!CG0CCuy1<=Xt)15CVkIWa0FyMA?y2f|L?G&%0%eT{w<& z-tBhZmnJY8jrQX>K26i~NCE4^CJJ01jIqlg2+lS)H~%b5U^pCpdwzcYSyfd}0b-16 z&*u>U01!gd|CG}H`uh5ht*x!!a|sNG!*Ap9_!sBAi!lbBfF6EZ3iLGS1cVSk2pP27 z?e8Xu4+KV|(f)WmKCh~(hcN~lfkkV;Bp`$UN@>5{ZlCDsplUK04A$P>-cC($^Sfvb z=#fzAtE#FO$MLBU;;$-9tQJqJ{X@YiB29x30)&vcdb-UtO^?d5JW;q9V6xBCG(EOe zyBP%?+%|8Dm_gz=ilRg0L8*QD4)MAtQoPY5v+d>VzlD2h6ibG}oSWe;Nvlu`(SzzSy0 z^{r?-d~DlLN(pfsLs1k^mSvA~zC)#y8_HO#)q?HqZR-~MEV;kGhnJTZMSKIRD-{61 z_x&#gH!*||Ej&Z_zX-+{C+a85vM&eJF)+pnMF>3tObRJU5_ouc_|hUwlBCv02t86l z$erUjea?BWsw#MWeYKu$O-HLDV1k*0b6(E@CfySu=^GC8-UU|vly#x5PlIj^^(nsmF} z_aF$)D5d>*VYGoZ5iwh`APCO%HhtwyDFs86g4veWjK8`qSF+NFiISB*&2(AW