diff --git a/examples/node/pdf2png/pdf2png.js b/examples/node/pdf2png/pdf2png.js index 421a8a834..b8765c1ff 100644 --- a/examples/node/pdf2png/pdf2png.js +++ b/examples/node/pdf2png/pdf2png.js @@ -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."); diff --git a/examples/node/pdf2svg.js b/examples/node/pdf2svg.js index 24ca75ba8..58c419f4a 100644 --- a/examples/node/pdf2svg.js +++ b/examples/node/pdf2svg.js @@ -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 diff --git a/gulpfile.js b/gulpfile.js index a850f717d..363d34f1e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1617,6 +1617,7 @@ gulp.task( bugs: DIST_BUGS_URL, license: DIST_LICENSE, browser: { + canvas: false, fs: false, http: false, https: false, diff --git a/src/display/api.js b/src/display/api.js index 9c524809c..e88618a32 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -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 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, }); diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 4578a7d59..80bb9e59a 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -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, diff --git a/src/display/node_utils.js b/src/display/node_utils.js new file mode 100644 index 000000000..32d49baa5 --- /dev/null +++ b/src/display/node_utils.js @@ -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 }; diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index b06d8c625..3a5c77ba9 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -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"; diff --git a/test/unit/cmap_spec.js b/test/unit/cmap_spec.js index 553a0b21e..cc637aace 100644 --- a/test/unit/cmap_spec.js +++ b/test/unit/cmap_spec.js @@ -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 = { diff --git a/test/unit/custom_spec.js b/test/unit/custom_spec.js index 75cca0154..02e85609c 100644 --- a/test/unit/custom_spec.js +++ b/test/unit/custom_spec.js @@ -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); diff --git a/test/unit/test_utils.js b/test/unit/test_utils.js index a1156c5a8..f9220cdd1 100644 --- a/test/unit/test_utils.js +++ b/test/unit/test_utils.js @@ -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,