0351852d74
Currently some JPEG images are decoded by the built-in PDF.js decoder in `src/core/jpg.js`, while others attempt to use the browser JPEG decoder. This inconsistency seem unfortunate for a number of reasons:
- It adds, compared to the other image formats supported in the PDF specification, a fair amount of code/complexity to the image handling in the PDF.js library.
- The PDF specification support JPEG images with features, e.g. certain ColorSpaces, that browsers are unable to decode natively. Hence, determining if a JPEG image is possible to decode natively in the browser require a non-trivial amount of parsing. In particular, we're parsing (part of) the raw JPEG data to extract certain marker data and we also need to parse the ColorSpace for the JPEG image.
- While some JPEG images may, for all intents and purposes, appear to be natively supported there's still cases where the browser may fail to decode some JPEG images. In order to support those cases, we've had to implement a fallback to the PDF.js JPEG decoder if there's any issues during the native decoding. This also means that it's no longer possible to simply send the JPEG image to the main-thread and continue parsing, but you now need to actually wait for the main-thread to indicate success/failure first.
In practice this means that there's a code-path where the worker-thread is forced to wait for the main-thread, while the reverse should *always* be the case.
- The native decoding, for anything except the *simplest* of JPEG images, result in increased peak memory usage because there's a handful of short-lived copies of the JPEG data (see PR 11707).
Furthermore this also leads to data being *parsed* on the main-thread, rather than the worker-thread, which you usually want to avoid for e.g. performance and UI-reponsiveness reasons.
- Not all environments, e.g. Node.js, fully support native JPEG decoding. This has, historically, lead to some issues and support requests.
- Different browsers may use different JPEG decoders, possibly leading to images being rendered slightly differently depending on the platform/browser where the PDF.js library is used.
Originally the implementation in `src/core/jpg.js` were unable to handle all of the JPEG images in the test-suite, but over the last couple of years I've fixed (hopefully) all of those issues.
At this point in time, there's two kinds of failure with this patch:
- Changes which are basically imperceivable to the naked eye, where some pixels in the images are essentially off-by-one (in all components), which could probably be attributed to things such as different rounding behaviour in the browser/PDF.js JPEG decoder.
This type of "failure" accounts for the *vast* majority of the total number of changes in the reference tests.
- Changes where the JPEG images now looks *ever so slightly* blurrier than with the native browser decoder. For quite some time I've just assumed that this pointed to a general deficiency in the `src/core/jpg.js` implementation, however I've discovered when comparing two viewers side-by-side that the differences vanish at higher zoom levels (usually around 200% is enough).
Basically if you disable [this downscaling in canvas.js](8fb82e939c/src/display/canvas.js (L2356-L2395)
), which is what happens when zooming in, the differences simply vanish!
Hence I'm pretty satisfied that there's no significant problems with the `src/core/jpg.js` implementation, and the problems are rather tied to the general quality of the downscaling algorithm used. It could even be seen as a positive that *all* images now share the same downscaling behaviour, since this actually fixes one old bug; see issue 7041.
761 lines
25 KiB
JavaScript
761 lines
25 KiB
JavaScript
/* Copyright 2012 Mozilla Foundation
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
/* globals pdfjsLib, pdfjsViewer */
|
|
|
|
"use strict";
|
|
|
|
const WAITING_TIME = 100; // ms
|
|
const PDF_TO_CSS_UNITS = 96.0 / 72.0;
|
|
const CMAP_URL = "../external/bcmaps/";
|
|
const CMAP_PACKED = true;
|
|
const IMAGE_RESOURCES_PATH = "/web/images/";
|
|
const WORKER_SRC = "../build/generic/build/pdf.worker.js";
|
|
|
|
/**
|
|
* @class
|
|
*/
|
|
var rasterizeTextLayer = (function rasterizeTextLayerClosure() {
|
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
|
|
var textLayerStylePromise = null;
|
|
function getTextLayerStyle() {
|
|
if (textLayerStylePromise) {
|
|
return textLayerStylePromise;
|
|
}
|
|
textLayerStylePromise = new Promise(function (resolve) {
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open("GET", "./text_layer_test.css");
|
|
xhr.onload = function () {
|
|
resolve(xhr.responseText);
|
|
};
|
|
xhr.send(null);
|
|
});
|
|
return textLayerStylePromise;
|
|
}
|
|
|
|
// eslint-disable-next-line no-shadow
|
|
function rasterizeTextLayer(
|
|
ctx,
|
|
viewport,
|
|
textContent,
|
|
enhanceTextSelection
|
|
) {
|
|
return new Promise(function (resolve, reject) {
|
|
// Building SVG with size of the viewport.
|
|
var 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");
|
|
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 = getTextLayerStyle();
|
|
foreignObject.appendChild(style);
|
|
var div = document.createElement("div");
|
|
div.className = "textLayer";
|
|
foreignObject.appendChild(div);
|
|
|
|
// Rendering text layer as HTML.
|
|
var task = pdfjsLib.renderTextLayer({
|
|
textContent,
|
|
container: div,
|
|
viewport,
|
|
enhanceTextSelection,
|
|
});
|
|
Promise.all([stylePromise, task.promise]).then(function (results) {
|
|
task.expandTextDivs(true);
|
|
style.textContent = results[0];
|
|
svg.appendChild(foreignObject);
|
|
|
|
// We need to have UTF-8 encoded XML.
|
|
var svg_xml = unescape(
|
|
encodeURIComponent(new XMLSerializer().serializeToString(svg))
|
|
);
|
|
var 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 rasterizeTextLayer;
|
|
})();
|
|
|
|
/**
|
|
* @class
|
|
*/
|
|
var rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() {
|
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* 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,
|
|
},
|
|
};
|
|
|
|
function getAnnotationLayerStyle() {
|
|
// Use the cached promises if they are available.
|
|
if (styles.common.promise && styles.overrides.promise) {
|
|
return Promise.all([styles.common.promise, styles.overrides.promise]);
|
|
}
|
|
|
|
// Load the style files and cache the results.
|
|
for (const key in styles) {
|
|
styles[key].promise = new Promise(function (resolve, reject) {
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open("GET", styles[key].file);
|
|
xhr.onload = function () {
|
|
resolve(xhr.responseText);
|
|
};
|
|
xhr.onerror = function (e) {
|
|
reject(new Error("Error fetching annotation style " + e));
|
|
};
|
|
xhr.send(null);
|
|
});
|
|
}
|
|
|
|
return Promise.all([styles.common.promise, styles.overrides.promise]);
|
|
}
|
|
|
|
function inlineAnnotationImages(images) {
|
|
var imagePromises = [];
|
|
for (var i = 0, ii = images.length; i < ii; i++) {
|
|
var imagePromise = new Promise(function (resolve, reject) {
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.responseType = "blob";
|
|
xhr.onload = function () {
|
|
var reader = new FileReader();
|
|
reader.onloadend = function () {
|
|
resolve(reader.result);
|
|
};
|
|
reader.readAsDataURL(xhr.response);
|
|
};
|
|
xhr.onerror = function (e) {
|
|
reject(new Error("Error fetching inline annotation image " + e));
|
|
};
|
|
xhr.open("GET", images[i].src);
|
|
xhr.send();
|
|
});
|
|
imagePromises.push(imagePromise);
|
|
}
|
|
return imagePromises;
|
|
}
|
|
|
|
// eslint-disable-next-line no-shadow
|
|
function rasterizeAnnotationLayer(
|
|
ctx,
|
|
viewport,
|
|
annotations,
|
|
page,
|
|
imageResourcesPath,
|
|
renderInteractiveForms
|
|
) {
|
|
return new Promise(function (resolve, reject) {
|
|
// Building SVG with size of the viewport.
|
|
var 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");
|
|
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 = getAnnotationLayerStyle();
|
|
foreignObject.appendChild(style);
|
|
var div = document.createElement("div");
|
|
div.className = "annotationLayer";
|
|
|
|
// Rendering annotation layer as HTML.
|
|
stylePromise.then(function (common, overrides) {
|
|
style.textContent = common + overrides;
|
|
|
|
var annotation_viewport = viewport.clone({ dontFlip: true });
|
|
var parameters = {
|
|
viewport: annotation_viewport,
|
|
div,
|
|
annotations,
|
|
page,
|
|
linkService: new pdfjsViewer.SimpleLinkService(),
|
|
imageResourcesPath,
|
|
renderInteractiveForms,
|
|
};
|
|
pdfjsLib.AnnotationLayer.render(parameters);
|
|
|
|
// Inline SVG images from text annotations.
|
|
var images = div.getElementsByTagName("img");
|
|
var imagePromises = inlineAnnotationImages(images);
|
|
var converted = Promise.all(imagePromises).then(function (data) {
|
|
var loadedPromises = [];
|
|
for (var i = 0, ii = data.length; i < ii; i++) {
|
|
images[i].src = data[i];
|
|
loadedPromises.push(
|
|
new Promise(function (resolveImage, rejectImage) {
|
|
images[i].onload = resolveImage;
|
|
images[i].onerror = function (e) {
|
|
rejectImage(new Error("Error loading image " + e));
|
|
};
|
|
})
|
|
);
|
|
}
|
|
return loadedPromises;
|
|
});
|
|
|
|
foreignObject.appendChild(div);
|
|
svg.appendChild(foreignObject);
|
|
|
|
// We need to have UTF-8 encoded XML.
|
|
converted.then(function () {
|
|
var svg_xml = unescape(
|
|
encodeURIComponent(new XMLSerializer().serializeToString(svg))
|
|
);
|
|
var 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 annotation layer " + e));
|
|
};
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
return rasterizeAnnotationLayer;
|
|
})();
|
|
|
|
/**
|
|
* @typedef {Object} DriverOptions
|
|
* @property {HTMLSpanElement} inflight - Field displaying the number of
|
|
* inflight requests.
|
|
* @property {HTMLInputElement} disableScrolling - Checkbox to disable
|
|
* automatic scrolling of the output container.
|
|
* @property {HTMLPreElement} output - Container for all output messages.
|
|
* @property {HTMLDivElement} end - Container for a completion message.
|
|
*/
|
|
|
|
/**
|
|
* @class
|
|
*/
|
|
// eslint-disable-next-line no-unused-vars
|
|
var Driver = (function DriverClosure() {
|
|
/**
|
|
* @constructs Driver
|
|
* @param {DriverOptions} options
|
|
*/
|
|
// eslint-disable-next-line no-shadow
|
|
function Driver(options) {
|
|
// Configure the global worker options.
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = WORKER_SRC;
|
|
|
|
// Set the passed options
|
|
this.inflight = options.inflight;
|
|
this.disableScrolling = options.disableScrolling;
|
|
this.output = options.output;
|
|
this.end = options.end;
|
|
|
|
// Set parameters from the query string
|
|
var parameters = this._getQueryStringParameters();
|
|
this.browser = parameters.browser;
|
|
this.manifestFile = parameters.manifestFile;
|
|
this.delay = parameters.delay | 0 || 0;
|
|
this.inFlightRequests = 0;
|
|
this.testFilter = parameters.testFilter
|
|
? JSON.parse(parameters.testFilter)
|
|
: [];
|
|
|
|
// Create a working canvas
|
|
this.canvas = document.createElement("canvas");
|
|
}
|
|
|
|
Driver.prototype = {
|
|
_getQueryStringParameters: function Driver_getQueryStringParameters() {
|
|
var queryString = window.location.search.substring(1);
|
|
var values = queryString.split("&");
|
|
var parameters = {};
|
|
for (var i = 0, ii = values.length; i < ii; i++) {
|
|
var value = values[i].split("=");
|
|
parameters[unescape(value[0])] = unescape(value[1]);
|
|
}
|
|
return parameters;
|
|
},
|
|
|
|
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 + '"... ');
|
|
|
|
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 && self.testFilter.length) {
|
|
self.manifest = self.manifest.filter(function (item) {
|
|
return self.testFilter.includes(item.id);
|
|
});
|
|
}
|
|
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);
|
|
},
|
|
|
|
_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: [] };
|
|
|
|
this._log('Loading file "' + task.file + '"\n');
|
|
|
|
const absoluteUrl = new URL(task.file, window.location).href;
|
|
try {
|
|
const loadingTask = pdfjsLib.getDocument({
|
|
url: absoluteUrl,
|
|
password: task.password,
|
|
cMapUrl: CMAP_URL,
|
|
cMapPacked: CMAP_PACKED,
|
|
disableRange: task.disableRange,
|
|
disableAutoFetch: !task.enableAutoFetch,
|
|
pdfBug: true,
|
|
});
|
|
loadingTask.promise.then(
|
|
doc => {
|
|
task.pdfDoc = doc;
|
|
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);
|
|
});
|
|
},
|
|
|
|
_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.removeChild(body.lastChild);
|
|
}
|
|
|
|
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();
|
|
});
|
|
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: 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;
|
|
|
|
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,
|
|
})
|
|
.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;
|
|
|
|
// Render the annotation layer if necessary.
|
|
if (renderAnnotations || renderForms) {
|
|
// 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
|
|
);
|
|
|
|
// The annotation builder will draw its content on the canvas.
|
|
initPromise = page
|
|
.getAnnotations({ intent: "display" })
|
|
.then(function (annotations) {
|
|
return rasterizeAnnotationLayer(
|
|
annotationLayerContext,
|
|
viewport,
|
|
annotations,
|
|
page,
|
|
IMAGE_RESOURCES_PATH,
|
|
renderForms
|
|
);
|
|
});
|
|
} else {
|
|
annotationLayerCanvas = null;
|
|
initPromise = Promise.resolve();
|
|
}
|
|
}
|
|
|
|
var renderContext = {
|
|
canvasContext: ctx,
|
|
viewport,
|
|
renderInteractiveForms: renderForms,
|
|
};
|
|
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 () {
|
|
return page.render(renderContext).promise.then(function () {
|
|
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: function Driver_clearCanvas() {
|
|
var ctx = this.canvas.getContext("2d", { alpha: false });
|
|
ctx.beginPath();
|
|
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
},
|
|
|
|
_snapshot: function 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(
|
|
"done" + (failure ? " (failed !: " + failure + ")" : "") + "\n"
|
|
);
|
|
task.pageNum++;
|
|
self._nextPage(task);
|
|
});
|
|
},
|
|
|
|
_quit: function Driver_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: function Driver_info(message) {
|
|
this._send(
|
|
"/info",
|
|
JSON.stringify({
|
|
browser: this.browser,
|
|
message,
|
|
})
|
|
);
|
|
},
|
|
|
|
_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 (callback) {
|
|
callback();
|
|
}
|
|
}
|
|
};
|
|
this.inflight.textContent = this.inFlightRequests++;
|
|
r.send(message);
|
|
},
|
|
};
|
|
|
|
return Driver;
|
|
})();
|