[api-minor] Use the NodeCanvasFactory/NodeCMapReaderFactory classes as defaults in Node.js environments (issue 11900)

This moves, and slightly simplifies, code that's currently residing in the unit-test utils into the actual library, such that it's bundled with `GENERIC`-builds and used in e.g. the API-code.

As an added bonus, this also brings out-of-the-box support for CMaps in e.g. the Node.js examples.
This commit is contained in:
Jonas Jenwald 2020-06-29 13:18:51 +02:00
parent fe3df495cc
commit 4a7e29865d
10 changed files with 164 additions and 123 deletions

View File

@ -50,14 +50,21 @@ NodeCanvasFactory.prototype = {
var pdfjsLib = require("pdfjs-dist/es5/build/pdf.js");
// Relative path of the PDF file.
var pdfURL = "../../../web/compressed.tracemonkey-pldi-09.pdf";
// Some PDFs need external cmaps.
var CMAP_URL = "../../../node_modules/pdfjs-dist/cmaps/";
var CMAP_PACKED = true;
// Read the PDF file into a typed array so PDF.js can load it.
var rawData = new Uint8Array(fs.readFileSync(pdfURL));
// Loading file from file system into typed array.
var pdfPath =
process.argv[2] || "../../../web/compressed.tracemonkey-pldi-09.pdf";
var data = new Uint8Array(fs.readFileSync(pdfPath));
// Load the PDF file.
var loadingTask = pdfjsLib.getDocument(rawData);
var loadingTask = pdfjsLib.getDocument({
data: data,
cMapUrl: CMAP_URL,
cMapPacked: CMAP_PACKED,
});
loadingTask.promise
.then(function (pdfDocument) {
console.log("# PDF document loaded.");

View File

@ -16,6 +16,10 @@ require("./domstubs.js").setStubs(global);
// Run `gulp dist-install` to generate 'pdfjs-dist' npm package files.
var pdfjsLib = require("pdfjs-dist/es5/build/pdf.js");
// Some PDFs need external cmaps.
var CMAP_URL = "../../node_modules/pdfjs-dist/cmaps/";
var CMAP_PACKED = true;
// Loading file from file system into typed array
var pdfPath = process.argv[2] || "../../web/compressed.tracemonkey-pldi-09.pdf";
var data = new Uint8Array(fs.readFileSync(pdfPath));
@ -86,6 +90,8 @@ function writeSvgToFile(svgElement, filePath) {
// callback.
var loadingTask = pdfjsLib.getDocument({
data: data,
cMapUrl: CMAP_URL,
cMapPacked: CMAP_PACKED,
fontExtraProperties: true,
});
loadingTask.promise

View File

@ -1617,6 +1617,7 @@ gulp.task(
bugs: DIST_BUGS_URL,
license: DIST_LICENSE,
browser: {
canvas: false,
fs: false,
http: false,
https: false,

View File

@ -47,6 +47,7 @@ import {
StatTimer,
} from "./display_utils.js";
import { FontFaceObject, FontLoader } from "./font_loader.js";
import { NodeCanvasFactory, NodeCMapReaderFactory } from "./node_utils.js";
import { apiCompatibilityParams } from "./api_compatibility.js";
import { CanvasGraphics } from "./canvas.js";
import { GlobalWorkerOptions } from "./worker_options.js";
@ -59,6 +60,15 @@ import { WebGLContext } from "./webgl.js";
const DEFAULT_RANGE_CHUNK_SIZE = 65536; // 2^16 = 65536
const RENDERING_CANCELLED_TIMEOUT = 100; // ms
const DefaultCanvasFactory =
(typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) && isNodeJS
? NodeCanvasFactory
: DOMCanvasFactory;
const DefaultCMapReaderFactory =
(typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) && isNodeJS
? NodeCMapReaderFactory
: DOMCMapReaderFactory;
/**
* @typedef {function} IPDFStreamFactory
* @param {DocumentInitParameters} params - The document initialization
@ -242,7 +252,8 @@ function getDocument(src) {
}
params.rangeChunkSize = params.rangeChunkSize || DEFAULT_RANGE_CHUNK_SIZE;
params.CMapReaderFactory = params.CMapReaderFactory || DOMCMapReaderFactory;
params.CMapReaderFactory =
params.CMapReaderFactory || DefaultCMapReaderFactory;
params.ignoreErrors = params.stopAtErrors !== true;
params.fontExtraProperties = params.fontExtraProperties === true;
params.pdfBug = params.pdfBug === true;
@ -863,9 +874,9 @@ class PDFDocumentProxy {
* just before viewport transform.
* @property {Object} [imageLayer] - An object that has beginLayout,
* endLayout and appendImage functions.
* @property {Object} [canvasFactory] - The factory that will be used
* @property {Object} [canvasFactory] - The factory instance that will be used
* when creating canvases. The default value is
* {DOMCanvasFactory}.
* {new DOMCanvasFactory()}.
* @property {Object} [background] - Background to use for the canvas.
* Can use any valid canvas.fillStyle: A DOMString parsed as
* CSS <color> value, a CanvasGradient object (a linear or
@ -1015,7 +1026,7 @@ class PDFPageProxy {
intentState.streamReaderCancelTimeout = null;
}
const canvasFactoryInstance = canvasFactory || new DOMCanvasFactory();
const canvasFactoryInstance = canvasFactory || new DefaultCanvasFactory();
const webGLContext = new WebGLContext({
enable: enableWebGL,
});

View File

@ -21,6 +21,7 @@ import {
isString,
removeNullCharacters,
stringToBytes,
unreachable,
Util,
warn,
} from "../shared/util.js";
@ -28,19 +29,15 @@ import {
const DEFAULT_LINK_REL = "noopener noreferrer nofollow";
const SVG_NS = "http://www.w3.org/2000/svg";
class DOMCanvasFactory {
create(width, height) {
if (width <= 0 || height <= 0) {
throw new Error("Invalid canvas size");
class BaseCanvasFactory {
constructor() {
if (this.constructor === BaseCanvasFactory) {
unreachable("Cannot initialize BaseCanvasFactory.");
}
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
return {
canvas,
context,
};
}
create(width, height) {
unreachable("Abstract method `create` called.");
}
reset(canvasAndContext, width, height) {
@ -67,8 +64,27 @@ class DOMCanvasFactory {
}
}
class DOMCMapReaderFactory {
class DOMCanvasFactory extends BaseCanvasFactory {
create(width, height) {
if (width <= 0 || height <= 0) {
throw new Error("Invalid canvas size");
}
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
return {
canvas,
context,
};
}
}
class BaseCMapReaderFactory {
constructor({ baseUrl = null, isCompressed = false }) {
if (this.constructor === BaseCMapReaderFactory) {
unreachable("Cannot initialize BaseCMapReaderFactory.");
}
this.baseUrl = baseUrl;
this.isCompressed = isCompressed;
}
@ -88,29 +104,39 @@ class DOMCMapReaderFactory {
? CMapCompressionType.BINARY
: CMapCompressionType.NONE;
return this._fetchData(url, compressionType).catch(reason => {
throw new Error(
`Unable to load ${this.isCompressed ? "binary " : ""}CMap at: ${url}`
);
});
}
/**
* @private
*/
_fetchData(url, compressionType) {
unreachable("Abstract method `_fetchData` called.");
}
}
class DOMCMapReaderFactory extends BaseCMapReaderFactory {
_fetchData(url, compressionType) {
if (
(typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) ||
(isFetchSupported() && isValidFetchUrl(url, document.baseURI))
) {
return fetch(url)
.then(async response => {
if (!response.ok) {
throw new Error(response.statusText);
}
let cMapData;
if (this.isCompressed) {
cMapData = new Uint8Array(await response.arrayBuffer());
} else {
cMapData = stringToBytes(await response.text());
}
return { cMapData, compressionType };
})
.catch(reason => {
throw new Error(
`Unable to load ${this.isCompressed ? "binary " : ""}` +
`CMap at: ${url}`
);
});
return fetch(url).then(async response => {
if (!response.ok) {
throw new Error(response.statusText);
}
let cMapData;
if (this.isCompressed) {
cMapData = new Uint8Array(await response.arrayBuffer());
} else {
cMapData = stringToBytes(await response.text());
}
return { cMapData, compressionType };
});
}
// The Fetch API is not supported.
@ -141,11 +167,6 @@ class DOMCMapReaderFactory {
};
request.send(null);
}).catch(reason => {
throw new Error(
`Unable to load ${this.isCompressed ? "binary " : ""}` +
`CMap at: ${url}`
);
});
}
}
@ -609,7 +630,9 @@ export {
getFilenameFromUrl,
LinkTarget,
DEFAULT_LINK_REL,
BaseCanvasFactory,
DOMCanvasFactory,
BaseCMapReaderFactory,
DOMCMapReaderFactory,
DOMSVGFactory,
StatTimer,

65
src/display/node_utils.js Normal file
View File

@ -0,0 +1,65 @@
/* Copyright 2020 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 __non_webpack_require__ */
/* eslint no-var: error */
import { BaseCanvasFactory, BaseCMapReaderFactory } from "./display_utils.js";
import { isNodeJS } from "../shared/is_node.js";
import { unreachable } from "../shared/util.js";
let NodeCanvasFactory = class {
constructor() {
unreachable("Not implemented: NodeCanvasFactory");
}
};
let NodeCMapReaderFactory = class {
constructor() {
unreachable("Not implemented: NodeCMapReaderFactory");
}
};
if ((typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) && isNodeJS) {
NodeCanvasFactory = class extends BaseCanvasFactory {
create(width, height) {
if (width <= 0 || height <= 0) {
throw new Error("Invalid canvas size");
}
const Canvas = __non_webpack_require__("canvas");
const canvas = Canvas.createCanvas(width, height);
return {
canvas,
context: canvas.getContext("2d"),
};
}
};
NodeCMapReaderFactory = class extends BaseCMapReaderFactory {
_fetchData(url, compressionType) {
return new Promise((resolve, reject) => {
const fs = __non_webpack_require__("fs");
fs.readFile(url, (error, data) => {
if (error || !data) {
reject(new Error(error));
return;
}
resolve({ cMapData: new Uint8Array(data), compressionType });
});
});
}
};
}
export { NodeCanvasFactory, NodeCMapReaderFactory };

View File

@ -16,7 +16,6 @@
import {
buildGetDocumentParams,
DOMFileReaderFactory,
NodeCanvasFactory,
NodeFileReaderFactory,
TEST_PDFS_PATH,
} from "./test_utils.js";
@ -49,6 +48,7 @@ import { GlobalImageCache } from "../../src/core/image_utils.js";
import { GlobalWorkerOptions } from "../../src/display/worker_options.js";
import { isNodeJS } from "../../src/shared/is_node.js";
import { Metadata } from "../../src/display/metadata.js";
import { NodeCanvasFactory } from "../../src/display/node_utils.js";
describe("api", function () {
const basicApiFileName = "basicapi.pdf";

View File

@ -17,7 +17,7 @@ import { CMap, CMapFactory, IdentityCMap } from "../../src/core/cmap.js";
import { DOMCMapReaderFactory } from "../../src/display/display_utils.js";
import { isNodeJS } from "../../src/shared/is_node.js";
import { Name } from "../../src/core/primitives.js";
import { NodeCMapReaderFactory } from "./test_utils.js";
import { NodeCMapReaderFactory } from "../../src/display/node_utils.js";
import { StringStream } from "../../src/core/stream.js";
var cMapUrl = {

View File

@ -13,10 +13,11 @@
* limitations under the License.
*/
import { buildGetDocumentParams, NodeCanvasFactory } from "./test_utils.js";
import { buildGetDocumentParams } from "./test_utils.js";
import { DOMCanvasFactory } from "../../src/display/display_utils.js";
import { getDocument } from "../../src/display/api.js";
import { isNodeJS } from "../../src/shared/is_node.js";
import { NodeCanvasFactory } from "../../src/display/node_utils.js";
function getTopLeftPixel(canvasContext) {
const imgData = canvasContext.getImageData(0, 0, 1, 1);

View File

@ -13,7 +13,7 @@
* limitations under the License.
*/
import { assert, CMapCompressionType } from "../../src/shared/util.js";
import { assert } from "../../src/shared/util.js";
import { isNodeJS } from "../../src/shared/is_node.js";
import { isRef } from "../../src/core/primitives.js";
import { Page } from "../../src/core/document.js";
@ -62,77 +62,6 @@ function buildGetDocumentParams(filename, options) {
return params;
}
class NodeCanvasFactory {
create(width, height) {
assert(width > 0 && height > 0, "Invalid canvas size");
const Canvas = require("canvas");
const canvas = Canvas.createCanvas(width, height);
return {
canvas,
context: canvas.getContext("2d"),
};
}
reset(canvasAndContext, width, height) {
assert(canvasAndContext.canvas, "Canvas is not specified");
assert(width > 0 && height > 0, "Invalid canvas size");
canvasAndContext.canvas.width = width;
canvasAndContext.canvas.height = height;
}
destroy(canvasAndContext) {
assert(canvasAndContext.canvas, "Canvas is not specified");
// Zeroing the width and height cause Firefox to release graphics
// resources immediately, which can greatly reduce memory consumption.
canvasAndContext.canvas.width = 0;
canvasAndContext.canvas.height = 0;
canvasAndContext.canvas = null;
canvasAndContext.context = null;
}
}
class NodeCMapReaderFactory {
constructor({ baseUrl = null, isCompressed = false }) {
this.baseUrl = baseUrl;
this.isCompressed = isCompressed;
}
async fetch({ name }) {
if (!this.baseUrl) {
throw new Error(
'The CMap "baseUrl" parameter must be specified, ensure that ' +
'the "cMapUrl" and "cMapPacked" API parameters are provided.'
);
}
if (!name) {
throw new Error("CMap name must be specified.");
}
const url = this.baseUrl + name + (this.isCompressed ? ".bcmap" : "");
const compressionType = this.isCompressed
? CMapCompressionType.BINARY
: CMapCompressionType.NONE;
return new Promise((resolve, reject) => {
const fs = require("fs");
fs.readFile(url, (error, data) => {
if (error || !data) {
reject(new Error(error));
return;
}
resolve({ cMapData: new Uint8Array(data), compressionType });
});
}).catch(reason => {
throw new Error(
`Unable to load ${this.isCompressed ? "binary " : ""}` +
`CMap at: ${url}`
);
});
}
}
class XRefMock {
constructor(array) {
this._map = Object.create(null);
@ -186,8 +115,6 @@ function isEmptyObj(obj) {
export {
DOMFileReaderFactory,
NodeFileReaderFactory,
NodeCanvasFactory,
NodeCMapReaderFactory,
XRefMock,
buildGetDocumentParams,
TEST_PDFS_PATH,