reftest-analyzer.xhtml (31672B)
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 <!-- 8 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 21 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[ 28 29 html, body { margin: 0; } 30 html { padding: 0; } 31 body { padding: 4px; } 32 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; } 38 39 #leftpane { width: 320px; } 40 #images { position: fixed; top: 10px; left: 340px; } 41 42 form#imgcontrols { margin: 0; display: block; } 43 44 #itemlist > table { border-collapse: collapse; } 45 #itemlist > table > tbody > tr > td { border: 1px solid; padding: 1px; } 46 #itemlist td.activeitem { background-color: yellow; } 47 48 /* 49 #itemlist > table > tbody > tr.pass > td.url { background: lime; } 50 #itemlist > table > tbody > tr.fail > td.url { background: red; } 51 */ 52 53 #magnification > svg { display: block; width: 84px; height: 84px; } 54 55 #pixelinfo { font: small sans-serif; position: absolute; width: 200px; left: 84px; } 56 #pixelinfo table { border-collapse: collapse; } 57 #pixelinfo table th { white-space: nowrap; text-align: left; padding: 0; } 58 #pixelinfo table td { font-family: monospace; padding: 0 0 0 0.25em; } 59 60 #pixelhint { display: inline; color: #88f; cursor: help; } 61 #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; } 62 #pixelhint:hover { color: #000; } 63 #pixelhint:hover > * { display: block; } 64 #pixelhint p { margin: 0; } 65 #pixelhint p + p { margin-top: 1em; } 66 67 ]]></style> 68 <script type="text/javascript"><![CDATA[ 69 70 let heatmapCanvas = null; 71 let heatmapUMouse; 72 let gl = null; 73 74 function heatmap_render_setup(canvas) { 75 gl = canvas.getContext('webgl', {antialias: false, depth: false, preserveDrawingBuffer:false}); 76 77 const vertices = [ 78 0, 0, 79 1, 0, 80 0, 1, 81 1, 1, 82 ]; 83 84 const vertexBuffer = gl.createBuffer(); 85 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); 86 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 87 88 const vsCode = 89 ` 90 attribute vec2 a_vertCoord; 91 varying vec2 v_texCoord; 92 void main(void) { 93 gl_Position = vec4(2.0 * a_vertCoord - 1.0, 0.0, 1.0); 94 v_texCoord = a_vertCoord; 95 }`; 96 97 const VS = gl.createShader(gl.VERTEX_SHADER); 98 gl.shaderSource(VS, vsCode); 99 gl.compileShader(VS); 100 101 const psCode = 102 ` 103 precision mediump float; 104 uniform vec2 heatmapUMouse; 105 varying vec2 v_texCoord; 106 uniform sampler2D u_image1, u_image2; 107 void main(void) { 108 vec2 dxy = abs(heatmapUMouse - gl_FragCoord.xy); 109 if(dxy.x < 1.0 || dxy.y < 1.0) { // crosshair 110 gl_FragColor = vec4( 1.0, 1.0, 0.5, 1.0 ); 111 return; 112 } 113 114 vec3 img1 = texture2D(u_image1, v_texCoord).rgb; 115 vec3 img2 = texture2D(u_image2, v_texCoord).rgb; 116 117 bool is_top = gl_FragCoord.y > float(heatmapUMouse.y); 118 bool is_left = gl_FragCoord.x < float(heatmapUMouse.x); 119 120 vec3 rgb; 121 if(is_top) { 122 if(is_left) { 123 rgb = img1; 124 } else { 125 rgb = img2; 126 } 127 } else { 128 vec3 diff = abs(img1 - img2); 129 if(is_left) { 130 rgb = diff; 131 } else { 132 float max_diff = max(diff.r, max(diff.g, diff.b)); 133 if(max_diff == 0.0) { 134 rgb = vec3(0.0, 0.0, 0.2); 135 } else { 136 // some arbitrary colorization -- transition from green to red 137 // with some contrast tweaks to make red stand out a bit more 138 // at about 0.5'ish 139 rgb = vec3( pow(max_diff, 0.5), pow(1.0 - max_diff, 3.0), 0.0 ); 140 } 141 } 142 } 143 144 gl_FragColor = vec4( rgb, 1.0 ); 145 }`; 146 147 const FS = gl.createShader(gl.FRAGMENT_SHADER); 148 gl.shaderSource(FS, psCode); 149 gl.compileShader(FS); 150 151 const program = gl.createProgram(); 152 gl.attachShader(program, VS); 153 gl.attachShader(program, FS); 154 gl.linkProgram(program); 155 if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 156 console.error('Link failed: ' + gl.getProgramInfoLog(program)); 157 console.error('vs info-log: ' + gl.getShaderInfoLog(VS)); 158 console.error('fs info-log: ' + gl.getShaderInfoLog(FS)); 159 return; // don't assign heatmapCanvas 160 } 161 gl.useProgram(program); 162 163 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); 164 165 const coord = gl.getAttribLocation(program, "a_vertCoord"); 166 gl.vertexAttribPointer(coord, 2, gl.FLOAT, false, 0, 0); 167 gl.enableVertexAttribArray(coord); 168 169 heatmapUMouse = gl.getUniformLocation(program, "heatmapUMouse"); 170 171 gl.uniform1i(gl.getUniformLocation(program, 'u_image1'), 0); 172 gl.uniform1i(gl.getUniformLocation(program, 'u_image2'), 1); 173 174 gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); 175 heatmapCanvas = canvas; 176 } 177 178 function heatmap_change_image(index, image) { 179 if (heatmapCanvas === null) { 180 return; 181 } 182 const texture = gl.createTexture(); 183 gl.activeTexture(gl.TEXTURE0 + index); 184 gl.bindTexture (gl.TEXTURE_2D, texture); 185 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 186 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 187 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 188 gl.texImage2D (gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); 189 } 190 191 function heatmap_render(mouse_x, mouse_y) { 192 if (heatmapCanvas === null) { 193 return; 194 } 195 196 gl.uniform2f(heatmapUMouse, mouse_x, mouse_y); 197 198 // the canvas resizes as user selects different reftests 199 gl.viewport(0, 0, heatmapCanvas.width, heatmapCanvas.height); 200 201 gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 202 } 203 204 function heatmap_on_mousemove(mousemove_event) { 205 if (heatmapCanvas === null) { 206 return; 207 } 208 const rect = heatmapCanvas.getBoundingClientRect(); 209 let x = mousemove_event.clientX - rect.left; 210 let y = mousemove_event.clientY - rect.top; 211 x = x * heatmapCanvas.width / heatmapCanvas.clientWidth; 212 y = y * heatmapCanvas.height / heatmapCanvas.clientHeight; 213 214 // mouse has Y == 0 at the top, GL has it at the bottom: 215 const flip_y = heatmapCanvas.height-1 - y; 216 heatmap_render(x, flip_y); 217 218 return { x:x, y:y }; 219 } 220 221 ]]></script> 222 223 <script type="text/javascript"><![CDATA[ 224 225 var XLINK_NS = "http://www.w3.org/1999/xlink"; 226 var SVG_NS = "http://www.w3.org/2000/svg"; 227 var IMAGE_NOT_AVAILABLE = "data:image/png;base64,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"; 228 229 var gPhases = null; 230 231 var gIDCache = {}; 232 233 var gMagPixPaths = []; // 2D array of array-of-two <path> objects used in the pixel magnifier 234 var gMagWidth = 5; // number of zoomed in pixels to show horizontally 235 var gMagHeight = 5; // number of zoomed in pixels to show vertically 236 var gMagZoom = 16; // size of the zoomed in pixels 237 var gImage1Data; // ImageData object for the reference image 238 var gImage2Data; // ImageData object for the test output image 239 var gFlashingPixels = []; // array of <path> objects that should be flashed due to pixel color mismatch 240 var gParams; 241 242 function ID(id) { 243 if (!(id in gIDCache)) 244 gIDCache[id] = document.getElementById(id); 245 return gIDCache[id]; 246 } 247 248 function hash_parameters() { 249 var result = { }; 250 var params = window.location.hash.substr(1).split(/[&;]/); 251 for (var i = 0; i < params.length; i++) { 252 var parts = params[i].split("="); 253 result[parts[0]] = unescape(unescape(parts[1])); 254 } 255 return result; 256 } 257 258 function load() { 259 gPhases = [ ID("entry"), ID("loading"), ID("viewer") ]; 260 build_mag(); 261 gParams = hash_parameters(); 262 if (gParams.log) { 263 show_phase("loading"); 264 process_log(gParams.log); 265 } else if (gParams.logurl) { 266 show_phase("loading"); 267 var req = new XMLHttpRequest(); 268 req.onreadystatechange = function() { 269 if (req.readyState === 4) { 270 process_log(req.responseText); 271 } 272 }; 273 req.open('GET', gParams.logurl, true); 274 req.send(); 275 } 276 window.addEventListener('keypress', handle_keyboard_shortcut); 277 ID("image1").addEventListener('error', image_load_error); 278 ID("image2").addEventListener('error', image_load_error); 279 } 280 281 function image_load_error(e) { 282 e.target.setAttributeNS(XLINK_NS, "xlink:href", IMAGE_NOT_AVAILABLE); 283 } 284 285 function build_mag() { 286 var mag = ID("mag"); 287 288 var r = document.createElementNS(SVG_NS, "rect"); 289 r.setAttribute("x", gMagZoom * -gMagWidth / 2); 290 r.setAttribute("y", gMagZoom * -gMagHeight / 2); 291 r.setAttribute("width", gMagZoom * gMagWidth); 292 r.setAttribute("height", gMagZoom * gMagHeight); 293 mag.appendChild(r); 294 295 mag.setAttribute("transform", "translate(" + (gMagZoom * (gMagWidth / 2) + 1) + "," + (gMagZoom * (gMagHeight / 2) + 1) + ")"); 296 297 for (var x = 0; x < gMagWidth; x++) { 298 gMagPixPaths[x] = []; 299 for (var y = 0; y < gMagHeight; y++) { 300 var p1 = document.createElementNS(SVG_NS, "path"); 301 p1.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "h" + -gMagZoom + "v" + gMagZoom); 302 p1.setAttribute("stroke", "black"); 303 p1.setAttribute("stroke-width", "1px"); 304 p1.setAttribute("fill", "#aaa"); 305 306 var p2 = document.createElementNS(SVG_NS, "path"); 307 p2.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "v" + gMagZoom + "h" + -gMagZoom); 308 p2.setAttribute("stroke", "black"); 309 p2.setAttribute("stroke-width", "1px"); 310 p2.setAttribute("fill", "#888"); 311 312 mag.appendChild(p1); 313 mag.appendChild(p2); 314 gMagPixPaths[x][y] = [p1, p2]; 315 } 316 } 317 318 var flashedOn = false; 319 setInterval(function() { 320 flashedOn = !flashedOn; 321 flash_pixels(flashedOn); 322 }, 500); 323 } 324 325 function show_phase(phaseid) { 326 for (var i in gPhases) { 327 var phase = gPhases[i]; 328 phase.style.display = (phase.id == phaseid) ? "" : "none"; 329 } 330 331 if (phase == "viewer") 332 ID("images").style.display = "none"; 333 } 334 335 function fileentry_changed() { 336 show_phase("loading"); 337 var input = ID("fileentry"); 338 var files = input.files; 339 if (files.length > 0) { 340 // Only handle the first file; don't handle multiple selection. 341 // The parts of the log we care about are ASCII-only. Since we 342 // can ignore lines we don't care about, best to read in as 343 // iso-8859-1, which guarantees we don't get decoding errors. 344 var fileReader = new FileReader(); 345 fileReader.onload = function(e) { 346 var log = null; 347 348 log = e.target.result; 349 350 if (log) 351 process_log(log); 352 else 353 show_phase("entry"); 354 } 355 fileReader.readAsText(files[0], "iso-8859-1"); 356 } 357 // So the user can process the same filename again (after 358 // overwriting the log), clear the value on the form input so we 359 // will always get an onchange event. 360 input.value = ""; 361 } 362 363 function log_pasted() { 364 show_phase("loading"); 365 var entry = ID("logentry"); 366 var log = entry.value; 367 entry.value = ""; 368 process_log(log); 369 } 370 371 var gTestItems; 372 373 function process_log(contents) { 374 var lines = contents.split(/[\r\n]+/); 375 gTestItems = []; 376 for (var j in lines) { 377 var line = lines[j]; 378 // Ignore duplicated output in logcat. 379 if (line.match(/I\/Gecko.*?REFTEST/)) 380 continue; 381 var match = line.match(/^(?:.*? (?:INFO|ERROR) -\s+)?(?:REFTEST\s+)?(.*)$/); 382 if (!match) 383 continue; 384 line = match[1]; 385 match = line.match(/^(TEST-PASS|TEST-UNEXPECTED-PASS|TEST-KNOWN-FAIL|TEST-UNEXPECTED-FAIL|TEST-DEBUG-INFO)(\(EXPECTED RANDOM\)|) \| ([^\|]+) \|(.*)/); 386 if (match) { 387 var state = match[1]; 388 var random = match[2]; 389 var url = match[3]; 390 var extra = match[4]; 391 gTestItems.push( 392 { 393 pass: !state.match(/DEBUG-INFO$|FAIL$/), 394 // only one of the following three should ever be true 395 unexpected: !!state.match(/^TEST-UNEXPECTED/), 396 random: (random == "(EXPECTED RANDOM)"), 397 skip: (extra == " (SKIP)"), 398 url: url, 399 images: [], 400 imageLabels: [] 401 }); 402 continue; 403 } 404 match = line.match(/IMAGE([^:]*): (data:.*)$/); 405 if (match) { 406 var item = gTestItems[gTestItems.length - 1]; 407 item.images.push(match[2]); 408 item.imageLabels.push(match[1]); 409 } 410 } 411 412 build_viewer(); 413 } 414 415 function build_viewer() { 416 if (gTestItems.length == 0) { 417 show_phase("entry"); 418 return; 419 } 420 421 var cell = ID("itemlist"); 422 while (cell.childNodes.length > 0) 423 cell.removeChild(cell.childNodes[cell.childNodes.length - 1]); 424 425 var table = document.createElement("table"); 426 var tbody = document.createElement("tbody"); 427 table.appendChild(tbody); 428 429 for (var i in gTestItems) { 430 var item = gTestItems[i]; 431 432 // optional url filter for only showing unexpected results 433 if (parseInt(gParams.only_show_unexpected) && !item.unexpected) 434 continue; 435 436 // XXX regardless skip expected pass items until we have filtering UI 437 if (item.pass && !item.unexpected) 438 continue; 439 440 var tr = document.createElement("tr"); 441 var rowclass = item.pass ? "pass" : "fail"; 442 var td; 443 var text; 444 445 td = document.createElement("td"); 446 text = ""; 447 if (item.unexpected) { text += "!"; rowclass += " unexpected"; } 448 if (item.random) { text += "R"; rowclass += " random"; } 449 if (item.skip) { text += "S"; rowclass += " skip"; } 450 td.appendChild(document.createTextNode(text)); 451 tr.appendChild(td); 452 453 td = document.createElement("td"); 454 td.id = "item" + i; 455 td.className = "url"; 456 // Only display part of URL after "/mozilla/". 457 var match = item.url.match(/\/mozilla\/(.*)/); 458 text = document.createTextNode(match ? match[1] : item.url); 459 if (item.images.length > 0) { 460 var a = document.createElement("a"); 461 a.href = "javascript:show_images(" + i + ")"; 462 a.appendChild(text); 463 td.appendChild(a); 464 } else { 465 td.appendChild(text); 466 } 467 tr.appendChild(td); 468 469 tbody.appendChild(tr); 470 } 471 472 cell.appendChild(table); 473 474 show_phase("viewer"); 475 } 476 477 function get_image_data(src, whenReady) { 478 var img = new Image(); 479 img.onload = function() { 480 var canvas = document.createElement("canvas"); 481 canvas.width = img.naturalWidth; 482 canvas.height = img.naturalHeight; 483 484 var ctx = canvas.getContext("2d"); 485 ctx.drawImage(img, 0, 0); 486 487 whenReady(ctx.getImageData(0, 0, img.naturalWidth, img.naturalHeight)); 488 }; 489 img.src = src; 490 } 491 492 function sync_svg_size(imageData) { 493 // We need the size of the 'svg' and its 'image' elements to match the size 494 // of the ImageData objects that we're going to read pixels from or else our 495 // magnify() function will be very broken. 496 ID("svg").setAttribute("width", imageData.width); 497 ID("svg").setAttribute("height", imageData.height); 498 } 499 500 function sync_heatmap_size(imageData) { 501 ID("heat_canvas").setAttribute("width" , imageData.width); 502 ID("heat_canvas").setAttribute("height", imageData.height); 503 } 504 505 function show_images(i) { 506 var item = gTestItems[i]; 507 var cell = ID("images"); 508 509 // Remove activeitem class from any existing elements 510 var activeItems = document.querySelectorAll(".activeitem"); 511 for (var activeItemIdx = activeItems.length; activeItemIdx-- != 0;) { 512 activeItems[activeItemIdx].classList.remove("activeitem"); 513 } 514 515 ID("item" + i).classList.add("activeitem"); 516 ID("image1").style.display = ""; 517 ID("image2").style.display = "none"; 518 show_diff_none(); 519 ID("imgcontrols").reset(); 520 ID("diffcontrols").reset(); 521 522 ID("image1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]); 523 // Making the href be #image1 doesn't seem to work 524 ID("feimage1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]); 525 if (item.images.length == 1) { 526 ID("imgcontrols").style.display = "none"; 527 } else { 528 ID("imgcontrols").style.display = ""; 529 530 ID("image2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]); 531 // Making the href be #image2 doesn't seem to work 532 ID("feimage2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]); 533 534 ID("label1").textContent = 'Image ' + item.imageLabels[0]; 535 ID("label2").textContent = 'Image ' + item.imageLabels[1]; 536 } 537 538 cell.style.display = ""; 539 540 get_image_data(item.images[0], function(data) { gImage1Data = data; sync_svg_size(gImage1Data); sync_heatmap_size(gImage1Data); heatmap_change_image(0, gImage1Data); }); 541 get_image_data(item.images[1], function(data) { gImage2Data = data; heatmap_change_image(1, gImage2Data); }); 542 } 543 544 function show_image(i) { 545 if (i == 1) { 546 ID("image1").style.display = ""; 547 ID("image2").style.display = "none"; 548 } else { 549 ID("image1").style.display = "none"; 550 ID("image2").style.display = ""; 551 } 552 } 553 554 function handle_keyboard_shortcut(event) { 555 switch (event.charCode) { 556 case 49: // "1" key 557 document.getElementById("radio1").checked = true; 558 show_image(1); 559 break; 560 case 50: // "2" key 561 document.getElementById("radio2").checked = true; 562 show_image(2); 563 break; 564 case 100: // "d" key 565 document.getElementById("radio_diff_circle").click(); 566 break; 567 case 104: // "h" key 568 document.getElementById("radio_diff_heatmap").click(); 569 break; 570 case 112: // "p" key 571 shift_images(-1); 572 break; 573 case 110: // "n" key 574 shift_images(1); 575 break; 576 } 577 } 578 579 function shift_images(dir) { 580 var activeItem = document.querySelector(".activeitem"); 581 if (!activeItem) { 582 return; 583 } 584 for (var elm = activeItem; elm; elm = elm.parentElement) { 585 if (elm.tagName != "tr") { 586 continue; 587 } 588 elm = dir > 0 ? elm.nextElementSibling : elm.previousElementSibling; 589 if (elm) { 590 elm.getElementsByTagName("a")[0].click(); 591 } 592 return; 593 } 594 } 595 596 function show_diff_none() { 597 ID("svg") .style.display = ""; 598 ID("diffrect") .style.display = "none"; 599 ID("heat_canvas").style.display = "none"; 600 } 601 602 function show_diff_circle() { 603 ID("svg") .style.display = ""; 604 ID("diffrect") .style.display = ""; 605 ID("heat_canvas").style.display = "none"; 606 } 607 608 function show_diff_heatmap() { 609 ID("svg") .style.display = "none"; 610 ID("diffrect") .style.display = "none"; 611 ID("heat_canvas").style.display = ""; 612 613 if (heatmapCanvas === null) { 614 canvas = document.getElementById('heat_canvas'); 615 heatmap_render_setup(canvas); 616 heatmap_change_image(0, gImage1Data); 617 heatmap_change_image(1, gImage2Data); 618 heatmap_render(0, 0); 619 620 window.addEventListener('mousemove', e => { 621 var { x: x, y: y } = heatmap_on_mousemove(e); 622 magnify_around(Math.floor(x), Math.floor(y)); 623 }); 624 } 625 } 626 627 function flash_pixels(on) { 628 var stroke = on ? "red" : "black"; 629 var strokeWidth = on ? "2px" : "1px"; 630 for (var i = 0; i < gFlashingPixels.length; i++) { 631 gFlashingPixels[i].setAttribute("stroke", stroke); 632 gFlashingPixels[i].setAttribute("stroke-width", strokeWidth); 633 } 634 } 635 636 function cursor_point(evt) { 637 var m = evt.target.getScreenCTM().inverse(); 638 var p = ID("svg").createSVGPoint(); 639 p.x = evt.clientX; 640 p.y = evt.clientY; 641 p = p.matrixTransform(m); 642 return { x: Math.floor(p.x), y: Math.floor(p.y) }; 643 } 644 645 function hex2(i) { 646 return (i < 16 ? "0" : "") + i.toString(16); 647 } 648 649 function canvas_pixel_as_hex(data, x, y) { 650 var offset = (y * data.width + x) * 4; 651 var r = data.data[offset]; 652 var g = data.data[offset + 1]; 653 var b = data.data[offset + 2]; 654 return "#" + hex2(r) + hex2(g) + hex2(b); 655 } 656 657 function hex_as_rgb(hex) { 658 return "rgb(" + [parseInt(hex.substring(1, 3), 16), parseInt(hex.substring(3, 5), 16), parseInt(hex.substring(5, 7), 16)] + ")"; 659 } 660 661 function magnify(evt) { 662 var { x: x, y: y } = cursor_point(evt); 663 magnify_around(x, y); 664 } 665 666 function magnify_around(x, y) { 667 if (x < 0 || y < 0 || x >= gImage1Data.width || y >= gImage1Data.height) { 668 return; 669 } 670 var centerPixelColor1, centerPixelColor2; 671 672 var dx_lo = -Math.floor(gMagWidth / 2); 673 var dx_hi = Math.floor(gMagWidth / 2); 674 var dy_lo = -Math.floor(gMagHeight / 2); 675 var dy_hi = Math.floor(gMagHeight / 2); 676 677 flash_pixels(false); 678 gFlashingPixels = []; 679 for (var j = dy_lo; j <= dy_hi; j++) { 680 for (var i = dx_lo; i <= dx_hi; i++) { 681 var px = x + i; 682 var py = y + j; 683 var p1 = gMagPixPaths[i + dx_hi][j + dy_hi][0]; 684 var p2 = gMagPixPaths[i + dx_hi][j + dy_hi][1]; 685 // Here we just use the dimensions of gImage1Data since we expect test 686 // and reference to have the same dimensions. 687 if (px < 0 || py < 0 || px >= gImage1Data.width || py >= gImage1Data.height) { 688 p1.setAttribute("fill", "#aaa"); 689 p2.setAttribute("fill", "#888"); 690 } else { 691 var color1 = canvas_pixel_as_hex(gImage1Data, x + i, y + j); 692 var color2 = canvas_pixel_as_hex(gImage2Data, x + i, y + j); 693 p1.setAttribute("fill", color1); 694 p2.setAttribute("fill", color2); 695 if (color1 != color2) { 696 gFlashingPixels.push(p1, p2); 697 p1.parentNode.appendChild(p1); 698 p2.parentNode.appendChild(p2); 699 } 700 if (i == 0 && j == 0) { 701 centerPixelColor1 = color1; 702 centerPixelColor2 = color2; 703 } 704 } 705 } 706 } 707 flash_pixels(true); 708 show_pixelinfo(x, y, centerPixelColor1, hex_as_rgb(centerPixelColor1), centerPixelColor2, hex_as_rgb(centerPixelColor2)); 709 } 710 711 function show_pixelinfo(x, y, pix1rgb, pix1hex, pix2rgb, pix2hex) { 712 var pixelinfo = ID("pixelinfo"); 713 ID("coords").textContent = [x, y]; 714 ID("pix1hex").textContent = pix1hex; 715 ID("pix1rgb").textContent = pix1rgb; 716 ID("pix2hex").textContent = pix2hex; 717 ID("pix2rgb").textContent = pix2rgb; 718 } 719 720 ]]></script> 721 722 </head> 723 <body onload="load()"> 724 725 <div id="entry"> 726 727 <h1>Reftest analyzer: load reftest log</h1> 728 729 <p>Either paste your log into this textarea:<br /> 730 <textarea cols="80" rows="10" id="logentry"/><br/> 731 <input type="button" value="Process pasted log" onclick="log_pasted()" /></p> 732 733 <p>... or load it from a file:<br/> 734 <input type="file" id="fileentry" onchange="fileentry_changed()" /> 735 </p> 736 </div> 737 738 <div id="loading" style="display:none">Loading log...</div> 739 740 <div id="viewer" style="display:none"> 741 <div id="pixelarea"> 742 <div id="pixelinfo"> 743 <table> 744 <tbody> 745 <tr><th>Pixel at:</th><td colspan="2" id="coords"/></tr> 746 <tr><th>Image 1:</th><td id="pix1rgb"></td><td id="pix1hex"></td></tr> 747 <tr><th>Image 2:</th><td id="pix2rgb"></td><td id="pix2hex"></td></tr> 748 </tbody> 749 </table> 750 <div> 751 <div id="pixelhint">★ 752 <div> 753 <p>Move the mouse over the reftest image on the right to show 754 magnified pixels on the left. The color information above is for 755 the pixel centered in the magnified view.</p> 756 <p>Image 1 is shown in the upper triangle of each pixel and Image 2 757 is shown in the lower triangle.</p> 758 </div> 759 </div> 760 </div> 761 </div> 762 <div id="magnification"> 763 <svg xmlns="http://www.w3.org/2000/svg" width="84" height="84" shape-rendering="optimizeSpeed"> 764 <g id="mag"/> 765 </svg> 766 </div> 767 </div> 768 <div id="itemlist"></div> 769 <div id="images" style="display:none"> 770 <form id="imgcontrols"> 771 <input id="radio1" type="radio" name="which" value="0" onchange="show_image(1)" checked="checked" /><label id="label1" title="1" for="radio1">Image 1</label> 772 <input id="radio2" type="radio" name="which" value="1" onchange="show_image(2)" /><label id="label2" title="2" for="radio2">Image 2</label> 773 </form> 774 775 <form id="diffcontrols"> 776 Differences: 777 <input id="radio_diff_none" name="diff" value="0" type="radio" onchange="show_diff_none()" checked="checked"/> 778 <label for="radio_diff_none">None</label> 779 <input id="radio_diff_circle" name="diff" value="1" type="radio" onchange="show_diff_circle()" /> 780 <label for="radio_diff_circle">Circle</label> 781 <input id="radio_diff_heatmap" name="diff" value="2" type="radio" onchange="show_diff_heatmap()" /> 782 <label for="radio_diff_heatmap">Heatmap</label> 783 </form> 784 785 <canvas width="800" height="1000" id="heat_canvas" style="display:none;"></canvas> 786 787 <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800" height="1000" id="svg"> 788 <defs> 789 <!-- use sRGB to avoid loss of data --> 790 <filter id="showDifferences" x="0%" y="0%" width="100%" height="100%" 791 style="color-interpolation-filters: sRGB"> 792 <feImage id="feimage1" result="img1" xlink:href="#image1" /> 793 <feImage id="feimage2" result="img2" xlink:href="#image2" /> 794 <!-- inv1 and inv2 are the images with RGB inverted --> 795 <feComponentTransfer result="inv1" in="img1"> 796 <feFuncR type="linear" slope="-1" intercept="1" /> 797 <feFuncG type="linear" slope="-1" intercept="1" /> 798 <feFuncB type="linear" slope="-1" intercept="1" /> 799 </feComponentTransfer> 800 <feComponentTransfer result="inv2" in="img2"> 801 <feFuncR type="linear" slope="-1" intercept="1" /> 802 <feFuncG type="linear" slope="-1" intercept="1" /> 803 <feFuncB type="linear" slope="-1" intercept="1" /> 804 </feComponentTransfer> 805 <!-- w1 will have non-white pixels anywhere that img2 806 is brighter than img1, and w2 for the reverse. 807 It would be nice not to have to go through these 808 intermediate states, but feComposite 809 type="arithmetic" can't transform the RGB channels 810 and leave the alpha channel untouched. --> 811 <feComposite result="w1" in="img1" in2="inv2" operator="arithmetic" k2="1" k3="1" /> 812 <feComposite result="w2" in="img2" in2="inv1" operator="arithmetic" k2="1" k3="1" /> 813 <!-- c1 will have non-black pixels anywhere that img2 814 is brighter than img1, and c2 for the reverse --> 815 <feComponentTransfer result="c1" in="w1"> 816 <feFuncR type="linear" slope="-1" intercept="1" /> 817 <feFuncG type="linear" slope="-1" intercept="1" /> 818 <feFuncB type="linear" slope="-1" intercept="1" /> 819 </feComponentTransfer> 820 <feComponentTransfer result="c2" in="w2"> 821 <feFuncR type="linear" slope="-1" intercept="1" /> 822 <feFuncG type="linear" slope="-1" intercept="1" /> 823 <feFuncB type="linear" slope="-1" intercept="1" /> 824 </feComponentTransfer> 825 <!-- c will be nonblack (and fully on) for every pixel+component where there are differences --> 826 <feComposite result="c" in="c1" in2="c2" operator="arithmetic" k2="255" k3="255" /> 827 <!-- a will be opaque for every pixel with differences and transparent for all others --> 828 <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" /> 829 830 <!-- a, dilated by 1 pixel --> 831 <feMorphology result="dila1" in="a" operator="dilate" radius="1" /> 832 <!-- a, dilated by 2 pixels --> 833 <feMorphology result="dila2" in="dila1" operator="dilate" radius="1" /> 834 835 <!-- all the pixels in the 2-pixel dilation of a but not in the 1-pixel dilation, to highlight the diffs --> 836 <feComposite result="highlight" in="dila2" in2="dila1" operator="out" /> 837 838 <feFlood result="red" flood-color="red" /> 839 <feComposite result="redhighlight" in="red" in2="highlight" operator="in" /> 840 <feFlood result="black" flood-color="black" flood-opacity="0.5" /> 841 <feMerge> 842 <feMergeNode in="black" /> 843 <feMergeNode in="redhighlight" /> 844 </feMerge> 845 </filter> 846 </defs> 847 <g onmousemove="magnify(evt)"> 848 <image x="0" y="0" width="100%" height="100%" id="image1" /> 849 <image x="0" y="0" width="100%" height="100%" id="image2" /> 850 </g> 851 <rect id="diffrect" filter="url(#showDifferences)" pointer-events="none" x="0" y="0" width="100%" height="100%" /> 852 </svg> 853 </div> 854 </div> 855 856 </body> 857 </html>