From f5c3abb8f72ae95b34eaf5f83f0289cdd0b68ede Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Mon, 7 Feb 2022 14:40:21 -0800 Subject: [PATCH] Generate test images at different output scales. This will default to generating test images at the device pixel ratio of the machine the tests are created on unless the test explicitly defines and output scale using the `outputScale` setting. This makes the test look visually like they would on the machine they are running on. It also allows us to test different output scales. --- test/driver.js | 61 ++++++++++++++++++++++------ test/resources/reftest-analyzer.html | 4 +- test/resources/reftest-analyzer.js | 44 +++++++++++++------- test/test.js | 14 ++++--- test/test_manifest.json | 12 ++++++ 5 files changed, 101 insertions(+), 34 deletions(-) diff --git a/test/driver.js b/test/driver.js index 52d933889..f6d5115ff 100644 --- a/test/driver.js +++ b/test/driver.js @@ -59,7 +59,7 @@ function loadStyles(styles) { return Promise.all(promises); } -function writeSVG(svgElement, ctx) { +function writeSVG(svgElement, ctx, outputScale) { // We need to have UTF-8 encoded XML. const svg_xml = unescape( encodeURIComponent(new XMLSerializer().serializeToString(svgElement)) @@ -102,7 +102,7 @@ function inlineImages(images) { return Promise.all(imagePromises); } -async function convertCanvasesToImages(annotationCanvasMap) { +async function convertCanvasesToImages(annotationCanvasMap, outputScale) { const results = new Map(); const promises = []; for (const [key, canvas] of annotationCanvasMap) { @@ -110,7 +110,10 @@ async function convertCanvasesToImages(annotationCanvasMap) { new Promise(resolve => { canvas.toBlob(blob => { const image = document.createElement("img"); - image.onload = resolve; + image.onload = function () { + image.style.width = Math.floor(image.width / outputScale) + "px"; + resolve(); + }; results.set(key, image); image.src = URL.createObjectURL(blob); }); @@ -198,6 +201,7 @@ class Rasterize { static async annotationLayer( ctx, viewport, + outputScale, annotations, annotationCanvasMap, page, @@ -213,7 +217,8 @@ class Rasterize { const annotationViewport = viewport.clone({ dontFlip: true }); const annotationImageMap = await convertCanvasesToImages( - annotationCanvasMap + annotationCanvasMap, + outputScale ); // Rendering annotation layer as HTML. @@ -600,13 +605,38 @@ class Driver { ctx = this.canvas.getContext("2d", { alpha: false }); task.pdfDoc.getPage(task.pageNum).then( page => { - const viewport = page.getViewport({ + // Default to creating the test images at the devices pixel ratio, + // unless the test explicitly specifies an output scale. + const outputScale = task.outputScale || window.devicePixelRatio; + let viewport = page.getViewport({ scale: PixelsPerInch.PDF_TO_CSS_UNITS, }); - this.canvas.width = viewport.width; - this.canvas.height = viewport.height; + // Restrict the test from creating a canvas that is too big. + const MAX_CANVAS_PIXEL_DIMENSION = 4096; + const largestDimension = Math.max(viewport.width, viewport.height); + if ( + Math.floor(largestDimension * outputScale) > + MAX_CANVAS_PIXEL_DIMENSION + ) { + const rescale = MAX_CANVAS_PIXEL_DIMENSION / largestDimension; + viewport = viewport.clone({ + scale: PixelsPerInch.PDF_TO_CSS_UNITS * rescale, + }); + } + const pixelWidth = Math.floor(viewport.width * outputScale); + const pixelHeight = Math.floor(viewport.height * outputScale); + task.viewportWidth = Math.floor(viewport.width); + task.viewportHeight = Math.floor(viewport.height); + task.outputScale = outputScale; + this.canvas.width = pixelWidth; + this.canvas.height = pixelHeight; + this.canvas.style.width = Math.floor(viewport.width) + "px"; + this.canvas.style.height = Math.floor(viewport.height) + "px"; this._clearCanvas(); + const transform = + outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null; + // Initialize various `eq` test subtypes, see comment below. let renderAnnotations = false, renderForms = false, @@ -631,8 +661,8 @@ class Driver { textLayerCanvas = document.createElement("canvas"); this.textLayerCanvas = textLayerCanvas; } - textLayerCanvas.width = viewport.width; - textLayerCanvas.height = viewport.height; + textLayerCanvas.width = pixelWidth; + textLayerCanvas.height = pixelHeight; const textLayerContext = textLayerCanvas.getContext("2d"); textLayerContext.clearRect( 0, @@ -640,6 +670,7 @@ class Driver { textLayerCanvas.width, textLayerCanvas.height ); + textLayerContext.scale(outputScale, outputScale); const enhanceText = !!task.enhance; // The text builder will draw its content on the test canvas initPromise = page @@ -672,8 +703,8 @@ class Driver { annotationLayerCanvas = document.createElement("canvas"); this.annotationLayerCanvas = annotationLayerCanvas; } - annotationLayerCanvas.width = viewport.width; - annotationLayerCanvas.height = viewport.height; + annotationLayerCanvas.width = pixelWidth; + annotationLayerCanvas.height = pixelHeight; annotationLayerContext = annotationLayerCanvas.getContext("2d"); annotationLayerContext.clearRect( 0, @@ -681,6 +712,7 @@ class Driver { annotationLayerCanvas.width, annotationLayerCanvas.height ); + annotationLayerContext.scale(outputScale, outputScale); if (!renderXfa) { // The annotation builder will draw its content @@ -709,6 +741,7 @@ class Driver { viewport, optionalContentConfigPromise: task.optionalContentConfigPromise, annotationCanvasMap, + transform, }; if (renderForms) { renderContext.annotationMode = AnnotationMode.ENABLE_FORMS; @@ -725,7 +758,7 @@ class Driver { ctx.save(); ctx.globalCompositeOperation = "screen"; ctx.fillStyle = "rgb(128, 255, 128)"; // making it green - ctx.fillRect(0, 0, viewport.width, viewport.height); + ctx.fillRect(0, 0, pixelWidth, pixelHeight); ctx.restore(); ctx.drawImage(textLayerCanvas, 0, 0); } @@ -755,6 +788,7 @@ class Driver { Rasterize.annotationLayer( annotationLayerContext, viewport, + outputScale, data, annotationCanvasMap, page, @@ -864,6 +898,9 @@ class Driver { page: task.pageNum, snapshot, stats: task.stats.times, + viewportWidth: task.viewportWidth, + viewportHeight: task.viewportHeight, + outputScale: task.outputScale, }); this._send("/submit_task_results", result, callback); } diff --git a/test/resources/reftest-analyzer.html b/test/resources/reftest-analyzer.html index 2836f6860..249c2eb40 100644 --- a/test/resources/reftest-analyzer.html +++ b/test/resources/reftest-analyzer.html @@ -165,8 +165,8 @@ Original author: L. David Baron - - + + diff --git a/test/resources/reftest-analyzer.js b/test/resources/reftest-analyzer.js index 2a192f4a0..913f2331e 100644 --- a/test/resources/reftest-analyzer.js +++ b/test/resources/reftest-analyzer.js @@ -228,10 +228,15 @@ window.onload = function () { }); continue; } - match = line.match(/^ {2}IMAGE[^:]*: (.*)$/); + match = line.match(/^ {2}IMAGE[^:]*\((\d+)x(\d+)x(\d+)\): (.*)$/); if (match) { const item = gTestItems[gTestItems.length - 1]; - item.images.push(match[1]); + item.images.push({ + width: parseFloat(match[1]), + height: parseFloat(match[2]), + outputScale: parseFloat(match[3]), + file: match[4], + }); } } buildViewer(); @@ -335,16 +340,31 @@ window.onload = function () { const cell = ID("images"); ID("image1").style.display = ""; + const scale = item.images[0].outputScale / window.devicePixelRatio; + ID("image1").setAttribute("width", item.images[0].width * scale); + ID("image1").setAttribute("height", item.images[0].height * scale); + + ID("svg").setAttribute("width", item.images[0].width * scale); + ID("svg").setAttribute("height", item.images[0].height * scale); + ID("image2").style.display = "none"; + if (item.images[1]) { + ID("image2").setAttribute("width", item.images[1].width * scale); + ID("image2").setAttribute("height", item.images[1].height * scale); + } ID("diffrect").style.display = "none"; ID("imgcontrols").reset(); - ID("image1").setAttributeNS(XLINK_NS, "xlink:href", gPath + item.images[0]); + ID("image1").setAttributeNS( + XLINK_NS, + "xlink:href", + gPath + item.images[0].file + ); // Making the href be #image1 doesn't seem to work ID("feimage1").setAttributeNS( XLINK_NS, "xlink:href", - gPath + item.images[0] + gPath + item.images[0].file ); if (item.images.length === 1) { ID("imgcontrols").style.display = "none"; @@ -353,30 +373,24 @@ window.onload = function () { ID("image2").setAttributeNS( XLINK_NS, "xlink:href", - gPath + item.images[1] + gPath + item.images[1].file ); // Making the href be #image2 doesn't seem to work ID("feimage2").setAttributeNS( XLINK_NS, "xlink:href", - gPath + item.images[1] + gPath + item.images[1].file ); } cell.style.display = ""; - getImageData(item.images[0], function (data) { + getImageData(item.images[0].file, function (data) { gImage1Data = data; - syncSVGSize(gImage1Data); }); - getImageData(item.images[1], function (data) { + getImageData(item.images[1].file, function (data) { gImage2Data = data; }); } - function syncSVGSize(imageData) { - ID("svg").setAttribute("width", imageData.width); - ID("svg").setAttribute("height", imageData.height); - } - function showImage(i) { if (i === 1) { ID("image1").style.display = ""; @@ -414,7 +428,7 @@ window.onload = function () { } function canvasPixelAsHex(data, x, y) { - const offset = (y * data.width + x) * 4; + const offset = (y * data.width + x) * 4 * window.devicePixelRatio; const r = data.data[offset]; const g = data.data[offset + 1]; const b = data.data[offset + 2]; diff --git a/test/test.js b/test/test.js index 3ec9f518d..9cb619932 100644 --- a/test/test.js +++ b/test/test.js @@ -451,7 +451,8 @@ function checkEq(task, results, browser, masterMode) { if (!pageResults[page]) { continue; } - var testSnapshot = pageResults[page].snapshot; + const pageResult = pageResults[page]; + let testSnapshot = pageResult.snapshot; if (testSnapshot && testSnapshot.startsWith("data:image/png;base64,")) { testSnapshot = Buffer.from(testSnapshot.substring(22), "base64"); } else { @@ -492,8 +493,8 @@ function checkEq(task, results, browser, masterMode) { refSnapshot ); - // NB: this follows the format of Mozilla reftest output so that - // we can reuse its reftest-analyzer script + // This no longer follows the format of Mozilla reftest output. + const viewportString = `(${pageResult.viewportWidth}x${pageResult.viewportHeight}x${pageResult.outputScale})`; fs.appendFileSync( eqLog, "REFTEST TEST-UNEXPECTED-FAIL | " + @@ -503,10 +504,10 @@ function checkEq(task, results, browser, masterMode) { "-page" + (page + 1) + " | image comparison (==)\n" + - "REFTEST IMAGE 1 (TEST): " + + `REFTEST IMAGE 1 (TEST)${viewportString}: ` + path.join(testSnapshotDir, page + 1 + ".png") + "\n" + - "REFTEST IMAGE 2 (REFERENCE): " + + `REFTEST IMAGE 2 (REFERENCE)${viewportString}: ` + path.join(testSnapshotDir, page + 1 + "_ref.png") + "\n" ); @@ -735,6 +736,9 @@ function refTestPostHandler(req, res) { taskResults[round][page] = { failure, snapshot, + viewportWidth: data.viewportWidth, + viewportHeight: data.viewportHeight, + outputScale: data.outputScale, }; if (stats) { stats.push({ diff --git a/test/test_manifest.json b/test/test_manifest.json index 38c3eee19..3e5441f99 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -6115,6 +6115,18 @@ "forms": true, "lastPage": 1 }, + { + "id": "issue12716-hidpi", + "file": "pdfs/issue12716.pdf", + "md5": "9bdc9c552bcfccd629f5f97385e79ca5", + "rounds": 1, + "link": true, + "type": "eq", + "forms": true, + "lastPage": 1, + "outputScale": 2, + "about": "This tests draws to another canvas for the button, so it's a good test to ensure output scale is working." + }, { "id": "xfa_issue13500", "file": "pdfs/xfa_issue13500.pdf", "md5": "b81274a19f5a95c1466db3648f1be491",