tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

reftest-analyzer-structured.xhtml (29510B)


      1 <?xml version="1.0" encoding="UTF-8"?>
      2 <!-- This Source Code Form is subject to the terms of the Mozilla Public
      3   - License, v. 2.0. If a copy of the MPL was not distributed with this
      4   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
      5 <!--
      6 
      7 Features to add:
      8 * make the left and right parts of the viewer independently scrollable
      9 * make the test list filterable
     10 ** default to only showing unexpecteds
     11 * add other ways to highlight differences other than circling?
     12 * add zoom/pan to images
     13 * Add ability to load log via XMLHttpRequest (also triggered via URL param)
     14 * color the test list based on pass/fail and expected/unexpected/random/skip
     15 * ability to load multiple logs ?
     16 ** rename them by clicking on the name and editing
     17 ** turn the test list into a collapsing tree view
     18 ** move log loading into popup from viewer UI
     19 
     20 -->
     21 <!DOCTYPE html>
     22 <html lang="en-US" xml:lang="en-US" xmlns="http://www.w3.org/1999/xhtml">
     23  <head>
     24    <title>Reftest analyzer</title>
     25    <style type="text/css">
     26      <![CDATA[
     27 
     28        html, body { margin: 0; }
     29        html { padding: 0; }
     30        body { padding: 4px; }
     31 
     32        #pixelarea, #itemlist, #images { position: absolute; }
     33        #itemlist, #images { overflow: auto; }
     34        #pixelarea { top: 0; left: 0; width: 320px; height: 84px; overflow: visible }
     35        #itemlist { top: 84px; left: 0; width: 320px; bottom: 0; }
     36        #images { top: 0; bottom: 0; left: 320px; right: 0; }
     37 
     38        #leftpane { width: 320px; }
     39        #images { position: fixed; top: 10px; left: 340px; }
     40 
     41        form#imgcontrols { margin: 0; display: block; }
     42 
     43        #itemlist > table { border-collapse: collapse; }
     44        #itemlist > table > tbody > tr > td { border: 1px solid; padding: 1px; }
     45        #itemlist td.activeitem { background-color: yellow; }
     46 
     47        /*
     48        #itemlist > table > tbody > tr.pass > td.url { background: lime; }
     49        #itemlist > table > tbody > tr.fail > td.url { background: red; }
     50        */
     51 
     52        #magnification > svg { display: block; width: 84px; height: 84px; }
     53 
     54        #pixelinfo { font: small sans-serif; position: absolute; width: 200px; left: 84px; }
     55        #pixelinfo table { border-collapse: collapse; }
     56        #pixelinfo table th { white-space: nowrap; text-align: left; padding: 0; }
     57        #pixelinfo table td { font-family: monospace; padding: 0 0 0 0.25em; }
     58 
     59        #pixelhint { display: inline; color: #88f; cursor: help; }
     60        #pixelhint > * { display: none; position: absolute; margin: 8px 0 0 8px; padding: 4px; width: 400px; background: #ffa; color: black; box-shadow: 3px 3px 2px #888; z-index: 1; }
     61        #pixelhint:hover { color: #000; }
     62        #pixelhint:hover > * { display: block; }
     63        #pixelhint p { margin: 0; }
     64        #pixelhint p + p { margin-top: 1em; }
     65 
     66        ]]>
     67    </style>
     68    <script type="text/javascript">
     69      <![CDATA[
     70 
     71      var XLINK_NS = "http://www.w3.org/1999/xlink";
     72      var SVG_NS = "http://www.w3.org/2000/svg";
     73      var IMAGE_NOT_AVAILABLE = "data:image/png;base64,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";
     74 
     75      var gPhases = null;
     76 
     77      var gIDCache = {};
     78 
     79      var gMagPixPaths = [];     // 2D array of array-of-two <path> objects used in the pixel magnifier
     80      var gMagWidth = 5;         // number of zoomed in pixels to show horizontally
     81      var gMagHeight = 5;        // number of zoomed in pixels to show vertically
     82      var gMagZoom = 16;         // size of the zoomed in pixels
     83      var gImage1Data;           // ImageData object for the reference image
     84      var gImage2Data;           // ImageData object for the test output image
     85      var gFlashingPixels = [];  // array of <path> objects that should be flashed due to pixel color mismatch
     86      var gParams;
     87 
     88      function ID(id) {
     89        if (!(id in gIDCache))
     90          gIDCache[id] = document.getElementById(id);
     91        return gIDCache[id];
     92      }
     93 
     94      function hash_parameters() {
     95        var result = { };
     96        var params = window.location.hash.substr(1).split(/[&;]/);
     97        for (var i = 0; i < params.length; i++) {
     98          var parts = params[i].split("=");
     99          result[parts[0]] = unescape(unescape(parts[1]));
    100        }
    101        return result;
    102      }
    103 
    104      function load() {
    105        gPhases = [ ID("entry"), ID("loading"), ID("viewer") ];
    106        build_mag();
    107        gParams = hash_parameters();
    108        if (gParams.log) {
    109          show_phase("loading");
    110          process_log(gParams.log);
    111        } else if (gParams.logurl) {
    112          show_phase("loading");
    113          var req = new XMLHttpRequest();
    114          req.onreadystatechange = function() {
    115            if (req.readyState === 4) {
    116              process_log(req.responseText);
    117            }
    118          };
    119          req.open('GET', gParams.logurl, true);
    120          req.send();
    121        }
    122        window.addEventListener('keypress', handle_keyboard_shortcut);
    123        ID("image1").addEventListener('error', image_load_error);
    124        ID("image2").addEventListener('error', image_load_error);
    125      }
    126 
    127      function image_load_error(e) {
    128        e.target.setAttributeNS(XLINK_NS, "xlink:href", IMAGE_NOT_AVAILABLE);
    129      }
    130 
    131      function build_mag() {
    132        var mag = ID("mag");
    133 
    134        var r = document.createElementNS(SVG_NS, "rect");
    135        r.setAttribute("x", gMagZoom * -gMagWidth / 2);
    136        r.setAttribute("y", gMagZoom * -gMagHeight / 2);
    137        r.setAttribute("width", gMagZoom * gMagWidth);
    138        r.setAttribute("height", gMagZoom * gMagHeight);
    139        mag.appendChild(r);
    140 
    141        mag.setAttribute("transform", "translate(" + (gMagZoom * (gMagWidth / 2) + 1) + "," + (gMagZoom * (gMagHeight / 2) + 1) + ")");
    142 
    143        for (var x = 0; x < gMagWidth; x++) {
    144          gMagPixPaths[x] = [];
    145          for (var y = 0; y < gMagHeight; y++) {
    146            var p1 = document.createElementNS(SVG_NS, "path");
    147            p1.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "h" + -gMagZoom + "v" + gMagZoom);
    148            p1.setAttribute("stroke", "black");
    149            p1.setAttribute("stroke-width", "1px");
    150            p1.setAttribute("fill", "#aaa");
    151 
    152            var p2 = document.createElementNS(SVG_NS, "path");
    153            p2.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "v" + gMagZoom + "h" + -gMagZoom);
    154            p2.setAttribute("stroke", "black");
    155            p2.setAttribute("stroke-width", "1px");
    156            p2.setAttribute("fill", "#888");
    157 
    158            mag.appendChild(p1);
    159            mag.appendChild(p2);
    160            gMagPixPaths[x][y] = [p1, p2];
    161          }
    162        }
    163 
    164        var flashedOn = false;
    165        setInterval(function() {
    166          flashedOn = !flashedOn;
    167          flash_pixels(flashedOn);
    168        }, 500);
    169      }
    170 
    171      function show_phase(phaseid) {
    172        for (var i in gPhases) {
    173          var phase = gPhases[i];
    174          phase.style.display = (phase.id == phaseid) ? "" : "none";
    175        }
    176 
    177        if (phase == "viewer")
    178          ID("images").style.display = "none";
    179      }
    180 
    181      function fileentry_changed() {
    182        show_phase("loading");
    183        var input = ID("fileentry");
    184        var files = input.files;
    185        if (files.length) {
    186          // Only handle the first file; don't handle multiple selection.
    187          // The parts of the log we care about are ASCII-only.  Since we
    188          // can ignore lines we don't care about, best to read in as
    189          // iso-8859-1, which guarantees we don't get decoding errors.
    190          var fileReader = new FileReader();
    191          fileReader.onload = function(e) {
    192            var log = null;
    193 
    194            log = e.target.result;
    195 
    196            if (log)
    197              process_log(log);
    198            else
    199              show_phase("entry");
    200          }
    201          fileReader.readAsText(files[0], "iso-8859-1");
    202        }
    203        // So the user can process the same filename again (after
    204        // overwriting the log), clear the value on the form input so we
    205        // will always get an onchange event.
    206        input.value = "";
    207      }
    208 
    209      function log_pasted() {
    210        show_phase("loading");
    211        var entry = ID("logentry");
    212        var log = entry.value;
    213        entry.value = "";
    214        process_log(log);
    215      }
    216 
    217      var gTestItems;
    218 
    219      function process_log(contents) {
    220        var lines = contents.split(/[\r\n]+/);
    221        gTestItems = [];
    222        for (var j in lines) {
    223          var line = lines[j];
    224          try {
    225            var data = JSON.parse(line);
    226          } catch(e) {
    227            continue;
    228          }
    229          // Ignore duplicated output in logcat.
    230          if (!data.action == "test_end" && data.status != "FAIL")
    231            continue;
    232 
    233          if (!data.hasOwnProperty("extra") ||
    234              !data.extra.hasOwnProperty("reftest_screenshots")) {
    235            continue;
    236          }
    237 
    238          var url = data.test;
    239          var screenshots = data.extra.reftest_screenshots;
    240          gTestItems.push(
    241            {
    242              pass: data.status === "PASS",
    243              // only one of the following three should ever be true
    244              unexpected: data.hasOwnProperty("expected"),
    245              random: false,
    246              skip: data.status == "SKIP",
    247              url,
    248              images: [],
    249              imageLabels: []
    250            });
    251 
    252          var item = gTestItems[gTestItems.length - 1];
    253          item.images.push("data:image/png;base64," + screenshots[0].screenshot);
    254          item.imageLabels.push(screenshots[0].url);
    255          if (screenshots.length > 1) {
    256            item.images.push("data:image/png;base64," + screenshots[2].screenshot);
    257            item.imageLabels.push(screenshots[2].url);
    258          }
    259        }
    260        build_viewer();
    261      }
    262 
    263      function build_viewer() {
    264        if (!gTestItems.length) {
    265          show_phase("entry");
    266          return;
    267        }
    268 
    269        var cell = ID("itemlist");
    270        while (cell.childNodes.length)
    271          cell.removeChild(cell.childNodes[cell.childNodes.length - 1]);
    272 
    273        var table = document.createElement("table");
    274        var tbody = document.createElement("tbody");
    275        table.appendChild(tbody);
    276 
    277        for (var i in gTestItems) {
    278          var item = gTestItems[i];
    279 
    280          // optional url filter for only showing unexpected results
    281          if (parseInt(gParams.only_show_unexpected) && !item.unexpected)
    282            continue;
    283 
    284          // XXX regardless skip expected pass items until we have filtering UI
    285          if (item.pass && !item.unexpected)
    286            continue;
    287 
    288          var tr = document.createElement("tr");
    289          var td;
    290          var text;
    291 
    292          td = document.createElement("td");
    293          text = "";
    294          if (item.unexpected) { text += "!"; }
    295          if (item.random) { text += "R"; }
    296          if (item.skip) { text += "S"; }
    297          td.appendChild(document.createTextNode(text));
    298          tr.appendChild(td);
    299 
    300          td = document.createElement("td");
    301          td.id = "item" + i;
    302          td.className = "url";
    303          // Only display part of URL after "/mozilla/".
    304          var match = item.url.match(/\/mozilla\/(.*)/);
    305          text = document.createTextNode(match ? match[1] : item.url);
    306          if (item.images.length) {
    307            var a = document.createElement("a");
    308            a.href = "javascript:show_images(" + i + ")";
    309            a.appendChild(text);
    310            td.appendChild(a);
    311          } else {
    312            td.appendChild(text);
    313          }
    314          tr.appendChild(td);
    315 
    316          tbody.appendChild(tr);
    317        }
    318 
    319        cell.appendChild(table);
    320 
    321        show_phase("viewer");
    322      }
    323 
    324      function get_image_data(src, whenReady) {
    325        var img = new Image();
    326        img.onload = function() {
    327          var canvas = document.createElement("canvas");
    328          canvas.width = img.naturalWidth;
    329          canvas.height = img.naturalHeight;
    330 
    331          var ctx = canvas.getContext("2d");
    332          ctx.drawImage(img, 0, 0);
    333 
    334          whenReady(ctx.getImageData(0, 0, img.naturalWidth, img.naturalHeight));
    335        };
    336        img.src = src;
    337      }
    338 
    339      function sync_svg_size(imageData) {
    340        // We need the size of the 'svg' and its 'image' elements to match the size
    341        // of the ImageData objects that we're going to read pixels from or else our
    342        // magnify() function will be very broken.
    343        ID("svg").setAttribute("width", imageData.width);
    344        ID("svg").setAttribute("height", imageData.height);
    345      }
    346 
    347      function show_images(i) {
    348        var item = gTestItems[i];
    349        var cell = ID("images");
    350 
    351        // Remove activeitem class from any existing elements
    352        var activeItems = document.querySelectorAll(".activeitem");
    353        for (var activeItemIdx = activeItems.length; activeItemIdx-- != 0;) {
    354          activeItems[activeItemIdx].classList.remove("activeitem");
    355        }
    356 
    357        ID("item" + i).classList.add("activeitem");
    358        ID("image1").style.display = "";
    359        ID("image2").style.display = "none";
    360        ID("diffrect").style.display = "none";
    361        ID("imgcontrols").reset();
    362 
    363        ID("image1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
    364        // Making the href be #image1 doesn't seem to work
    365        ID("feimage1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
    366        if (item.images.length == 1) {
    367          ID("imgcontrols").style.display = "none";
    368        } else {
    369          ID("imgcontrols").style.display = "";
    370 
    371          ID("image2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
    372          // Making the href be #image2 doesn't seem to work
    373          ID("feimage2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
    374 
    375          ID("label1").textContent = 'Image ' + item.imageLabels[0];
    376          ID("label2").textContent = 'Image ' + item.imageLabels[1];
    377        }
    378 
    379        cell.style.display = "";
    380 
    381        get_image_data(item.images[0], function(data) { gImage1Data = data; sync_svg_size(gImage1Data); });
    382        get_image_data(item.images[1], function(data) { gImage2Data = data });
    383      }
    384 
    385      function show_image(i) {
    386        if (i == 1) {
    387          ID("image1").style.display = "";
    388          ID("image2").style.display = "none";
    389        } else {
    390          ID("image1").style.display = "none";
    391          ID("image2").style.display = "";
    392        }
    393      }
    394 
    395      function handle_keyboard_shortcut(event) {
    396        switch (event.charCode) {
    397        case 49: // "1" key
    398          document.getElementById("radio1").checked = true;
    399          show_image(1);
    400          break;
    401        case 50: // "2" key
    402          document.getElementById("radio2").checked = true;
    403          show_image(2);
    404          break;
    405        case 100: // "d" key
    406          document.getElementById("differences").click();
    407          break;
    408        case 112: // "p" key
    409          shift_images(-1);
    410          break;
    411        case 110: // "n" key
    412          shift_images(1);
    413          break;
    414        }
    415      }
    416 
    417      function shift_images(dir) {
    418        var activeItem = document.querySelector(".activeitem");
    419        if (!activeItem) {
    420          return;
    421        }
    422        for (var elm = activeItem; elm; elm = elm.parentElement) {
    423          if (elm.tagName != "tr") {
    424            continue;
    425          }
    426          elm = dir > 0 ? elm.nextElementSibling : elm.previousElementSibling;
    427          if (elm) {
    428            elm.getElementsByTagName("a")[0].click();
    429          }
    430          return;
    431        }
    432      }
    433 
    434      function show_differences(cb) {
    435        ID("diffrect").style.display = cb.checked ? "" : "none";
    436      }
    437 
    438      function flash_pixels(on) {
    439        var stroke = on ? "red" : "black";
    440        var strokeWidth = on ? "2px" : "1px";
    441        for (var i = 0; i < gFlashingPixels.length; i++) {
    442          gFlashingPixels[i].setAttribute("stroke", stroke);
    443          gFlashingPixels[i].setAttribute("stroke-width", strokeWidth);
    444        }
    445      }
    446 
    447      function cursor_point(evt) {
    448        var m = evt.target.getScreenCTM().inverse();
    449        var p = ID("svg").createSVGPoint();
    450        p.x = evt.clientX;
    451        p.y = evt.clientY;
    452        p = p.matrixTransform(m);
    453        return { x: Math.floor(p.x), y: Math.floor(p.y) };
    454      }
    455 
    456      function hex2(i) {
    457        return (i < 16 ? "0" : "") + i.toString(16);
    458      }
    459 
    460      function canvas_pixel_as_hex(data, x, y) {
    461        var offset = (y * data.width + x) * 4;
    462        var r = data.data[offset];
    463        var g = data.data[offset + 1];
    464        var b = data.data[offset + 2];
    465        return "#" + hex2(r) + hex2(g) + hex2(b);
    466      }
    467 
    468      function hex_as_rgb(hex) {
    469        return "rgb(" + [parseInt(hex.substring(1, 3), 16), parseInt(hex.substring(3, 5), 16), parseInt(hex.substring(5, 7), 16)] + ")";
    470      }
    471 
    472      function magnify(evt) {
    473        var { x: x, y: y } = cursor_point(evt);
    474        var centerPixelColor1, centerPixelColor2;
    475 
    476        var dx_lo = -Math.floor(gMagWidth / 2);
    477        var dx_hi = Math.floor(gMagWidth / 2);
    478        var dy_lo = -Math.floor(gMagHeight / 2);
    479        var dy_hi = Math.floor(gMagHeight / 2);
    480 
    481        flash_pixels(false);
    482        gFlashingPixels = [];
    483        for (var j = dy_lo; j <= dy_hi; j++) {
    484          for (var i = dx_lo; i <= dx_hi; i++) {
    485            var px = x + i;
    486            var py = y + j;
    487            var p1 = gMagPixPaths[i + dx_hi][j + dy_hi][0];
    488            var p2 = gMagPixPaths[i + dx_hi][j + dy_hi][1];
    489            // Here we just use the dimensions of gImage1Data since we expect test
    490            // and reference to have the same dimensions.
    491            if (px < 0 || py < 0 || px >= gImage1Data.width || py >= gImage1Data.height) {
    492              p1.setAttribute("fill", "#aaa");
    493              p2.setAttribute("fill", "#888");
    494            } else {
    495              var color1 = canvas_pixel_as_hex(gImage1Data, x + i, y + j);
    496              var color2 = canvas_pixel_as_hex(gImage2Data, x + i, y + j);
    497              p1.setAttribute("fill", color1);
    498              p2.setAttribute("fill", color2);
    499              if (color1 != color2) {
    500                gFlashingPixels.push(p1, p2);
    501                p1.parentNode.appendChild(p1);
    502                p2.parentNode.appendChild(p2);
    503              }
    504              if (i == 0 && j == 0) {
    505                centerPixelColor1 = color1;
    506                centerPixelColor2 = color2;
    507              }
    508            }
    509          }
    510        }
    511        flash_pixels(true);
    512        show_pixelinfo(x, y, centerPixelColor1, hex_as_rgb(centerPixelColor1), centerPixelColor2, hex_as_rgb(centerPixelColor2));
    513      }
    514 
    515      function show_pixelinfo(x, y, pix1rgb, pix1hex, pix2rgb, pix2hex) {
    516        ID("coords").textContent = [x, y];
    517        ID("pix1hex").textContent = pix1hex;
    518        ID("pix1rgb").textContent = pix1rgb;
    519        ID("pix2hex").textContent = pix2hex;
    520        ID("pix2rgb").textContent = pix2rgb;
    521      }
    522 
    523        ]]>
    524    </script>
    525  </head>
    526  <body onload="load()">
    527    <div id="entry">
    528      <h1>Reftest analyzer: load raw structured log</h1>
    529 
    530      <p>
    531        Either paste your log into this textarea:<br />
    532        <textarea cols="80" rows="10" id="logentry" /><br />
    533        <input
    534          type="button"
    535          value="Process pasted log"
    536          onclick="log_pasted()"
    537        />
    538      </p>
    539 
    540      <p>
    541        ... or load it from a file:<br />
    542        <input type="file" id="fileentry" onchange="fileentry_changed()" />
    543      </p>
    544    </div>
    545 
    546    <div id="loading" style="display: none">Loading log...</div>
    547 
    548    <div id="viewer" style="display: none">
    549      <div id="pixelarea">
    550        <div id="pixelinfo">
    551          <table>
    552            <tbody>
    553              <tr>
    554                <th>Pixel at:</th>
    555                <td colspan="2" id="coords" />
    556              </tr>
    557              <tr>
    558                <th>Image 1:</th>
    559                <td id="pix1rgb"></td>
    560                <td id="pix1hex"></td>
    561              </tr>
    562              <tr>
    563                <th>Image 2:</th>
    564                <td id="pix2rgb"></td>
    565                <td id="pix2hex"></td>
    566              </tr>
    567            </tbody>
    568          </table>
    569          <div>
    570            <div id="pixelhint">
    571     572              <div>
    573                <p>
    574                  Move the mouse over the reftest image on the right to show
    575                  magnified pixels on the left. The color information above is
    576                  for the pixel centered in the magnified view.
    577                </p>
    578                <p>
    579                  Image 1 is shown in the upper triangle of each pixel and Image
    580                  2 is shown in the lower triangle.
    581                </p>
    582              </div>
    583            </div>
    584          </div>
    585        </div>
    586        <div id="magnification">
    587          <svg
    588            xmlns="http://www.w3.org/2000/svg"
    589            width="84"
    590            height="84"
    591            shape-rendering="optimizeSpeed"
    592          >
    593            <g id="mag" />
    594          </svg>
    595        </div>
    596      </div>
    597      <div id="itemlist"></div>
    598      <div id="images" style="display: none">
    599        <form id="imgcontrols">
    600          <input
    601            id="radio1"
    602            type="radio"
    603            name="which"
    604            value="0"
    605            onchange="show_image(1)"
    606            checked="checked"
    607          /><label id="label1" title="1" for="radio1">Image 1</label>
    608          <input
    609            id="radio2"
    610            type="radio"
    611            name="which"
    612            value="1"
    613            onchange="show_image(2)"
    614          /><label id="label2" title="2" for="radio2">Image 2</label>
    615          <label
    616            ><input
    617              id="differences"
    618              type="checkbox"
    619              onchange="show_differences(this)"
    620            />Circle differences</label
    621          >
    622        </form>
    623        <svg
    624          xmlns="http://www.w3.org/2000/svg"
    625          xmlns:xlink="http://www.w3.org/1999/xlink"
    626          version="1.1"
    627          width="800"
    628          height="1000"
    629          id="svg"
    630        >
    631          <defs>
    632            <!-- use sRGB to avoid loss of data -->
    633            <filter
    634              id="showDifferences"
    635              x="0%"
    636              y="0%"
    637              width="100%"
    638              height="100%"
    639              style="color-interpolation-filters: sRGB"
    640            >
    641              <feImage id="feimage1" result="img1" xlink:href="#image1" />
    642              <feImage id="feimage2" result="img2" xlink:href="#image2" />
    643              <!-- inv1 and inv2 are the images with RGB inverted -->
    644              <feComponentTransfer result="inv1" in="img1">
    645                <feFuncR type="linear" slope="-1" intercept="1" />
    646                <feFuncG type="linear" slope="-1" intercept="1" />
    647                <feFuncB type="linear" slope="-1" intercept="1" />
    648              </feComponentTransfer>
    649              <feComponentTransfer result="inv2" in="img2">
    650                <feFuncR type="linear" slope="-1" intercept="1" />
    651                <feFuncG type="linear" slope="-1" intercept="1" />
    652                <feFuncB type="linear" slope="-1" intercept="1" />
    653              </feComponentTransfer>
    654              <!-- w1 will have non-white pixels anywhere that img2
    655               is brighter than img1, and w2 for the reverse.
    656               It would be nice not to have to go through these
    657               intermediate states, but feComposite
    658               type="arithmetic" can't transform the RGB channels
    659               and leave the alpha channel untouched. -->
    660              <feComposite
    661                result="w1"
    662                in="img1"
    663                in2="inv2"
    664                operator="arithmetic"
    665                k2="1"
    666                k3="1"
    667              />
    668              <feComposite
    669                result="w2"
    670                in="img2"
    671                in2="inv1"
    672                operator="arithmetic"
    673                k2="1"
    674                k3="1"
    675              />
    676              <!-- c1 will have non-black pixels anywhere that img2
    677               is brighter than img1, and c2 for the reverse -->
    678              <feComponentTransfer result="c1" in="w1">
    679                <feFuncR type="linear" slope="-1" intercept="1" />
    680                <feFuncG type="linear" slope="-1" intercept="1" />
    681                <feFuncB type="linear" slope="-1" intercept="1" />
    682              </feComponentTransfer>
    683              <feComponentTransfer result="c2" in="w2">
    684                <feFuncR type="linear" slope="-1" intercept="1" />
    685                <feFuncG type="linear" slope="-1" intercept="1" />
    686                <feFuncB type="linear" slope="-1" intercept="1" />
    687              </feComponentTransfer>
    688              <!-- c will be nonblack (and fully on) for every pixel+component where there are differences -->
    689              <feComposite
    690                result="c"
    691                in="c1"
    692                in2="c2"
    693                operator="arithmetic"
    694                k2="255"
    695                k3="255"
    696              />
    697              <!-- a will be opaque for every pixel with differences and transparent for all others -->
    698              <feColorMatrix
    699                result="a"
    700                type="matrix"
    701                values="0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  1 1 1 0 0"
    702              />
    703 
    704              <!-- a, dilated by 1 pixel -->
    705              <feMorphology
    706                result="dila1"
    707                in="a"
    708                operator="dilate"
    709                radius="1"
    710              />
    711              <!-- a, dilated by 2 pixels -->
    712              <feMorphology
    713                result="dila2"
    714                in="dila1"
    715                operator="dilate"
    716                radius="1"
    717              />
    718 
    719              <!-- all the pixels in the 2-pixel dilation of a but not in the 1-pixel dilation, to highlight the diffs -->
    720              <feComposite
    721                result="highlight"
    722                in="dila2"
    723                in2="dila1"
    724                operator="out"
    725              />
    726 
    727              <feFlood result="red" flood-color="red" />
    728              <feComposite
    729                result="redhighlight"
    730                in="red"
    731                in2="highlight"
    732                operator="in"
    733              />
    734              <feFlood result="black" flood-color="black" flood-opacity="0.5" />
    735              <feMerge>
    736                <feMergeNode in="black" />
    737                <feMergeNode in="redhighlight" />
    738              </feMerge>
    739            </filter>
    740          </defs>
    741          <g onmousemove="magnify(evt)">
    742            <image x="0" y="0" width="100%" height="100%" id="image1" />
    743            <image x="0" y="0" width="100%" height="100%" id="image2" />
    744          </g>
    745          <rect
    746            id="diffrect"
    747            filter="url(#showDifferences)"
    748            pointer-events="none"
    749            x="0"
    750            y="0"
    751            width="100%"
    752            height="100%"
    753          />
    754        </svg>
    755      </div>
    756    </div>
    757  </body>
    758 </html>