From a58700b0dca082400036deff0b09bbd43a6c4c2a Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sat, 4 Dec 2021 15:21:04 +0100 Subject: [PATCH 1/7] Convert the `Driver` class to ES6 syntax in `test/driver.js` --- test/driver.js | 1067 ++++++++++++++++++++++++------------------------ 1 file changed, 524 insertions(+), 543 deletions(-) diff --git a/test/driver.js b/test/driver.js index 51a35d311..db31b7221 100644 --- a/test/driver.js +++ b/test/driver.js @@ -393,17 +393,12 @@ var rasterizeXfaLayer = (function rasterizeXfaLayerClosure() { * @property {HTMLDivElement} end - Container for a completion message. */ -/** - * @class - */ // eslint-disable-next-line no-unused-vars -var Driver = (function DriverClosure() { +class Driver { /** - * @constructs Driver * @param {DriverOptions} options */ - // eslint-disable-next-line no-shadow - function Driver(options) { + constructor(options) { // Configure the global worker options. GlobalWorkerOptions.workerSrc = WORKER_SRC; @@ -428,569 +423,555 @@ var Driver = (function DriverClosure() { this.canvas = document.createElement("canvas"); } - Driver.prototype = { - _getQueryStringParameters: function Driver_getQueryStringParameters() { - const queryString = window.location.search.substring(1); - return Object.fromEntries(new URLSearchParams(queryString).entries()); - }, + _getQueryStringParameters() { + const queryString = window.location.search.substring(1); + return Object.fromEntries(new URLSearchParams(queryString).entries()); + } - run: function Driver_run() { - var self = this; - window.onerror = function (message, source, line, column, error) { - self._info( - "Error: " + - message + - " Script: " + - source + - " Line: " + - line + - " Column: " + - column + - " StackTrace: " + - error - ); - }; - this._info("User agent: " + navigator.userAgent); - this._log(`Harness thinks this browser is ${this.browser}\n`); - this._log('Fetching manifest "' + this.manifestFile + '"... '); + run() { + var self = this; + window.onerror = function (message, source, line, column, error) { + self._info( + "Error: " + + message + + " Script: " + + source + + " Line: " + + line + + " Column: " + + column + + " StackTrace: " + + error + ); + }; + this._info("User agent: " + navigator.userAgent); + this._log(`Harness thinks this browser is ${this.browser}\n`); + this._log('Fetching manifest "' + this.manifestFile + '"... '); - var r = new XMLHttpRequest(); - r.open("GET", this.manifestFile, false); - r.onreadystatechange = function () { - if (r.readyState === 4) { - self._log("done\n"); - self.manifest = JSON.parse(r.responseText); - if (self.testFilter?.length || self.xfaOnly) { - self.manifest = self.manifest.filter(function (item) { - if (self.testFilter.includes(item.id)) { - return true; - } - if (self.xfaOnly && item.enableXfa) { - return true; - } - return false; - }); - } - self.currentTask = 0; - self._nextTask(); - } - }; - if (this.delay > 0) { - this._log("\nDelaying for " + this.delay + " ms...\n"); - } - // When gathering the stats the numbers seem to be more reliable - // if the browser is given more time to start. - setTimeout(function () { - r.send(null); - }, this.delay); - }, - - /** - * A debugging tool to log to the terminal while tests are running. - * XXX: This isn't currently referenced, but it's useful for debugging so - * do not remove it. - * - * @param {string} msg - The message to log, it will be prepended with the - * current PDF ID if there is one. - */ - log(msg) { - let id = this.browser; - const task = this.manifest[this.currentTask]; - if (task) { - id += `-${task.id}`; - } - - this._info(`${id}: ${msg}`); - }, - - _nextTask() { - let failure = ""; - - this._cleanup().then(() => { - if (this.currentTask === this.manifest.length) { - this._done(); - return; - } - const task = this.manifest[this.currentTask]; - task.round = 0; - task.pageNum = task.firstPage || 1; - task.stats = { times: [] }; - task.enableXfa = task.enableXfa === true; - - // Support *linked* test-cases for the other suites, e.g. unit- and - // integration-tests, without needing to run them as reference-tests. - if (task.type === "other") { - this._log(`Skipping file "${task.file}"\n`); - - if (!task.link) { - this._nextPage(task, 'Expected "other" test-case to be linked.'); - return; - } - this.currentTask++; - this._nextTask(); - return; - } - - this._log('Loading file "' + task.file + '"\n'); - - const absoluteUrl = new URL(task.file, window.location).href; - try { - let xfaStyleElement = null; - if (task.enableXfa) { - // Need to get the font definitions to inject them in the SVG. - // So we create this element and those definitions will be - // appended in font_loader.js. - xfaStyleElement = document.createElement("style"); - document.documentElement - .getElementsByTagName("head")[0] - .appendChild(xfaStyleElement); - } - - const loadingTask = getDocument({ - url: absoluteUrl, - password: task.password, - cMapUrl: CMAP_URL, - cMapPacked: CMAP_PACKED, - standardFontDataUrl: STANDARD_FONT_DATA_URL, - disableRange: task.disableRange, - disableAutoFetch: !task.enableAutoFetch, - pdfBug: true, - useSystemFonts: task.useSystemFonts, - useWorkerFetch: task.useWorkerFetch, - enableXfa: task.enableXfa, - styleElement: xfaStyleElement, + var r = new XMLHttpRequest(); + r.open("GET", this.manifestFile, false); + r.onreadystatechange = function () { + if (r.readyState === 4) { + self._log("done\n"); + self.manifest = JSON.parse(r.responseText); + if (self.testFilter?.length || self.xfaOnly) { + self.manifest = self.manifest.filter(function (item) { + if (self.testFilter.includes(item.id)) { + return true; + } + if (self.xfaOnly && item.enableXfa) { + return true; + } + return false; }); - loadingTask.promise.then( - async doc => { - if (task.enableXfa) { - task.fontRules = ""; - for (const rule of xfaStyleElement.sheet.cssRules) { - task.fontRules += rule.cssText + "\n"; - } - } + } + self.currentTask = 0; + self._nextTask(); + } + }; + if (this.delay > 0) { + this._log("\nDelaying for " + this.delay + " ms...\n"); + } + // When gathering the stats the numbers seem to be more reliable + // if the browser is given more time to start. + setTimeout(function () { + r.send(null); + }, this.delay); + } - task.pdfDoc = doc; - task.optionalContentConfigPromise = - doc.getOptionalContentConfig(); + /** + * A debugging tool to log to the terminal while tests are running. + * XXX: This isn't currently referenced, but it's useful for debugging so + * do not remove it. + * + * @param {string} msg - The message to log, it will be prepended with the + * current PDF ID if there is one. + */ + log(msg) { + let id = this.browser; + const task = this.manifest[this.currentTask]; + if (task) { + id += `-${task.id}`; + } - if (task.optionalContent) { - const entries = Object.entries(task.optionalContent), - optionalContentConfig = - await task.optionalContentConfigPromise; - for (const [id, visible] of entries) { - optionalContentConfig.setVisibility(id, visible); - } - } + this._info(`${id}: ${msg}`); + } - this._nextPage(task, failure); - }, - err => { - failure = "Loading PDF document: " + err; - this._nextPage(task, failure); - } - ); + _nextTask() { + let failure = ""; + + this._cleanup().then(() => { + if (this.currentTask === this.manifest.length) { + this._done(); + return; + } + const task = this.manifest[this.currentTask]; + task.round = 0; + task.pageNum = task.firstPage || 1; + task.stats = { times: [] }; + task.enableXfa = task.enableXfa === true; + + // Support *linked* test-cases for the other suites, e.g. unit- and + // integration-tests, without needing to run them as reference-tests. + if (task.type === "other") { + this._log(`Skipping file "${task.file}"\n`); + + if (!task.link) { + this._nextPage(task, 'Expected "other" test-case to be linked.'); return; - } catch (e) { - failure = "Loading PDF document: " + this._exceptionToString(e); } - this._nextPage(task, failure); - }); - }, + this.currentTask++; + this._nextTask(); + return; + } - _cleanup() { - // Clear out all the stylesheets since a new one is created for each font. - while (document.styleSheets.length > 0) { - const styleSheet = document.styleSheets[0]; - while (styleSheet.cssRules.length > 0) { - styleSheet.deleteRule(0); + this._log('Loading file "' + task.file + '"\n'); + + const absoluteUrl = new URL(task.file, window.location).href; + try { + let xfaStyleElement = null; + if (task.enableXfa) { + // Need to get the font definitions to inject them in the SVG. + // So we create this element and those definitions will be + // appended in font_loader.js. + xfaStyleElement = document.createElement("style"); + document.documentElement + .getElementsByTagName("head")[0] + .appendChild(xfaStyleElement); } - styleSheet.ownerNode.remove(); - } - const body = document.body; - while (body.lastChild !== this.end) { - body.lastChild.remove(); - } - const destroyedPromises = []; - // Wipe out the link to the pdfdoc so it can be GC'ed. - for (let i = 0; i < this.manifest.length; i++) { - if (this.manifest[i].pdfDoc) { - destroyedPromises.push(this.manifest[i].pdfDoc.destroy()); - delete this.manifest[i].pdfDoc; - } - } - return Promise.all(destroyedPromises); - }, - - _exceptionToString: function Driver_exceptionToString(e) { - if (typeof e !== "object") { - return String(e); - } - if (!("message" in e)) { - return JSON.stringify(e); - } - return e.message + ("stack" in e ? " at " + e.stack.split("\n")[0] : ""); - }, - - _getLastPageNumber: function Driver_getLastPageNumber(task) { - if (!task.pdfDoc) { - return task.firstPage || 1; - } - var lastPageNumber = task.lastPage || 0; - if (!lastPageNumber || lastPageNumber > task.pdfDoc.numPages) { - lastPageNumber = task.pdfDoc.numPages; - } - return lastPageNumber; - }, - - _nextPage: function Driver_nextPage(task, loadError) { - var self = this; - var failure = loadError || ""; - var ctx; - - if (!task.pdfDoc) { - var dataUrl = this.canvas.toDataURL("image/png"); - this._sendResult(dataUrl, task, failure, function () { - self._log( - "done" + (failure ? " (failed !: " + failure + ")" : "") + "\n" - ); - self.currentTask++; - self._nextTask(); + const loadingTask = getDocument({ + url: absoluteUrl, + password: task.password, + cMapUrl: CMAP_URL, + cMapPacked: CMAP_PACKED, + standardFontDataUrl: STANDARD_FONT_DATA_URL, + disableRange: task.disableRange, + disableAutoFetch: !task.enableAutoFetch, + pdfBug: true, + useSystemFonts: task.useSystemFonts, + useWorkerFetch: task.useWorkerFetch, + enableXfa: task.enableXfa, + styleElement: xfaStyleElement, }); - return; - } - - if (task.pageNum > this._getLastPageNumber(task)) { - if (++task.round < task.rounds) { - this._log(" Round " + (1 + task.round) + "\n"); - task.pageNum = task.firstPage || 1; - } else { - this.currentTask++; - this._nextTask(); - return; - } - } - - if (task.skipPages && task.skipPages.includes(task.pageNum)) { - this._log( - " Skipping page " + - task.pageNum + - "/" + - task.pdfDoc.numPages + - "...\n" - ); - task.pageNum++; - this._nextPage(task); - return; - } - - if (!failure) { - try { - this._log( - " Loading page " + - task.pageNum + - "/" + - task.pdfDoc.numPages + - "... " - ); - this.canvas.mozOpaque = true; - ctx = this.canvas.getContext("2d", { alpha: false }); - task.pdfDoc.getPage(task.pageNum).then( - function (page) { - var viewport = page.getViewport({ - scale: PixelsPerInch.PDF_TO_CSS_UNITS, - }); - self.canvas.width = viewport.width; - self.canvas.height = viewport.height; - self._clearCanvas(); - - // Initialize various `eq` test subtypes, see comment below. - var renderAnnotations = false, - renderForms = false, - renderPrint = false, - renderXfa = false, - annotationCanvasMap = null; - - if (task.annotationStorage) { - const entries = Object.entries(task.annotationStorage), - docAnnotationStorage = task.pdfDoc.annotationStorage; - for (const [key, value] of entries) { - docAnnotationStorage.setValue(key, value); - } + loadingTask.promise.then( + async doc => { + if (task.enableXfa) { + task.fontRules = ""; + for (const rule of xfaStyleElement.sheet.cssRules) { + task.fontRules += rule.cssText + "\n"; } - - var textLayerCanvas, annotationLayerCanvas; - var initPromise; - if (task.type === "text") { - // Using a dummy canvas for PDF context drawing operations - textLayerCanvas = self.textLayerCanvas; - if (!textLayerCanvas) { - textLayerCanvas = document.createElement("canvas"); - self.textLayerCanvas = textLayerCanvas; - } - textLayerCanvas.width = viewport.width; - textLayerCanvas.height = viewport.height; - var textLayerContext = textLayerCanvas.getContext("2d"); - textLayerContext.clearRect( - 0, - 0, - textLayerCanvas.width, - textLayerCanvas.height - ); - var enhanceText = !!task.enhance; - // The text builder will draw its content on the test canvas - initPromise = page - .getTextContent({ - normalizeWhitespace: true, - includeMarkedContent: true, - }) - .then(function (textContent) { - return rasterizeTextLayer( - textLayerContext, - viewport, - textContent, - enhanceText - ); - }); - } else { - textLayerCanvas = null; - // We fetch the `eq` specific test subtypes here, to avoid - // accidentally changing the behaviour for other types of tests. - renderAnnotations = !!task.annotations; - renderForms = !!task.forms; - renderPrint = !!task.print; - renderXfa = !!task.enableXfa; - - // Render the annotation layer if necessary. - if (renderAnnotations || renderForms || renderXfa) { - // Create a dummy canvas for the drawing operations. - annotationLayerCanvas = self.annotationLayerCanvas; - if (!annotationLayerCanvas) { - annotationLayerCanvas = document.createElement("canvas"); - self.annotationLayerCanvas = annotationLayerCanvas; - } - annotationLayerCanvas.width = viewport.width; - annotationLayerCanvas.height = viewport.height; - var annotationLayerContext = - annotationLayerCanvas.getContext("2d"); - annotationLayerContext.clearRect( - 0, - 0, - annotationLayerCanvas.width, - annotationLayerCanvas.height - ); - - if (!renderXfa) { - // The annotation builder will draw its content - // on the canvas. - initPromise = page.getAnnotations({ intent: "display" }); - annotationCanvasMap = new Map(); - } else { - initPromise = page.getXfa().then(function (xfa) { - return rasterizeXfaLayer( - annotationLayerContext, - viewport, - xfa, - task.fontRules, - task.pdfDoc.annotationStorage, - task.renderPrint - ); - }); - } - } else { - annotationLayerCanvas = null; - initPromise = Promise.resolve(); - } - } - var renderContext = { - canvasContext: ctx, - viewport, - optionalContentConfigPromise: task.optionalContentConfigPromise, - annotationCanvasMap, - }; - if (renderForms) { - renderContext.annotationMode = AnnotationMode.ENABLE_FORMS; - } else if (renderPrint) { - if (task.annotationStorage) { - renderContext.annotationMode = AnnotationMode.ENABLE_STORAGE; - } - renderContext.intent = "print"; - } - - var completeRender = function (error) { - // if text layer is present, compose it on top of the page - if (textLayerCanvas) { - ctx.save(); - ctx.globalCompositeOperation = "screen"; - ctx.fillStyle = "rgb(128, 255, 128)"; // making it green - ctx.fillRect(0, 0, viewport.width, viewport.height); - ctx.restore(); - ctx.drawImage(textLayerCanvas, 0, 0); - } - // If we have annotation layer, compose it on top of the page. - if (annotationLayerCanvas) { - ctx.drawImage(annotationLayerCanvas, 0, 0); - } - if (page.stats) { - // Get the page stats *before* running cleanup. - task.stats = page.stats; - } - page.cleanup(/* resetStats = */ true); - self._snapshot(task, error); - }; - initPromise - .then(function (data) { - const renderTask = page.render(renderContext); - - if (task.renderTaskOnContinue) { - renderTask.onContinue = function (cont) { - // Slightly delay the continued rendering. - setTimeout(cont, RENDER_TASK_ON_CONTINUE_DELAY); - }; - } - return renderTask.promise.then(function () { - if (annotationCanvasMap) { - rasterizeAnnotationLayer( - annotationLayerContext, - viewport, - data, - annotationCanvasMap, - page, - IMAGE_RESOURCES_PATH, - renderForms - ).then(() => { - completeRender(false); - }); - } else { - completeRender(false); - } - }); - }) - .catch(function (error) { - completeRender("render : " + error); - }); - }, - function (error) { - self._snapshot(task, "render : " + error); } - ); - } catch (e) { - failure = "page setup : " + this._exceptionToString(e); - this._snapshot(task, failure); - } + + task.pdfDoc = doc; + task.optionalContentConfigPromise = doc.getOptionalContentConfig(); + + if (task.optionalContent) { + const entries = Object.entries(task.optionalContent), + optionalContentConfig = await task.optionalContentConfigPromise; + for (const [id, visible] of entries) { + optionalContentConfig.setVisibility(id, visible); + } + } + + this._nextPage(task, failure); + }, + err => { + failure = "Loading PDF document: " + err; + this._nextPage(task, failure); + } + ); + return; + } catch (e) { + failure = "Loading PDF document: " + this._exceptionToString(e); } - }, + this._nextPage(task, failure); + }); + } - _clearCanvas: function Driver_clearCanvas() { - var ctx = this.canvas.getContext("2d", { alpha: false }); - ctx.beginPath(); - ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - }, + _cleanup() { + // Clear out all the stylesheets since a new one is created for each font. + while (document.styleSheets.length > 0) { + const styleSheet = document.styleSheets[0]; + while (styleSheet.cssRules.length > 0) { + styleSheet.deleteRule(0); + } + styleSheet.ownerNode.remove(); + } + const body = document.body; + while (body.lastChild !== this.end) { + body.lastChild.remove(); + } - _snapshot: function Driver_snapshot(task, failure) { - var self = this; - this._log("Snapshotting... "); + const destroyedPromises = []; + // Wipe out the link to the pdfdoc so it can be GC'ed. + for (let i = 0; i < this.manifest.length; i++) { + if (this.manifest[i].pdfDoc) { + destroyedPromises.push(this.manifest[i].pdfDoc.destroy()); + delete this.manifest[i].pdfDoc; + } + } + return Promise.all(destroyedPromises); + } + _exceptionToString(e) { + if (typeof e !== "object") { + return String(e); + } + if (!("message" in e)) { + return JSON.stringify(e); + } + return e.message + ("stack" in e ? " at " + e.stack.split("\n")[0] : ""); + } + + _getLastPageNumber(task) { + if (!task.pdfDoc) { + return task.firstPage || 1; + } + var lastPageNumber = task.lastPage || 0; + if (!lastPageNumber || lastPageNumber > task.pdfDoc.numPages) { + lastPageNumber = task.pdfDoc.numPages; + } + return lastPageNumber; + } + + _nextPage(task, loadError) { + var self = this; + var failure = loadError || ""; + var ctx; + + if (!task.pdfDoc) { var dataUrl = this.canvas.toDataURL("image/png"); this._sendResult(dataUrl, task, failure, function () { self._log( "done" + (failure ? " (failed !: " + failure + ")" : "") + "\n" ); - task.pageNum++; - self._nextPage(task); + self.currentTask++; + self._nextTask(); }); - }, + return; + } - _quit: function Driver_quit() { - this._log("Done !"); - this.end.textContent = "Tests finished. Close this window!"; + if (task.pageNum > this._getLastPageNumber(task)) { + if (++task.round < task.rounds) { + this._log(" Round " + (1 + task.round) + "\n"); + task.pageNum = task.firstPage || 1; + } else { + this.currentTask++; + this._nextTask(); + return; + } + } - // Send the quit request - var r = new XMLHttpRequest(); - r.open("POST", `/tellMeToQuit?browser=${escape(this.browser)}`, false); - r.onreadystatechange = function (e) { - if (r.readyState === 4) { - window.close(); - } - }; - r.send(null); - }, - - _info: function Driver_info(message) { - this._send( - "/info", - JSON.stringify({ - browser: this.browser, - message, - }) + if (task.skipPages && task.skipPages.includes(task.pageNum)) { + this._log( + " Skipping page " + task.pageNum + "/" + task.pdfDoc.numPages + "...\n" ); - }, + task.pageNum++; + this._nextPage(task); + return; + } - _log: function Driver_log(message) { - // Using insertAdjacentHTML yields a large performance gain and - // reduces runtime significantly. - if (this.output.insertAdjacentHTML) { - // eslint-disable-next-line no-unsanitized/method - this.output.insertAdjacentHTML("BeforeEnd", message); - } else { - this.output.textContent += message; - } - - if (message.lastIndexOf("\n") >= 0 && !this.disableScrolling.checked) { - // Scroll to the bottom of the page - this.output.scrollTop = this.output.scrollHeight; - } - }, - - _done: function Driver_done() { - if (this.inFlightRequests > 0) { - this.inflight.textContent = this.inFlightRequests; - setTimeout(this._done.bind(this), WAITING_TIME); - } else { - setTimeout(this._quit.bind(this), WAITING_TIME); - } - }, - - _sendResult: function Driver_sendResult(snapshot, task, failure, callback) { - var result = JSON.stringify({ - browser: this.browser, - id: task.id, - numPages: task.pdfDoc ? task.lastPage || task.pdfDoc.numPages : 0, - lastPageNum: this._getLastPageNumber(task), - failure, - file: task.file, - round: task.round, - page: task.pageNum, - snapshot, - stats: task.stats.times, - }); - this._send("/submit_task_results", result, callback); - }, - - _send: function Driver_send(url, message, callback) { - var self = this; - var r = new XMLHttpRequest(); - r.open("POST", url, true); - r.setRequestHeader("Content-Type", "application/json"); - r.onreadystatechange = function (e) { - if (r.readyState === 4) { - self.inFlightRequests--; - - // Retry until successful - if (r.status !== 200) { - setTimeout(function () { - self._send(url, message); + if (!failure) { + try { + this._log( + " Loading page " + task.pageNum + "/" + task.pdfDoc.numPages + "... " + ); + this.canvas.mozOpaque = true; + ctx = this.canvas.getContext("2d", { alpha: false }); + task.pdfDoc.getPage(task.pageNum).then( + function (page) { + var viewport = page.getViewport({ + scale: PixelsPerInch.PDF_TO_CSS_UNITS, }); - } - if (callback) { - callback(); - } - } - }; - this.inflight.textContent = this.inFlightRequests++; - r.send(message); - }, - }; + self.canvas.width = viewport.width; + self.canvas.height = viewport.height; + self._clearCanvas(); - return Driver; -})(); + // Initialize various `eq` test subtypes, see comment below. + var renderAnnotations = false, + renderForms = false, + renderPrint = false, + renderXfa = false, + annotationCanvasMap = null; + + if (task.annotationStorage) { + const entries = Object.entries(task.annotationStorage), + docAnnotationStorage = task.pdfDoc.annotationStorage; + for (const [key, value] of entries) { + docAnnotationStorage.setValue(key, value); + } + } + + var textLayerCanvas, annotationLayerCanvas; + var initPromise; + if (task.type === "text") { + // Using a dummy canvas for PDF context drawing operations + textLayerCanvas = self.textLayerCanvas; + if (!textLayerCanvas) { + textLayerCanvas = document.createElement("canvas"); + self.textLayerCanvas = textLayerCanvas; + } + textLayerCanvas.width = viewport.width; + textLayerCanvas.height = viewport.height; + var textLayerContext = textLayerCanvas.getContext("2d"); + textLayerContext.clearRect( + 0, + 0, + textLayerCanvas.width, + textLayerCanvas.height + ); + var enhanceText = !!task.enhance; + // The text builder will draw its content on the test canvas + initPromise = page + .getTextContent({ + normalizeWhitespace: true, + includeMarkedContent: true, + }) + .then(function (textContent) { + return rasterizeTextLayer( + textLayerContext, + viewport, + textContent, + enhanceText + ); + }); + } else { + textLayerCanvas = null; + // We fetch the `eq` specific test subtypes here, to avoid + // accidentally changing the behaviour for other types of tests. + renderAnnotations = !!task.annotations; + renderForms = !!task.forms; + renderPrint = !!task.print; + renderXfa = !!task.enableXfa; + + // Render the annotation layer if necessary. + if (renderAnnotations || renderForms || renderXfa) { + // Create a dummy canvas for the drawing operations. + annotationLayerCanvas = self.annotationLayerCanvas; + if (!annotationLayerCanvas) { + annotationLayerCanvas = document.createElement("canvas"); + self.annotationLayerCanvas = annotationLayerCanvas; + } + annotationLayerCanvas.width = viewport.width; + annotationLayerCanvas.height = viewport.height; + var annotationLayerContext = + annotationLayerCanvas.getContext("2d"); + annotationLayerContext.clearRect( + 0, + 0, + annotationLayerCanvas.width, + annotationLayerCanvas.height + ); + + if (!renderXfa) { + // The annotation builder will draw its content + // on the canvas. + initPromise = page.getAnnotations({ intent: "display" }); + annotationCanvasMap = new Map(); + } else { + initPromise = page.getXfa().then(function (xfa) { + return rasterizeXfaLayer( + annotationLayerContext, + viewport, + xfa, + task.fontRules, + task.pdfDoc.annotationStorage, + task.renderPrint + ); + }); + } + } else { + annotationLayerCanvas = null; + initPromise = Promise.resolve(); + } + } + var renderContext = { + canvasContext: ctx, + viewport, + optionalContentConfigPromise: task.optionalContentConfigPromise, + annotationCanvasMap, + }; + if (renderForms) { + renderContext.annotationMode = AnnotationMode.ENABLE_FORMS; + } else if (renderPrint) { + if (task.annotationStorage) { + renderContext.annotationMode = AnnotationMode.ENABLE_STORAGE; + } + renderContext.intent = "print"; + } + + var completeRender = function (error) { + // if text layer is present, compose it on top of the page + if (textLayerCanvas) { + ctx.save(); + ctx.globalCompositeOperation = "screen"; + ctx.fillStyle = "rgb(128, 255, 128)"; // making it green + ctx.fillRect(0, 0, viewport.width, viewport.height); + ctx.restore(); + ctx.drawImage(textLayerCanvas, 0, 0); + } + // If we have annotation layer, compose it on top of the page. + if (annotationLayerCanvas) { + ctx.drawImage(annotationLayerCanvas, 0, 0); + } + if (page.stats) { + // Get the page stats *before* running cleanup. + task.stats = page.stats; + } + page.cleanup(/* resetStats = */ true); + self._snapshot(task, error); + }; + initPromise + .then(function (data) { + const renderTask = page.render(renderContext); + + if (task.renderTaskOnContinue) { + renderTask.onContinue = function (cont) { + // Slightly delay the continued rendering. + setTimeout(cont, RENDER_TASK_ON_CONTINUE_DELAY); + }; + } + return renderTask.promise.then(function () { + if (annotationCanvasMap) { + rasterizeAnnotationLayer( + annotationLayerContext, + viewport, + data, + annotationCanvasMap, + page, + IMAGE_RESOURCES_PATH, + renderForms + ).then(() => { + completeRender(false); + }); + } else { + completeRender(false); + } + }); + }) + .catch(function (error) { + completeRender("render : " + error); + }); + }, + function (error) { + self._snapshot(task, "render : " + error); + } + ); + } catch (e) { + failure = "page setup : " + this._exceptionToString(e); + this._snapshot(task, failure); + } + } + } + + _clearCanvas() { + var ctx = this.canvas.getContext("2d", { alpha: false }); + ctx.beginPath(); + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + } + + _snapshot(task, failure) { + var self = this; + this._log("Snapshotting... "); + + var dataUrl = this.canvas.toDataURL("image/png"); + this._sendResult(dataUrl, task, failure, function () { + self._log( + "done" + (failure ? " (failed !: " + failure + ")" : "") + "\n" + ); + task.pageNum++; + self._nextPage(task); + }); + } + + _quit() { + this._log("Done !"); + this.end.textContent = "Tests finished. Close this window!"; + + // Send the quit request + var r = new XMLHttpRequest(); + r.open("POST", `/tellMeToQuit?browser=${escape(this.browser)}`, false); + r.onreadystatechange = function (e) { + if (r.readyState === 4) { + window.close(); + } + }; + r.send(null); + } + + _info(message) { + this._send( + "/info", + JSON.stringify({ + browser: this.browser, + message, + }) + ); + } + + _log(message) { + // Using insertAdjacentHTML yields a large performance gain and + // reduces runtime significantly. + if (this.output.insertAdjacentHTML) { + // eslint-disable-next-line no-unsanitized/method + this.output.insertAdjacentHTML("BeforeEnd", message); + } else { + this.output.textContent += message; + } + + if (message.lastIndexOf("\n") >= 0 && !this.disableScrolling.checked) { + // Scroll to the bottom of the page + this.output.scrollTop = this.output.scrollHeight; + } + } + + _done() { + if (this.inFlightRequests > 0) { + this.inflight.textContent = this.inFlightRequests; + setTimeout(this._done.bind(this), WAITING_TIME); + } else { + setTimeout(this._quit.bind(this), WAITING_TIME); + } + } + + _sendResult(snapshot, task, failure, callback) { + var result = JSON.stringify({ + browser: this.browser, + id: task.id, + numPages: task.pdfDoc ? task.lastPage || task.pdfDoc.numPages : 0, + lastPageNum: this._getLastPageNumber(task), + failure, + file: task.file, + round: task.round, + page: task.pageNum, + snapshot, + stats: task.stats.times, + }); + this._send("/submit_task_results", result, callback); + } + + _send(url, message, callback) { + var self = this; + var r = new XMLHttpRequest(); + r.open("POST", url, true); + r.setRequestHeader("Content-Type", "application/json"); + r.onreadystatechange = function (e) { + if (r.readyState === 4) { + self.inFlightRequests--; + + // Retry until successful + if (r.status !== 200) { + setTimeout(function () { + self._send(url, message); + }); + } + if (callback) { + callback(); + } + } + }; + this.inflight.textContent = this.inFlightRequests++; + r.send(message); + } +} From 1d1f713bfc793baa8d28e9d48626a18fbe8b9939 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 5 Dec 2021 13:46:41 +0100 Subject: [PATCH 2/7] Inline `loadStyles` calls in the rasterization classes in `test/driver.js` The wrapper functions in this case only really added indirection, so this commit simplifies the code a bit. --- test/driver.js | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/test/driver.js b/test/driver.js index db31b7221..b9d21e9bc 100644 --- a/test/driver.js +++ b/test/driver.js @@ -154,10 +154,6 @@ var rasterizeTextLayer = (function rasterizeTextLayerClosure() { }, }; - function getTextLayerStyle() { - return loadStyles(styles); - } - // eslint-disable-next-line no-shadow function rasterizeTextLayer( ctx, @@ -180,7 +176,7 @@ var rasterizeTextLayer = (function rasterizeTextLayerClosure() { foreignObject.setAttribute("width", viewport.width + "px"); foreignObject.setAttribute("height", viewport.height + "px"); var style = document.createElement("style"); - var stylePromise = getTextLayerStyle(); + var stylePromise = loadStyles(styles); foreignObject.appendChild(style); var div = document.createElement("div"); div.className = "textLayer"; @@ -237,10 +233,6 @@ var rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() { }, }; - function getAnnotationLayerStyle() { - return loadStyles(styles); - } - // eslint-disable-next-line no-shadow function rasterizeAnnotationLayer( ctx, @@ -264,7 +256,7 @@ var rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() { foreignObject.setAttribute("width", viewport.width + "px"); foreignObject.setAttribute("height", viewport.height + "px"); var style = document.createElement("style"); - var stylePromise = getAnnotationLayerStyle(); + var stylePromise = loadStyles(styles); foreignObject.appendChild(style); var div = document.createElement("div"); div.className = "annotationLayer"; @@ -322,10 +314,6 @@ var rasterizeXfaLayer = (function rasterizeXfaLayerClosure() { }, }; - function getXfaLayerStyle() { - return loadStyles(styles); - } - // eslint-disable-next-line no-shadow function rasterizeXfaLayer( ctx, @@ -349,7 +337,7 @@ var rasterizeXfaLayer = (function rasterizeXfaLayerClosure() { foreignObject.setAttribute("width", viewport.width + "px"); foreignObject.setAttribute("height", viewport.height + "px"); const style = document.createElement("style"); - const stylePromise = getXfaLayerStyle(); + const stylePromise = loadStyles(styles); foreignObject.appendChild(style); const div = document.createElement("div"); foreignObject.appendChild(div); From 13786ef80602419230f06f8b0f6399d81a0b0a5e Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sat, 4 Dec 2021 15:45:47 +0100 Subject: [PATCH 3/7] Use arrow functions instead of `self` variables in `test/driver.js` --- test/driver.js | 72 ++++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/test/driver.js b/test/driver.js index b9d21e9bc..5750a424e 100644 --- a/test/driver.js +++ b/test/driver.js @@ -417,9 +417,8 @@ class Driver { } run() { - var self = this; - window.onerror = function (message, source, line, column, error) { - self._info( + window.onerror = (message, source, line, column, error) => { + this._info( "Error: " + message + " Script: " + @@ -438,23 +437,23 @@ class Driver { var r = new XMLHttpRequest(); r.open("GET", this.manifestFile, false); - r.onreadystatechange = function () { + r.onreadystatechange = () => { if (r.readyState === 4) { - self._log("done\n"); - self.manifest = JSON.parse(r.responseText); - if (self.testFilter?.length || self.xfaOnly) { - self.manifest = self.manifest.filter(function (item) { - if (self.testFilter.includes(item.id)) { + this._log("done\n"); + this.manifest = JSON.parse(r.responseText); + if (this.testFilter?.length || this.xfaOnly) { + this.manifest = this.manifest.filter(item => { + if (this.testFilter.includes(item.id)) { return true; } - if (self.xfaOnly && item.enableXfa) { + if (this.xfaOnly && item.enableXfa) { return true; } return false; }); } - self.currentTask = 0; - self._nextTask(); + this.currentTask = 0; + this._nextTask(); } }; if (this.delay > 0) { @@ -624,18 +623,17 @@ class Driver { } _nextPage(task, loadError) { - var self = this; var failure = loadError || ""; var ctx; if (!task.pdfDoc) { var dataUrl = this.canvas.toDataURL("image/png"); - this._sendResult(dataUrl, task, failure, function () { - self._log( + this._sendResult(dataUrl, task, failure, () => { + this._log( "done" + (failure ? " (failed !: " + failure + ")" : "") + "\n" ); - self.currentTask++; - self._nextTask(); + this.currentTask++; + this._nextTask(); }); return; } @@ -668,13 +666,13 @@ class Driver { this.canvas.mozOpaque = true; ctx = this.canvas.getContext("2d", { alpha: false }); task.pdfDoc.getPage(task.pageNum).then( - function (page) { + page => { var viewport = page.getViewport({ scale: PixelsPerInch.PDF_TO_CSS_UNITS, }); - self.canvas.width = viewport.width; - self.canvas.height = viewport.height; - self._clearCanvas(); + this.canvas.width = viewport.width; + this.canvas.height = viewport.height; + this._clearCanvas(); // Initialize various `eq` test subtypes, see comment below. var renderAnnotations = false, @@ -695,10 +693,10 @@ class Driver { var initPromise; if (task.type === "text") { // Using a dummy canvas for PDF context drawing operations - textLayerCanvas = self.textLayerCanvas; + textLayerCanvas = this.textLayerCanvas; if (!textLayerCanvas) { textLayerCanvas = document.createElement("canvas"); - self.textLayerCanvas = textLayerCanvas; + this.textLayerCanvas = textLayerCanvas; } textLayerCanvas.width = viewport.width; textLayerCanvas.height = viewport.height; @@ -736,10 +734,10 @@ class Driver { // Render the annotation layer if necessary. if (renderAnnotations || renderForms || renderXfa) { // Create a dummy canvas for the drawing operations. - annotationLayerCanvas = self.annotationLayerCanvas; + annotationLayerCanvas = this.annotationLayerCanvas; if (!annotationLayerCanvas) { annotationLayerCanvas = document.createElement("canvas"); - self.annotationLayerCanvas = annotationLayerCanvas; + this.annotationLayerCanvas = annotationLayerCanvas; } annotationLayerCanvas.width = viewport.width; annotationLayerCanvas.height = viewport.height; @@ -789,7 +787,7 @@ class Driver { renderContext.intent = "print"; } - var completeRender = function (error) { + var completeRender = error => { // if text layer is present, compose it on top of the page if (textLayerCanvas) { ctx.save(); @@ -808,7 +806,7 @@ class Driver { task.stats = page.stats; } page.cleanup(/* resetStats = */ true); - self._snapshot(task, error); + this._snapshot(task, error); }; initPromise .then(function (data) { @@ -842,8 +840,8 @@ class Driver { completeRender("render : " + error); }); }, - function (error) { - self._snapshot(task, "render : " + error); + error => { + this._snapshot(task, "render : " + error); } ); } catch (e) { @@ -860,16 +858,15 @@ class Driver { } _snapshot(task, failure) { - var self = this; this._log("Snapshotting... "); var dataUrl = this.canvas.toDataURL("image/png"); - this._sendResult(dataUrl, task, failure, function () { - self._log( + this._sendResult(dataUrl, task, failure, () => { + this._log( "done" + (failure ? " (failed !: " + failure + ")" : "") + "\n" ); task.pageNum++; - self._nextPage(task); + this._nextPage(task); }); } @@ -940,18 +937,17 @@ class Driver { } _send(url, message, callback) { - var self = this; var r = new XMLHttpRequest(); r.open("POST", url, true); r.setRequestHeader("Content-Type", "application/json"); - r.onreadystatechange = function (e) { + r.onreadystatechange = e => { if (r.readyState === 4) { - self.inFlightRequests--; + this.inFlightRequests--; // Retry until successful if (r.status !== 200) { - setTimeout(function () { - self._send(url, message); + setTimeout(() => { + this._send(url, message); }); } if (callback) { From 5fd4276dcf7d5d9acc3afd91f67bb0de9fe3e81f Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 5 Dec 2021 14:04:23 +0100 Subject: [PATCH 4/7] Use async/await in the rasterization classes in `test/driver.js` This is achieved by letting the `writeSVG` function return a promise so we don't need callback passing anymore. --- test/driver.js | 161 +++++++++++++++++++++++-------------------------- 1 file changed, 77 insertions(+), 84 deletions(-) diff --git a/test/driver.js b/test/driver.js index 5750a424e..d14994973 100644 --- a/test/driver.js +++ b/test/driver.js @@ -60,20 +60,22 @@ function loadStyles(styles) { return Promise.all(styles.map(style => style.promise)); } -function writeSVG(svgElement, ctx, resolve, reject) { +function writeSVG(svgElement, ctx) { // We need to have UTF-8 encoded XML. const svg_xml = unescape( encodeURIComponent(new XMLSerializer().serializeToString(svgElement)) ); - const img = new Image(); - img.src = "data:image/svg+xml;base64," + btoa(svg_xml); - img.onload = function () { - ctx.drawImage(img, 0, 0); - resolve(); - }; - img.onerror = function (e) { - reject(new Error("Error rasterizing text layer " + e)); - }; + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = "data:image/svg+xml;base64," + btoa(svg_xml); + img.onload = function () { + ctx.drawImage(img, 0, 0); + resolve(); + }; + img.onerror = function (e) { + reject(new Error(`Error rasterizing SVG: ${e}`)); + }; + }); } function inlineImages(images) { @@ -155,13 +157,13 @@ var rasterizeTextLayer = (function rasterizeTextLayerClosure() { }; // eslint-disable-next-line no-shadow - function rasterizeTextLayer( + async function rasterizeTextLayer( ctx, viewport, textContent, enhanceTextSelection ) { - return new Promise(function (resolve, reject) { + try { // Building SVG with size of the viewport. var svg = document.createElementNS(SVG_NS, "svg:svg"); svg.setAttribute("width", viewport.width + "px"); @@ -182,28 +184,25 @@ var rasterizeTextLayer = (function rasterizeTextLayerClosure() { div.className = "textLayer"; foreignObject.appendChild(div); - stylePromise - .then(async ([cssRules]) => { - style.textContent = cssRules; + const [cssRules] = await stylePromise; + style.textContent = cssRules; - // Rendering text layer as HTML. - var task = renderTextLayer({ - textContent, - container: div, - viewport, - enhanceTextSelection, - }); - await task.promise; + // Rendering text layer as HTML. + var task = renderTextLayer({ + textContent, + container: div, + viewport, + enhanceTextSelection, + }); + await task.promise; - task.expandTextDivs(true); - svg.appendChild(foreignObject); + task.expandTextDivs(true); + svg.appendChild(foreignObject); - writeSVG(svg, ctx, resolve, reject); - }) - .catch(reason => { - reject(new Error(`rasterizeTextLayer: "${reason?.message}".`)); - }); - }); + await writeSVG(svg, ctx); + } catch (reason) { + throw new Error(`rasterizeTextLayer: "${reason?.message}".`); + } } return rasterizeTextLayer; @@ -234,7 +233,7 @@ var rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() { }; // eslint-disable-next-line no-shadow - function rasterizeAnnotationLayer( + async function rasterizeAnnotationLayer( ctx, viewport, annotations, @@ -243,7 +242,7 @@ var rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() { imageResourcesPath, renderForms = false ) { - return new Promise(function (resolve, reject) { + try { // Building SVG with size of the viewport. var svg = document.createElementNS(SVG_NS, "svg:svg"); svg.setAttribute("width", viewport.width + "px"); @@ -262,38 +261,35 @@ var rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() { div.className = "annotationLayer"; // Rendering annotation layer as HTML. - stylePromise - .then(async ([common, overrides]) => { - style.textContent = common + "\n" + overrides; + const [common, overrides] = await stylePromise; + style.textContent = common + "\n" + overrides; - var annotation_viewport = viewport.clone({ dontFlip: true }); - const annotationImageMap = await convertCanvasesToImages( - annotationCanvasMap - ); + var annotation_viewport = viewport.clone({ dontFlip: true }); + const annotationImageMap = await convertCanvasesToImages( + annotationCanvasMap + ); - var parameters = { - viewport: annotation_viewport, - div, - annotations, - page, - linkService: new SimpleLinkService(), - imageResourcesPath, - renderForms, - annotationCanvasMap: annotationImageMap, - }; - AnnotationLayer.render(parameters); + var parameters = { + viewport: annotation_viewport, + div, + annotations, + page, + linkService: new SimpleLinkService(), + imageResourcesPath, + renderForms, + annotationCanvasMap: annotationImageMap, + }; + AnnotationLayer.render(parameters); - // Inline SVG images from text annotations. - await resolveImages(div); - foreignObject.appendChild(div); - svg.appendChild(foreignObject); + // Inline SVG images from text annotations. + await resolveImages(div); + foreignObject.appendChild(div); + svg.appendChild(foreignObject); - writeSVG(svg, ctx, resolve, reject); - }) - .catch(reason => { - reject(new Error(`rasterizeAnnotationLayer: "${reason?.message}".`)); - }); - }); + await writeSVG(svg, ctx); + } catch (reason) { + throw new Error(`rasterizeAnnotationLayer: "${reason?.message}".`); + } } return rasterizeAnnotationLayer; @@ -315,7 +311,7 @@ var rasterizeXfaLayer = (function rasterizeXfaLayerClosure() { }; // eslint-disable-next-line no-shadow - function rasterizeXfaLayer( + async function rasterizeXfaLayer( ctx, viewport, xfa, @@ -323,7 +319,7 @@ var rasterizeXfaLayer = (function rasterizeXfaLayerClosure() { annotationStorage, isPrint ) { - return new Promise(function (resolve, reject) { + try { // Building SVG with size of the viewport. const svg = document.createElementNS(SVG_NS, "svg:svg"); svg.setAttribute("width", viewport.width + "px"); @@ -342,30 +338,27 @@ var rasterizeXfaLayer = (function rasterizeXfaLayerClosure() { const div = document.createElement("div"); foreignObject.appendChild(div); - stylePromise - .then(async ([common, overrides]) => { - style.textContent = fontRules + "\n" + common + "\n" + overrides; + const [common, overrides] = await stylePromise; + style.textContent = fontRules + "\n" + common + "\n" + overrides; - XfaLayer.render({ - xfa, - div, - viewport: viewport.clone({ dontFlip: true }), - annotationStorage, - linkService: new SimpleLinkService(), - intent: isPrint ? "print" : "display", - }); + XfaLayer.render({ + xfa, + div, + viewport: viewport.clone({ dontFlip: true }), + annotationStorage, + linkService: new SimpleLinkService(), + intent: isPrint ? "print" : "display", + }); - // Some unsupported type of images (e.g. tiff) - // lead to errors. - await resolveImages(div, /* silentErrors = */ true); - svg.appendChild(foreignObject); + // Some unsupported type of images (e.g. tiff) + // lead to errors. + await resolveImages(div, /* silentErrors = */ true); + svg.appendChild(foreignObject); - writeSVG(svg, ctx, resolve, reject); - }) - .catch(reason => { - reject(new Error(`rasterizeXfaLayer: "${reason?.message}".`)); - }); - }); + await writeSVG(svg, ctx); + } catch (reason) { + throw new Error(`rasterizeXfaLayer: "${reason?.message}".`); + } } return rasterizeXfaLayer; From 33dc0628a01dc2f81a7c41140569c77d94c0d220 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 5 Dec 2021 14:18:06 +0100 Subject: [PATCH 5/7] Enable the `no-var` linting rule in `test/driver.js` This is done automatically with the `gulp lint --fix` command with the only exception of the `annotationLayerContext` variable. --- test/driver.js | 80 ++++++++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/test/driver.js b/test/driver.js index d14994973..d929f4f8d 100644 --- a/test/driver.js +++ b/test/driver.js @@ -12,7 +12,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* eslint-disable no-var */ /* globals pdfjsLib, pdfjsViewer */ "use strict"; @@ -148,7 +147,7 @@ async function resolveImages(node, silentErrors = false) { /** * @class */ -var rasterizeTextLayer = (function rasterizeTextLayerClosure() { +const rasterizeTextLayer = (function rasterizeTextLayerClosure() { const styles = { common: { file: "./text_layer_test.css", @@ -165,22 +164,25 @@ var rasterizeTextLayer = (function rasterizeTextLayerClosure() { ) { try { // Building SVG with size of the viewport. - var svg = document.createElementNS(SVG_NS, "svg:svg"); + const svg = document.createElementNS(SVG_NS, "svg:svg"); svg.setAttribute("width", viewport.width + "px"); svg.setAttribute("height", viewport.height + "px"); // items are transformed to have 1px font size svg.setAttribute("font-size", 1); // Adding element to host our HTML (style + text layer div). - var foreignObject = document.createElementNS(SVG_NS, "svg:foreignObject"); + const foreignObject = document.createElementNS( + SVG_NS, + "svg:foreignObject" + ); foreignObject.setAttribute("x", "0"); foreignObject.setAttribute("y", "0"); foreignObject.setAttribute("width", viewport.width + "px"); foreignObject.setAttribute("height", viewport.height + "px"); - var style = document.createElement("style"); - var stylePromise = loadStyles(styles); + const style = document.createElement("style"); + const stylePromise = loadStyles(styles); foreignObject.appendChild(style); - var div = document.createElement("div"); + const div = document.createElement("div"); div.className = "textLayer"; foreignObject.appendChild(div); @@ -188,7 +190,7 @@ var rasterizeTextLayer = (function rasterizeTextLayerClosure() { style.textContent = cssRules; // Rendering text layer as HTML. - var task = renderTextLayer({ + const task = renderTextLayer({ textContent, container: div, viewport, @@ -211,7 +213,7 @@ var rasterizeTextLayer = (function rasterizeTextLayerClosure() { /** * @class */ -var rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() { +const rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() { /** * For the reference tests, the entire annotation layer must be visible. To * achieve this, we load the common styles as used by the viewer and extend @@ -244,32 +246,35 @@ var rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() { ) { try { // Building SVG with size of the viewport. - var svg = document.createElementNS(SVG_NS, "svg:svg"); + const svg = document.createElementNS(SVG_NS, "svg:svg"); svg.setAttribute("width", viewport.width + "px"); svg.setAttribute("height", viewport.height + "px"); // Adding element to host our HTML (style + annotation layer div). - var foreignObject = document.createElementNS(SVG_NS, "svg:foreignObject"); + const foreignObject = document.createElementNS( + SVG_NS, + "svg:foreignObject" + ); foreignObject.setAttribute("x", "0"); foreignObject.setAttribute("y", "0"); foreignObject.setAttribute("width", viewport.width + "px"); foreignObject.setAttribute("height", viewport.height + "px"); - var style = document.createElement("style"); - var stylePromise = loadStyles(styles); + const style = document.createElement("style"); + const stylePromise = loadStyles(styles); foreignObject.appendChild(style); - var div = document.createElement("div"); + const div = document.createElement("div"); div.className = "annotationLayer"; // Rendering annotation layer as HTML. const [common, overrides] = await stylePromise; style.textContent = common + "\n" + overrides; - var annotation_viewport = viewport.clone({ dontFlip: true }); + const annotation_viewport = viewport.clone({ dontFlip: true }); const annotationImageMap = await convertCanvasesToImages( annotationCanvasMap ); - var parameters = { + const parameters = { viewport: annotation_viewport, div, annotations, @@ -298,7 +303,7 @@ var rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() { /** * @class */ -var rasterizeXfaLayer = (function rasterizeXfaLayerClosure() { +const rasterizeXfaLayer = (function rasterizeXfaLayerClosure() { const styles = { common: { file: "../web/xfa_layer_builder.css", @@ -390,7 +395,7 @@ class Driver { this.end = options.end; // Set parameters from the query string - var parameters = this._getQueryStringParameters(); + const parameters = this._getQueryStringParameters(); this.browser = parameters.browser; this.manifestFile = parameters.manifestFile; this.delay = parameters.delay | 0 || 0; @@ -428,7 +433,7 @@ class Driver { this._log(`Harness thinks this browser is ${this.browser}\n`); this._log('Fetching manifest "' + this.manifestFile + '"... '); - var r = new XMLHttpRequest(); + const r = new XMLHttpRequest(); r.open("GET", this.manifestFile, false); r.onreadystatechange = () => { if (r.readyState === 4) { @@ -608,7 +613,7 @@ class Driver { if (!task.pdfDoc) { return task.firstPage || 1; } - var lastPageNumber = task.lastPage || 0; + let lastPageNumber = task.lastPage || 0; if (!lastPageNumber || lastPageNumber > task.pdfDoc.numPages) { lastPageNumber = task.pdfDoc.numPages; } @@ -616,11 +621,11 @@ class Driver { } _nextPage(task, loadError) { - var failure = loadError || ""; - var ctx; + let failure = loadError || ""; + let ctx; if (!task.pdfDoc) { - var dataUrl = this.canvas.toDataURL("image/png"); + const dataUrl = this.canvas.toDataURL("image/png"); this._sendResult(dataUrl, task, failure, () => { this._log( "done" + (failure ? " (failed !: " + failure + ")" : "") + "\n" @@ -660,7 +665,7 @@ class Driver { ctx = this.canvas.getContext("2d", { alpha: false }); task.pdfDoc.getPage(task.pageNum).then( page => { - var viewport = page.getViewport({ + const viewport = page.getViewport({ scale: PixelsPerInch.PDF_TO_CSS_UNITS, }); this.canvas.width = viewport.width; @@ -668,7 +673,7 @@ class Driver { this._clearCanvas(); // Initialize various `eq` test subtypes, see comment below. - var renderAnnotations = false, + let renderAnnotations = false, renderForms = false, renderPrint = false, renderXfa = false, @@ -682,8 +687,8 @@ class Driver { } } - var textLayerCanvas, annotationLayerCanvas; - var initPromise; + let textLayerCanvas, annotationLayerCanvas, annotationLayerContext; + let initPromise; if (task.type === "text") { // Using a dummy canvas for PDF context drawing operations textLayerCanvas = this.textLayerCanvas; @@ -693,14 +698,14 @@ class Driver { } textLayerCanvas.width = viewport.width; textLayerCanvas.height = viewport.height; - var textLayerContext = textLayerCanvas.getContext("2d"); + const textLayerContext = textLayerCanvas.getContext("2d"); textLayerContext.clearRect( 0, 0, textLayerCanvas.width, textLayerCanvas.height ); - var enhanceText = !!task.enhance; + const enhanceText = !!task.enhance; // The text builder will draw its content on the test canvas initPromise = page .getTextContent({ @@ -734,8 +739,7 @@ class Driver { } annotationLayerCanvas.width = viewport.width; annotationLayerCanvas.height = viewport.height; - var annotationLayerContext = - annotationLayerCanvas.getContext("2d"); + annotationLayerContext = annotationLayerCanvas.getContext("2d"); annotationLayerContext.clearRect( 0, 0, @@ -765,7 +769,7 @@ class Driver { initPromise = Promise.resolve(); } } - var renderContext = { + const renderContext = { canvasContext: ctx, viewport, optionalContentConfigPromise: task.optionalContentConfigPromise, @@ -780,7 +784,7 @@ class Driver { renderContext.intent = "print"; } - var completeRender = error => { + const completeRender = error => { // if text layer is present, compose it on top of the page if (textLayerCanvas) { ctx.save(); @@ -845,7 +849,7 @@ class Driver { } _clearCanvas() { - var ctx = this.canvas.getContext("2d", { alpha: false }); + const ctx = this.canvas.getContext("2d", { alpha: false }); ctx.beginPath(); ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); } @@ -853,7 +857,7 @@ class Driver { _snapshot(task, failure) { this._log("Snapshotting... "); - var dataUrl = this.canvas.toDataURL("image/png"); + const dataUrl = this.canvas.toDataURL("image/png"); this._sendResult(dataUrl, task, failure, () => { this._log( "done" + (failure ? " (failed !: " + failure + ")" : "") + "\n" @@ -868,7 +872,7 @@ class Driver { this.end.textContent = "Tests finished. Close this window!"; // Send the quit request - var r = new XMLHttpRequest(); + const r = new XMLHttpRequest(); r.open("POST", `/tellMeToQuit?browser=${escape(this.browser)}`, false); r.onreadystatechange = function (e) { if (r.readyState === 4) { @@ -914,7 +918,7 @@ class Driver { } _sendResult(snapshot, task, failure, callback) { - var result = JSON.stringify({ + const result = JSON.stringify({ browser: this.browser, id: task.id, numPages: task.pdfDoc ? task.lastPage || task.pdfDoc.numPages : 0, @@ -930,7 +934,7 @@ class Driver { } _send(url, message, callback) { - var r = new XMLHttpRequest(); + const r = new XMLHttpRequest(); r.open("POST", url, true); r.setRequestHeader("Content-Type", "application/json"); r.onreadystatechange = e => { From 03506f25c0ca8fc81e36bfe584e9359e566b82db Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 5 Dec 2021 14:57:01 +0100 Subject: [PATCH 6/7] Move the rasterization logic into one single class This refactoring ensures that we can get rid of the closures and encapsulate the logic in a nicer way with e.g., getters for the style promises. --- test/driver.js | 228 +++++++++++++++++++++---------------------------- 1 file changed, 95 insertions(+), 133 deletions(-) diff --git a/test/driver.js b/test/driver.js index d929f4f8d..13b648560 100644 --- a/test/driver.js +++ b/test/driver.js @@ -23,6 +23,7 @@ const { GlobalWorkerOptions, PixelsPerInch, renderTextLayer, + shadow, XfaLayer, } = pdfjsLib; const { SimpleLinkService } = pdfjsViewer; @@ -37,26 +38,25 @@ const RENDER_TASK_ON_CONTINUE_DELAY = 5; // ms const SVG_NS = "http://www.w3.org/2000/svg"; function loadStyles(styles) { - styles = Object.values(styles); - if (styles.every(style => style.promise)) { - return Promise.all(styles.map(style => style.promise)); + const promises = []; + + for (const file of styles) { + promises.push( + new Promise(function (resolve, reject) { + const xhr = new XMLHttpRequest(); + xhr.open("GET", file); + xhr.onload = function () { + resolve(xhr.responseText); + }; + xhr.onerror = function (e) { + reject(new Error(`Error fetching style (${file}): ${e}`)); + }; + xhr.send(null); + }) + ); } - for (const style of styles) { - style.promise = new Promise(function (resolve, reject) { - const xhr = new XMLHttpRequest(); - xhr.open("GET", style.file); - xhr.onload = function () { - resolve(xhr.responseText); - }; - xhr.onerror = function (e) { - reject(new Error(`Error fetching style (${style.file}): ${e}`)); - }; - xhr.send(null); - }); - } - - return Promise.all(styles.map(style => style.promise)); + return Promise.all(promises); } function writeSVG(svgElement, ctx) { @@ -144,98 +144,38 @@ async function resolveImages(node, silentErrors = false) { await Promise.all(loadedPromises); } -/** - * @class - */ -const rasterizeTextLayer = (function rasterizeTextLayerClosure() { - const styles = { - common: { - file: "./text_layer_test.css", - promise: null, - }, - }; - - // eslint-disable-next-line no-shadow - async function rasterizeTextLayer( - ctx, - viewport, - textContent, - enhanceTextSelection - ) { - try { - // Building SVG with size of the viewport. - const svg = document.createElementNS(SVG_NS, "svg:svg"); - svg.setAttribute("width", viewport.width + "px"); - svg.setAttribute("height", viewport.height + "px"); - // items are transformed to have 1px font size - svg.setAttribute("font-size", 1); - - // Adding element to host our HTML (style + text layer div). - const foreignObject = document.createElementNS( - SVG_NS, - "svg:foreignObject" - ); - foreignObject.setAttribute("x", "0"); - foreignObject.setAttribute("y", "0"); - foreignObject.setAttribute("width", viewport.width + "px"); - foreignObject.setAttribute("height", viewport.height + "px"); - const style = document.createElement("style"); - const stylePromise = loadStyles(styles); - foreignObject.appendChild(style); - const div = document.createElement("div"); - div.className = "textLayer"; - foreignObject.appendChild(div); - - const [cssRules] = await stylePromise; - style.textContent = cssRules; - - // Rendering text layer as HTML. - const task = renderTextLayer({ - textContent, - container: div, - viewport, - enhanceTextSelection, - }); - await task.promise; - - task.expandTextDivs(true); - svg.appendChild(foreignObject); - - await writeSVG(svg, ctx); - } catch (reason) { - throw new Error(`rasterizeTextLayer: "${reason?.message}".`); - } - } - - return rasterizeTextLayer; -})(); - -/** - * @class - */ -const rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() { +class Rasterize { /** - * For the reference tests, the entire annotation layer must be visible. To - * achieve this, we load the common styles as used by the viewer and extend - * them with a set of overrides to make all elements visible. + * For the reference tests, the full content of the various layers must be + * visible. To achieve this, we load the common styles as used by the viewer + * and extend them with a set of overrides to make all elements visible. * * Note that we cannot simply use `@import` to import the common styles in * the overrides file because the browser does not resolve that when the * styles are inserted via XHR. Therefore, we load and combine them here. */ - const styles = { - common: { - file: "../web/annotation_layer_builder.css", - promise: null, - }, - overrides: { - file: "./annotation_layer_builder_overrides.css", - promise: null, - }, - }; + static get annotationStylePromise() { + const styles = [ + "../web/annotation_layer_builder.css", + "./annotation_layer_builder_overrides.css", + ]; + return shadow(this, "annotationStylePromise", loadStyles(styles)); + } - // eslint-disable-next-line no-shadow - async function rasterizeAnnotationLayer( + static get textStylePromise() { + const styles = ["./text_layer_test.css"]; + return shadow(this, "textStylePromise", loadStyles(styles)); + } + + static get xfaStylePromise() { + const styles = [ + "../web/xfa_layer_builder.css", + "./xfa_layer_builder_overrides.css", + ]; + return shadow(this, "xfaStylePromise", loadStyles(styles)); + } + + static async annotationLayer( ctx, viewport, annotations, @@ -260,13 +200,12 @@ const rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() { foreignObject.setAttribute("width", viewport.width + "px"); foreignObject.setAttribute("height", viewport.height + "px"); const style = document.createElement("style"); - const stylePromise = loadStyles(styles); foreignObject.appendChild(style); const div = document.createElement("div"); div.className = "annotationLayer"; // Rendering annotation layer as HTML. - const [common, overrides] = await stylePromise; + const [common, overrides] = await this.annotationStylePromise; style.textContent = common + "\n" + overrides; const annotation_viewport = viewport.clone({ dontFlip: true }); @@ -293,30 +232,56 @@ const rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() { await writeSVG(svg, ctx); } catch (reason) { - throw new Error(`rasterizeAnnotationLayer: "${reason?.message}".`); + throw new Error(`Rasterize.annotationLayer: "${reason?.message}".`); } } - return rasterizeAnnotationLayer; -})(); + static async textLayer(ctx, viewport, textContent, enhanceTextSelection) { + try { + // Building SVG with size of the viewport. + const svg = document.createElementNS(SVG_NS, "svg:svg"); + svg.setAttribute("width", viewport.width + "px"); + svg.setAttribute("height", viewport.height + "px"); + // items are transformed to have 1px font size + svg.setAttribute("font-size", 1); -/** - * @class - */ -const rasterizeXfaLayer = (function rasterizeXfaLayerClosure() { - const styles = { - common: { - file: "../web/xfa_layer_builder.css", - promise: null, - }, - overrides: { - file: "./xfa_layer_builder_overrides.css", - promise: null, - }, - }; + // Adding element to host our HTML (style + text layer div). + const foreignObject = document.createElementNS( + SVG_NS, + "svg:foreignObject" + ); + foreignObject.setAttribute("x", "0"); + foreignObject.setAttribute("y", "0"); + foreignObject.setAttribute("width", viewport.width + "px"); + foreignObject.setAttribute("height", viewport.height + "px"); + const style = document.createElement("style"); + foreignObject.appendChild(style); + const div = document.createElement("div"); + div.className = "textLayer"; + foreignObject.appendChild(div); - // eslint-disable-next-line no-shadow - async function rasterizeXfaLayer( + const [cssRules] = await this.textStylePromise; + style.textContent = cssRules; + + // Rendering text layer as HTML. + const task = renderTextLayer({ + textContent, + container: div, + viewport, + enhanceTextSelection, + }); + await task.promise; + + task.expandTextDivs(true); + svg.appendChild(foreignObject); + + await writeSVG(svg, ctx); + } catch (reason) { + throw new Error(`Rasterize.textLayer: "${reason?.message}".`); + } + } + + static async xfaLayer( ctx, viewport, xfa, @@ -338,12 +303,11 @@ const rasterizeXfaLayer = (function rasterizeXfaLayerClosure() { foreignObject.setAttribute("width", viewport.width + "px"); foreignObject.setAttribute("height", viewport.height + "px"); const style = document.createElement("style"); - const stylePromise = loadStyles(styles); foreignObject.appendChild(style); const div = document.createElement("div"); foreignObject.appendChild(div); - const [common, overrides] = await stylePromise; + const [common, overrides] = await this.xfaStylePromise; style.textContent = fontRules + "\n" + common + "\n" + overrides; XfaLayer.render({ @@ -362,12 +326,10 @@ const rasterizeXfaLayer = (function rasterizeXfaLayerClosure() { await writeSVG(svg, ctx); } catch (reason) { - throw new Error(`rasterizeXfaLayer: "${reason?.message}".`); + throw new Error(`Rasterize.xfaLayer: "${reason?.message}".`); } } - - return rasterizeXfaLayer; -})(); +} /** * @typedef {Object} DriverOptions @@ -713,7 +675,7 @@ class Driver { includeMarkedContent: true, }) .then(function (textContent) { - return rasterizeTextLayer( + return Rasterize.textLayer( textLayerContext, viewport, textContent, @@ -754,7 +716,7 @@ class Driver { annotationCanvasMap = new Map(); } else { initPromise = page.getXfa().then(function (xfa) { - return rasterizeXfaLayer( + return Rasterize.xfaLayer( annotationLayerContext, viewport, xfa, @@ -817,7 +779,7 @@ class Driver { } return renderTask.promise.then(function () { if (annotationCanvasMap) { - rasterizeAnnotationLayer( + Rasterize.annotationLayer( annotationLayerContext, viewport, data, From 911a9d34b16d31ab5b1c0ba740000886251ce503 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 5 Dec 2021 15:15:31 +0100 Subject: [PATCH 7/7] Fix code duplication in the rasterization logic in `test/driver.js` Now that the rasterization logic is encapsulated in a class, we can easily move the container creation into a separate static method. --- test/driver.js | 93 ++++++++++++++++++-------------------------------- 1 file changed, 33 insertions(+), 60 deletions(-) diff --git a/test/driver.js b/test/driver.js index 13b648560..5d5ebb628 100644 --- a/test/driver.js +++ b/test/driver.js @@ -175,6 +175,26 @@ class Rasterize { return shadow(this, "xfaStylePromise", loadStyles(styles)); } + static createContainer(viewport) { + const svg = document.createElementNS(SVG_NS, "svg:svg"); + svg.setAttribute("width", `${viewport.width}px`); + svg.setAttribute("height", `${viewport.height}px`); + + const foreignObject = document.createElementNS(SVG_NS, "svg:foreignObject"); + foreignObject.setAttribute("x", "0"); + foreignObject.setAttribute("y", "0"); + foreignObject.setAttribute("width", `${viewport.width}px`); + foreignObject.setAttribute("height", `${viewport.height}px`); + + const style = document.createElement("style"); + foreignObject.appendChild(style); + + const div = document.createElement("div"); + foreignObject.appendChild(div); + + return { svg, foreignObject, style, div }; + } + static async annotationLayer( ctx, viewport, @@ -185,36 +205,20 @@ class Rasterize { renderForms = false ) { try { - // Building SVG with size of the viewport. - const svg = document.createElementNS(SVG_NS, "svg:svg"); - svg.setAttribute("width", viewport.width + "px"); - svg.setAttribute("height", viewport.height + "px"); - - // Adding element to host our HTML (style + annotation layer div). - const foreignObject = document.createElementNS( - SVG_NS, - "svg:foreignObject" - ); - foreignObject.setAttribute("x", "0"); - foreignObject.setAttribute("y", "0"); - foreignObject.setAttribute("width", viewport.width + "px"); - foreignObject.setAttribute("height", viewport.height + "px"); - const style = document.createElement("style"); - foreignObject.appendChild(style); - const div = document.createElement("div"); + const { svg, foreignObject, style, div } = this.createContainer(viewport); div.className = "annotationLayer"; - // Rendering annotation layer as HTML. const [common, overrides] = await this.annotationStylePromise; - style.textContent = common + "\n" + overrides; + style.textContent = `${common}\n${overrides}`; - const annotation_viewport = viewport.clone({ dontFlip: true }); + const annotationViewport = viewport.clone({ dontFlip: true }); const annotationImageMap = await convertCanvasesToImages( annotationCanvasMap ); + // Rendering annotation layer as HTML. const parameters = { - viewport: annotation_viewport, + viewport: annotationViewport, div, annotations, page, @@ -238,27 +242,11 @@ class Rasterize { static async textLayer(ctx, viewport, textContent, enhanceTextSelection) { try { - // Building SVG with size of the viewport. - const svg = document.createElementNS(SVG_NS, "svg:svg"); - svg.setAttribute("width", viewport.width + "px"); - svg.setAttribute("height", viewport.height + "px"); - // items are transformed to have 1px font size - svg.setAttribute("font-size", 1); - - // Adding element to host our HTML (style + text layer div). - const foreignObject = document.createElementNS( - SVG_NS, - "svg:foreignObject" - ); - foreignObject.setAttribute("x", "0"); - foreignObject.setAttribute("y", "0"); - foreignObject.setAttribute("width", viewport.width + "px"); - foreignObject.setAttribute("height", viewport.height + "px"); - const style = document.createElement("style"); - foreignObject.appendChild(style); - const div = document.createElement("div"); + const { svg, foreignObject, style, div } = this.createContainer(viewport); div.className = "textLayer"; - foreignObject.appendChild(div); + + // Items are transformed to have 1px font size. + svg.setAttribute("font-size", 1); const [cssRules] = await this.textStylePromise; style.textContent = cssRules; @@ -290,26 +278,12 @@ class Rasterize { isPrint ) { try { - // Building SVG with size of the viewport. - const svg = document.createElementNS(SVG_NS, "svg:svg"); - svg.setAttribute("width", viewport.width + "px"); - svg.setAttribute("height", viewport.height + "px"); - const foreignObject = document.createElementNS( - SVG_NS, - "svg:foreignObject" - ); - foreignObject.setAttribute("x", "0"); - foreignObject.setAttribute("y", "0"); - foreignObject.setAttribute("width", viewport.width + "px"); - foreignObject.setAttribute("height", viewport.height + "px"); - const style = document.createElement("style"); - foreignObject.appendChild(style); - const div = document.createElement("div"); - foreignObject.appendChild(div); + const { svg, foreignObject, style, div } = this.createContainer(viewport); const [common, overrides] = await this.xfaStylePromise; - style.textContent = fontRules + "\n" + common + "\n" + overrides; + style.textContent = `${fontRules}\n${common}\n${overrides}`; + // Rendering XFA layer as HTML. XfaLayer.render({ xfa, div, @@ -319,8 +293,7 @@ class Rasterize { intent: isPrint ? "print" : "display", }); - // Some unsupported type of images (e.g. tiff) - // lead to errors. + // Some unsupported type of images (e.g. tiff) lead to errors. await resolveImages(div, /* silentErrors = */ true); svg.appendChild(foreignObject);