Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 <?xml version="1.0" encoding="UTF-8"?>
2 <!-- -*- Mode: HTML; tab-width: 2; indent-tabs-mode: nil; -*- -->
3 <!-- vim: set shiftwidth=2 tabstop=2 autoindent expandtab: -->
4 <!-- This Source Code Form is subject to the terms of the Mozilla Public
5 - License, v. 2.0. If a copy of the MPL was not distributed with this
6 - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
7 <!--
9 Features to add:
10 * make the left and right parts of the viewer independently scrollable
11 * make the test list filterable
12 ** default to only showing unexpecteds
13 * add other ways to highlight differences other than circling?
14 * add zoom/pan to images
15 * Add ability to load log via XMLHttpRequest (also triggered via URL param)
16 * color the test list based on pass/fail and expected/unexpected/random/skip
17 * ability to load multiple logs ?
18 ** rename them by clicking on the name and editing
19 ** turn the test list into a collapsing tree view
20 ** move log loading into popup from viewer UI
22 -->
23 <!DOCTYPE html>
24 <html lang="en-US" xml:lang="en-US" xmlns="http://www.w3.org/1999/xhtml">
25 <head>
26 <title>Reftest analyzer</title>
27 <style type="text/css"><![CDATA[
29 html, body { margin: 0; }
30 html { padding: 0; }
31 body { padding: 4px; }
33 #pixelarea, #itemlist, #images { position: absolute; }
34 #itemlist, #images { overflow: auto; }
35 #pixelarea { top: 0; left: 0; width: 320px; height: 84px; overflow: visible }
36 #itemlist { top: 84px; left: 0; width: 320px; bottom: 0; }
37 #images { top: 0; bottom: 0; left: 320px; right: 0; }
39 #leftpane { width: 320px; }
40 #images { position: fixed; top: 10px; left: 340px; }
42 form#imgcontrols { margin: 0; display: block; }
44 #itemlist > table { border-collapse: collapse; }
45 #itemlist > table > tbody > tr > td { border: 1px solid; padding: 1px; }
47 /*
48 #itemlist > table > tbody > tr.pass > td.url { background: lime; }
49 #itemlist > table > tbody > tr.fail > td.url { background: red; }
50 */
52 #magnification > svg { display: block; width: 84px; height: 84px; }
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; }
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; }
66 ]]></style>
67 <script type="text/javascript"><![CDATA[
69 var XLINK_NS = "http://www.w3.org/1999/xlink";
70 var SVG_NS = "http://www.w3.org/2000/svg";
71 var IMAGE_NOT_AVAILABLE = "data:image/png;base64,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";
73 var gPhases = null;
75 var gIDCache = {};
77 var gMagPixPaths = []; // 2D array of array-of-two <path> objects used in the pixel magnifier
78 var gMagWidth = 5; // number of zoomed in pixels to show horizontally
79 var gMagHeight = 5; // number of zoomed in pixels to show vertically
80 var gMagZoom = 16; // size of the zoomed in pixels
81 var gImage1Data; // ImageData object for the reference image
82 var gImage2Data; // ImageData object for the test output image
83 var gFlashingPixels = []; // array of <path> objects that should be flashed due to pixel color mismatch
85 function ID(id) {
86 if (!(id in gIDCache))
87 gIDCache[id] = document.getElementById(id);
88 return gIDCache[id];
89 }
91 function hash_parameters() {
92 var result = { };
93 var params = window.location.hash.substr(1).split(/[&;]/);
94 for (var i = 0; i < params.length; i++) {
95 var parts = params[i].split("=");
96 result[parts[0]] = unescape(unescape(parts[1]));
97 }
98 return result;
99 }
101 function load() {
102 gPhases = [ ID("entry"), ID("loading"), ID("viewer") ];
103 build_mag();
104 var params = hash_parameters();
105 if (params.log) {
106 ID("logentry").value = params.log;
107 log_pasted();
108 }
109 window.addEventListener('keypress', maybe_load_image, false);
110 ID("image1").addEventListener('error', image_load_error, false);
111 ID("image2").addEventListener('error', image_load_error, false);
112 }
114 function image_load_error(e) {
115 e.target.setAttributeNS(XLINK_NS, "xlink:href", IMAGE_NOT_AVAILABLE);
116 }
118 function build_mag() {
119 var mag = ID("mag");
121 var r = document.createElementNS(SVG_NS, "rect");
122 r.setAttribute("x", gMagZoom * -gMagWidth / 2);
123 r.setAttribute("y", gMagZoom * -gMagHeight / 2);
124 r.setAttribute("width", gMagZoom * gMagWidth);
125 r.setAttribute("height", gMagZoom * gMagHeight);
126 mag.appendChild(r);
128 mag.setAttribute("transform", "translate(" + (gMagZoom * (gMagWidth / 2) + 1) + "," + (gMagZoom * (gMagHeight / 2) + 1) + ")");
130 for (var x = 0; x < gMagWidth; x++) {
131 gMagPixPaths[x] = [];
132 for (var y = 0; y < gMagHeight; y++) {
133 var p1 = document.createElementNS(SVG_NS, "path");
134 p1.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "h" + -gMagZoom + "v" + gMagZoom);
135 p1.setAttribute("stroke", "black");
136 p1.setAttribute("stroke-width", "1px");
137 p1.setAttribute("fill", "#aaa");
139 var p2 = document.createElementNS(SVG_NS, "path");
140 p2.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "v" + gMagZoom + "h" + -gMagZoom);
141 p2.setAttribute("stroke", "black");
142 p2.setAttribute("stroke-width", "1px");
143 p2.setAttribute("fill", "#888");
145 mag.appendChild(p1);
146 mag.appendChild(p2);
147 gMagPixPaths[x][y] = [p1, p2];
148 }
149 }
151 var flashedOn = false;
152 setInterval(function() {
153 flashedOn = !flashedOn;
154 flash_pixels(flashedOn);
155 }, 500);
156 }
158 function show_phase(phaseid) {
159 for (var i in gPhases) {
160 var phase = gPhases[i];
161 phase.style.display = (phase.id == phaseid) ? "" : "none";
162 }
164 if (phase == "viewer")
165 ID("images").style.display = "none";
166 }
168 function fileentry_changed() {
169 show_phase("loading");
170 var input = ID("fileentry");
171 var files = input.files;
172 if (files.length > 0) {
173 // Only handle the first file; don't handle multiple selection.
174 // The parts of the log we care about are ASCII-only. Since we
175 // can ignore lines we don't care about, best to read in as
176 // iso-8859-1, which guarantees we don't get decoding errors.
177 var fileReader = new FileReader();
178 fileReader.onload = function(e) {
179 var log = null;
181 log = e.target.result;
183 if (log)
184 process_log(log);
185 else
186 show_phase("entry");
187 }
188 fileReader.readAsText(files[0], "iso-8859-1");
189 }
190 // So the user can process the same filename again (after
191 // overwriting the log), clear the value on the form input so we
192 // will always get an onchange event.
193 input.value = "";
194 }
196 function log_pasted() {
197 show_phase("loading");
198 var entry = ID("logentry");
199 var log = entry.value;
200 entry.value = "";
201 process_log(log);
202 }
204 var gTestItems;
206 function process_log(contents) {
207 var lines = contents.split(/[\r\n]+/);
208 gTestItems = [];
209 for (var j in lines) {
210 var line = lines[j];
211 var match = line.match(/^.*?REFTEST (.*)$/);
212 if (!match)
213 continue;
214 line = match[1];
215 match = line.match(/^(TEST-PASS|TEST-UNEXPECTED-PASS|TEST-KNOWN-FAIL|TEST-UNEXPECTED-FAIL|TEST-DEBUG-INFO)(\(EXPECTED RANDOM\)|) \| ([^\|]+) \|(.*)/);
216 if (match) {
217 var state = match[1];
218 var random = match[2];
219 var url = match[3];
220 var extra = match[4];
221 gTestItems.push(
222 {
223 pass: !state.match(/DEBUG-INFO$|FAIL$/),
224 // only one of the following three should ever be true
225 unexpected: !!state.match(/^TEST-UNEXPECTED/),
226 random: (random == "(EXPECTED RANDOM)"),
227 skip: (extra == " (SKIP)"),
228 url: url,
229 images: []
230 });
231 continue;
232 }
233 match = line.match(/^ IMAGE[^:]*: (.*)$/);
234 if (match) {
235 var item = gTestItems[gTestItems.length - 1];
236 item.images.push(match[1]);
237 }
238 }
240 build_viewer();
241 }
243 function build_viewer() {
244 if (gTestItems.length == 0) {
245 show_phase("entry");
246 return;
247 }
249 var cell = ID("itemlist");
250 while (cell.childNodes.length > 0)
251 cell.removeChild(cell.childNodes[cell.childNodes.length - 1]);
253 var table = document.createElement("table");
254 var tbody = document.createElement("tbody");
255 table.appendChild(tbody);
257 for (var i in gTestItems) {
258 var item = gTestItems[i];
260 // XXX skip expected pass items until we have filtering UI
261 if (item.pass && !item.unexpected)
262 continue;
264 var tr = document.createElement("tr");
265 var rowclass = item.pass ? "pass" : "fail";
266 var td;
267 var text;
269 td = document.createElement("td");
270 text = "";
271 if (item.unexpected) { text += "!"; rowclass += " unexpected"; }
272 if (item.random) { text += "R"; rowclass += " random"; }
273 if (item.skip) { text += "S"; rowclass += " skip"; }
274 td.appendChild(document.createTextNode(text));
275 tr.appendChild(td);
277 td = document.createElement("td");
278 td.className = "url";
279 // Only display part of URL after "/mozilla/".
280 var match = item.url.match(/\/mozilla\/(.*)/);
281 text = document.createTextNode(match ? match[1] : item.url);
282 if (item.images.length > 0) {
283 var a = document.createElement("a");
284 a.href = "javascript:show_images(" + i + ")";
285 a.appendChild(text);
286 td.appendChild(a);
287 } else {
288 td.appendChild(text);
289 }
290 tr.appendChild(td);
292 tbody.appendChild(tr);
293 }
295 cell.appendChild(table);
297 show_phase("viewer");
298 }
300 function get_image_data(src, whenReady) {
301 var img = new Image();
302 img.onload = function() {
303 var canvas = document.createElement("canvas");
304 canvas.width = 800;
305 canvas.height = 1000;
307 var ctx = canvas.getContext("2d");
308 ctx.drawImage(img, 0, 0);
310 whenReady(ctx.getImageData(0, 0, 800, 1000));
311 };
312 img.src = src;
313 }
315 function show_images(i) {
316 var item = gTestItems[i];
317 var cell = ID("images");
319 ID("image1").style.display = "";
320 ID("image2").style.display = "none";
321 ID("diffrect").style.display = "none";
322 ID("imgcontrols").reset();
324 ID("image1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
325 // Making the href be #image1 doesn't seem to work
326 ID("feimage1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
327 if (item.images.length == 1) {
328 ID("imgcontrols").style.display = "none";
329 } else {
330 ID("imgcontrols").style.display = "";
332 ID("image2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
333 // Making the href be #image2 doesn't seem to work
334 ID("feimage2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
335 }
337 cell.style.display = "";
339 get_image_data(item.images[0], function(data) { gImage1Data = data });
340 get_image_data(item.images[1], function(data) { gImage2Data = data });
341 }
343 function show_image(i) {
344 if (i == 1) {
345 ID("image1").style.display = "";
346 ID("image2").style.display = "none";
347 } else {
348 ID("image1").style.display = "none";
349 ID("image2").style.display = "";
350 }
351 }
353 function maybe_load_image(event) {
354 switch (event.charCode) {
355 case 49: // "1" key
356 document.getElementById("radio1").checked = true;
357 show_image(1);
358 break;
359 case 50: // "2" key
360 document.getElementById("radio2").checked = true;
361 show_image(2);
362 break;
363 }
364 }
366 function show_differences(cb) {
367 ID("diffrect").style.display = cb.checked ? "" : "none";
368 }
370 function flash_pixels(on) {
371 var stroke = on ? "red" : "black";
372 var strokeWidth = on ? "2px" : "1px";
373 for (var i = 0; i < gFlashingPixels.length; i++) {
374 gFlashingPixels[i].setAttribute("stroke", stroke);
375 gFlashingPixels[i].setAttribute("stroke-width", strokeWidth);
376 }
377 }
379 function cursor_point(evt) {
380 var m = evt.target.getScreenCTM().inverse();
381 var p = ID("svg").createSVGPoint();
382 p.x = evt.clientX;
383 p.y = evt.clientY;
384 p = p.matrixTransform(m);
385 return { x: Math.floor(p.x), y: Math.floor(p.y) };
386 }
388 function hex2(i) {
389 return (i < 16 ? "0" : "") + i.toString(16);
390 }
392 function canvas_pixel_as_hex(data, x, y) {
393 var offset = (y * data.width + x) * 4;
394 var r = data.data[offset];
395 var g = data.data[offset + 1];
396 var b = data.data[offset + 2];
397 return "#" + hex2(r) + hex2(g) + hex2(b);
398 }
400 function hex_as_rgb(hex) {
401 return "rgb(" + [parseInt(hex.substring(1, 3), 16), parseInt(hex.substring(3, 5), 16), parseInt(hex.substring(5, 7), 16)] + ")";
402 }
404 function magnify(evt) {
405 var { x: x, y: y } = cursor_point(evt);
406 var centerPixelColor1, centerPixelColor2;
408 var dx_lo = -Math.floor(gMagWidth / 2);
409 var dx_hi = Math.floor(gMagWidth / 2);
410 var dy_lo = -Math.floor(gMagHeight / 2);
411 var dy_hi = Math.floor(gMagHeight / 2);
413 flash_pixels(false);
414 gFlashingPixels = [];
415 for (var j = dy_lo; j <= dy_hi; j++) {
416 for (var i = dx_lo; i <= dx_hi; i++) {
417 var px = x + i;
418 var py = y + j;
419 var p1 = gMagPixPaths[i + dx_hi][j + dy_hi][0];
420 var p2 = gMagPixPaths[i + dx_hi][j + dy_hi][1];
421 if (px < 0 || py < 0 || px >= 800 || py >= 1000) {
422 p1.setAttribute("fill", "#aaa");
423 p2.setAttribute("fill", "#888");
424 } else {
425 var color1 = canvas_pixel_as_hex(gImage1Data, x + i, y + j);
426 var color2 = canvas_pixel_as_hex(gImage2Data, x + i, y + j);
427 p1.setAttribute("fill", color1);
428 p2.setAttribute("fill", color2);
429 if (color1 != color2) {
430 gFlashingPixels.push(p1, p2);
431 p1.parentNode.appendChild(p1);
432 p2.parentNode.appendChild(p2);
433 }
434 if (i == 0 && j == 0) {
435 centerPixelColor1 = color1;
436 centerPixelColor2 = color2;
437 }
438 }
439 }
440 }
441 flash_pixels(true);
442 show_pixelinfo(x, y, centerPixelColor1, hex_as_rgb(centerPixelColor1), centerPixelColor2, hex_as_rgb(centerPixelColor2));
443 }
445 function show_pixelinfo(x, y, pix1rgb, pix1hex, pix2rgb, pix2hex) {
446 var pixelinfo = ID("pixelinfo");
447 ID("coords").textContent = [x, y];
448 ID("pix1hex").textContent = pix1hex;
449 ID("pix1rgb").textContent = pix1rgb;
450 ID("pix2hex").textContent = pix2hex;
451 ID("pix2rgb").textContent = pix2rgb;
452 }
454 ]]></script>
456 </head>
457 <body onload="load()">
459 <div id="entry">
461 <h1>Reftest analyzer: load reftest log</h1>
463 <p>Either paste your log into this textarea:<br />
464 <textarea cols="80" rows="10" id="logentry"/><br/>
465 <input type="button" value="Process pasted log" onclick="log_pasted()" /></p>
467 <p>... or load it from a file:<br/>
468 <input type="file" id="fileentry" onchange="fileentry_changed()" />
469 </p>
470 </div>
472 <div id="loading" style="display:none">Loading log...</div>
474 <div id="viewer" style="display:none">
475 <div id="pixelarea">
476 <div id="pixelinfo">
477 <table>
478 <tbody>
479 <tr><th>Pixel at:</th><td colspan="2" id="coords"/></tr>
480 <tr><th>Image 1:</th><td id="pix1rgb"></td><td id="pix1hex"></td></tr>
481 <tr><th>Image 2:</th><td id="pix2rgb"></td><td id="pix2hex"></td></tr>
482 </tbody>
483 </table>
484 <div>
485 <div id="pixelhint">★
486 <div>
487 <p>Move the mouse over the reftest image on the right to show
488 magnified pixels on the left. The color information above is for
489 the pixel centered in the magnified view.</p>
490 <p>Image 1 is shown in the upper triangle of each pixel and Image 2
491 is shown in the lower triangle.</p>
492 </div>
493 </div>
494 </div>
495 </div>
496 <div id="magnification">
497 <svg xmlns="http://www.w3.org/2000/svg" width="84" height="84" shape-rendering="optimizeSpeed">
498 <g id="mag"/>
499 </svg>
500 </div>
501 </div>
502 <div id="itemlist"></div>
503 <div id="images" style="display:none">
504 <form id="imgcontrols">
505 <label title="1"><input id="radio1" type="radio" name="which" value="0" onchange="show_image(1)" checked="checked" />Image 1</label>
506 <label title="2"><input id="radio2" type="radio" name="which" value="1" onchange="show_image(2)" />Image 2</label>
507 <label><input type="checkbox" onchange="show_differences(this)" />Circle differences</label>
508 </form>
509 <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800px" height="1000px" viewBox="0 0 800 1000" id="svg">
510 <defs>
511 <!-- use sRGB to avoid loss of data -->
512 <filter id="showDifferences" x="0%" y="0%" width="100%" height="100%"
513 style="color-interpolation-filters: sRGB">
514 <feImage id="feimage1" result="img1" xlink:href="#image1" />
515 <feImage id="feimage2" result="img2" xlink:href="#image2" />
516 <!-- inv1 and inv2 are the images with RGB inverted -->
517 <feComponentTransfer result="inv1" in="img1">
518 <feFuncR type="linear" slope="-1" intercept="1" />
519 <feFuncG type="linear" slope="-1" intercept="1" />
520 <feFuncB type="linear" slope="-1" intercept="1" />
521 </feComponentTransfer>
522 <feComponentTransfer result="inv2" in="img2">
523 <feFuncR type="linear" slope="-1" intercept="1" />
524 <feFuncG type="linear" slope="-1" intercept="1" />
525 <feFuncB type="linear" slope="-1" intercept="1" />
526 </feComponentTransfer>
527 <!-- w1 will have non-white pixels anywhere that img2
528 is brighter than img1, and w2 for the reverse.
529 It would be nice not to have to go through these
530 intermediate states, but feComposite
531 type="arithmetic" can't transform the RGB channels
532 and leave the alpha channel untouched. -->
533 <feComposite result="w1" in="img1" in2="inv2" operator="arithmetic" k2="1" k3="1" />
534 <feComposite result="w2" in="img2" in2="inv1" operator="arithmetic" k2="1" k3="1" />
535 <!-- c1 will have non-black pixels anywhere that img2
536 is brighter than img1, and c2 for the reverse -->
537 <feComponentTransfer result="c1" in="w1">
538 <feFuncR type="linear" slope="-1" intercept="1" />
539 <feFuncG type="linear" slope="-1" intercept="1" />
540 <feFuncB type="linear" slope="-1" intercept="1" />
541 </feComponentTransfer>
542 <feComponentTransfer result="c2" in="w2">
543 <feFuncR type="linear" slope="-1" intercept="1" />
544 <feFuncG type="linear" slope="-1" intercept="1" />
545 <feFuncB type="linear" slope="-1" intercept="1" />
546 </feComponentTransfer>
547 <!-- c will be nonblack (and fully on) for every pixel+component where there are differences -->
548 <feComposite result="c" in="c1" in2="c2" operator="arithmetic" k2="255" k3="255" />
549 <!-- a will be opaque for every pixel with differences and transparent for all others -->
550 <feColorMatrix result="a" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0" />
552 <!-- a, dilated by 1 pixel -->
553 <feMorphology result="dila1" in="a" operator="dilate" radius="1" />
554 <!-- a, dilated by 2 pixels -->
555 <feMorphology result="dila2" in="dila1" operator="dilate" radius="1" />
557 <!-- all the pixels in the 2-pixel dilation of a but not in the 1-pixel dilation, to highlight the diffs -->
558 <feComposite result="highlight" in="dila2" in2="dila1" operator="out" />
560 <feFlood result="red" flood-color="red" />
561 <feComposite result="redhighlight" in="red" in2="highlight" operator="in" />
562 <feFlood result="black" flood-color="black" flood-opacity="0.5" />
563 <feMerge>
564 <feMergeNode in="black" />
565 <feMergeNode in="redhighlight" />
566 </feMerge>
567 </filter>
568 </defs>
569 <g onmousemove="magnify(evt)">
570 <image x="0" y="0" width="100%" height="100%" id="image1" />
571 <image x="0" y="0" width="100%" height="100%" id="image2" />
572 </g>
573 <rect id="diffrect" filter="url(#showDifferences)" pointer-events="none" x="0" y="0" width="100%" height="100%" />
574 </svg>
575 </div>
576 </div>
578 </body>
579 </html>