diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 1b1dc1b73..243c225ba 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -96,8 +96,9 @@ const DefaultPartialEvaluatorOptions = Object.freeze({ ignoreErrors: false, isEvalSupported: true, fontExtraProperties: false, - standardFontDataUrl: null, useSystemFonts: true, + cMapUrl: null, + standardFontDataUrl: null, }); const PatternType = { @@ -360,23 +361,25 @@ class PartialEvaluator { if (cachedData) { return cachedData; } - const readableStream = this.handler.sendWithStream("FetchBuiltInCMap", { - name, - }); - const reader = readableStream.getReader(); + let data; - const data = await new Promise(function (resolve, reject) { - function pump() { - reader.read().then(function ({ value, done }) { - if (done) { - return; - } - resolve(value); - pump(); - }, reject); + if (this.options.cMapUrl !== null) { + // Only compressed CMaps are (currently) supported here. + const url = `${this.options.cMapUrl}${name}.bcmap`; + const response = await fetch(url); + if (!response.ok) { + throw new Error( + `fetchBuiltInCMap: failed to fetch file "${url}" with "${response.statusText}".` + ); } - pump(); - }); + data = { + cMapData: new Uint8Array(await response.arrayBuffer()), + compressionType: CMapCompressionType.BINARY, + }; + } else { + // Get the data on the main-thread instead. + data = await this.handler.sendWithPromise("FetchBuiltInCMap", { name }); + } if (data.compressionType !== CMapCompressionType.NONE) { // Given the size of uncompressed CMaps, only cache compressed ones. diff --git a/src/core/worker.js b/src/core/worker.js index a69b76743..2ce3ef206 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -412,6 +412,7 @@ class WorkerMessageHandler { isEvalSupported: data.isEvalSupported, fontExtraProperties: data.fontExtraProperties, useSystemFonts: data.useSystemFonts, + cMapUrl: data.cMapUrl, standardFontDataUrl: data.standardFontDataUrl, }; diff --git a/src/display/api.js b/src/display/api.js index c319802ad..056e343cc 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -152,19 +152,19 @@ function setPDFNetworkStreamFactory(pdfNetworkStreamFactory) { * reading built-in CMap files. Providing a custom factory is useful for * environments without Fetch API or `XMLHttpRequest` support, such as * Node.js. The default value is {DOMCMapReaderFactory}. - * @property {boolean} [useSystemFonts] - When true, fonts that aren't embedded - * in the PDF will fallback to a system font. Defaults to true for web - * environments and false for node. + * @property {boolean} [useSystemFonts] - When `true`, fonts that aren't + * embedded in the PDF document will fallback to a system font. + * The default value is `true` in web environments and `false` in Node.js. * @property {string} [standardFontDataUrl] - The URL where the standard font * files are located. Include the trailing slash. - * @property {boolean} [useWorkerFetch] - Enable using fetch in the worker for - * resources. This currently only used for fetching the font data from the - * worker thread. When `true`, StandardFontDataFactory will be ignored. The - * default value is `true` in web environment and `false` for Node. * @property {Object} [StandardFontDataFactory] - The factory that will be used * when reading the standard font files. Providing a custom factory is useful * for environments without Fetch API or `XMLHttpRequest` support, such as * Node.js. The default value is {DOMStandardFontDataFactory}. + * @property {boolean} [useWorkerFetch] - Enable using the Fetch API in the + * worker-thread when reading CMap and standard font files. When `true`, + * the `CMapReaderFactory` and `StandardFontDataFactory` options are ignored. + * The default value is `true` in web wenvironments and `false` in Node.js. * @property {boolean} [stopAtErrors] - Reject certain promises, e.g. * `getOperatorList`, `getTextContent`, and `RenderTask`, when the associated * PDF data cannot be successfully parsed, instead of attempting to recover @@ -333,6 +333,7 @@ function getDocument(src) { } if (typeof params.useWorkerFetch !== "boolean") { params.useWorkerFetch = + params.CMapReaderFactory === DOMCMapReaderFactory && params.StandardFontDataFactory === DOMStandardFontDataFactory; } if (typeof params.isEvalSupported !== "boolean") { @@ -487,6 +488,7 @@ function _fetchDocument(worker, source, pdfDataRangeTransport, docId) { fontExtraProperties: source.fontExtraProperties, enableXfa: source.enableXfa, useSystemFonts: source.useSystemFonts, + cMapUrl: source.useWorkerFetch ? source.cMapUrl : null, standardFontDataUrl: source.useWorkerFetch ? source.standardFontDataUrl : null, @@ -2680,36 +2682,19 @@ class WorkerTransport { this._onUnsupportedFeature.bind(this) ); + messageHandler.on("FetchBuiltInCMap", data => { + if (this.destroyed) { + return Promise.reject(new Error("Worker was destroyed")); + } + return this.CMapReaderFactory.fetch(data); + }); + messageHandler.on("FetchStandardFontData", data => { if (this.destroyed) { return Promise.reject(new Error("Worker was destroyed")); } return this.StandardFontDataFactory.fetch(data); }); - - messageHandler.on("FetchBuiltInCMap", (data, sink) => { - if (this.destroyed) { - sink.error(new Error("Worker was destroyed")); - return; - } - let fetched = false; - - sink.onPull = () => { - if (fetched) { - sink.close(); - return; - } - fetched = true; - - this.CMapReaderFactory.fetch(data) - .then(function (builtInCMap) { - sink.enqueue(builtInCMap, 1, [builtInCMap.cMapData.buffer]); - }) - .catch(function (reason) { - sink.error(reason); - }); - }; - }); } _onUnsupportedFeature({ featureId }) { diff --git a/src/display/base_factory.js b/src/display/base_factory.js new file mode 100644 index 000000000..fa880c155 --- /dev/null +++ b/src/display/base_factory.js @@ -0,0 +1,129 @@ +/* Copyright 2015 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. + */ + +import { CMapCompressionType, unreachable } from "../shared/util.js"; + +class BaseCanvasFactory { + constructor() { + if (this.constructor === BaseCanvasFactory) { + unreachable("Cannot initialize BaseCanvasFactory."); + } + } + + create(width, height) { + unreachable("Abstract method `create` called."); + } + + reset(canvasAndContext, width, height) { + if (!canvasAndContext.canvas) { + throw new Error("Canvas is not specified"); + } + if (width <= 0 || height <= 0) { + throw new Error("Invalid canvas size"); + } + canvasAndContext.canvas.width = width; + canvasAndContext.canvas.height = height; + } + + destroy(canvasAndContext) { + if (!canvasAndContext.canvas) { + throw new Error("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 BaseCMapReaderFactory { + constructor({ baseUrl = null, isCompressed = false }) { + if (this.constructor === BaseCMapReaderFactory) { + unreachable("Cannot initialize BaseCMapReaderFactory."); + } + 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 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 BaseStandardFontDataFactory { + constructor({ baseUrl = null }) { + if (this.constructor === BaseStandardFontDataFactory) { + unreachable("Cannot initialize BaseStandardFontDataFactory."); + } + this.baseUrl = baseUrl; + } + + async fetch({ filename }) { + if (!this.baseUrl) { + throw new Error( + 'The standard font "baseUrl" parameter must be specified, ensure that ' + + 'the "standardFontDataUrl" API parameter is provided.' + ); + } + if (!filename) { + throw new Error("Font filename must be specified."); + } + const url = this.baseUrl + filename + ".pfb"; + + return this._fetchData(url).catch(reason => { + throw new Error(`Unable to load font data at: ${url}`); + }); + } + + /** + * @private + */ + _fetchData(url) { + unreachable("Abstract method `_fetchData` called."); + } +} + +export { + BaseCanvasFactory, + BaseCMapReaderFactory, + BaseStandardFontDataFactory, +}; diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 06b6f6e67..72f25810e 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -16,53 +16,21 @@ import { assert, BaseException, - CMapCompressionType, isString, removeNullCharacters, stringToBytes, - unreachable, Util, warn, } from "../shared/util.js"; +import { + BaseCanvasFactory, + BaseCMapReaderFactory, + BaseStandardFontDataFactory, +} from "./base_factory.js"; const DEFAULT_LINK_REL = "noopener noreferrer nofollow"; const SVG_NS = "http://www.w3.org/2000/svg"; -class BaseCanvasFactory { - constructor() { - if (this.constructor === BaseCanvasFactory) { - unreachable("Cannot initialize BaseCanvasFactory."); - } - } - - create(width, height) { - unreachable("Abstract method `create` called."); - } - - reset(canvasAndContext, width, height) { - if (!canvasAndContext.canvas) { - throw new Error("Canvas is not specified"); - } - if (width <= 0 || height <= 0) { - throw new Error("Invalid canvas size"); - } - canvasAndContext.canvas.width = width; - canvasAndContext.canvas.height = height; - } - - destroy(canvasAndContext) { - if (!canvasAndContext.canvas) { - throw new Error("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 DOMCanvasFactory extends BaseCanvasFactory { constructor({ ownerDocument = globalThis.document } = {}) { super(); @@ -134,45 +102,6 @@ function fetchData(url, asTypedArray) { }); } -class BaseCMapReaderFactory { - constructor({ baseUrl = null, isCompressed = false }) { - if (this.constructor === BaseCMapReaderFactory) { - unreachable("Cannot initialize BaseCMapReaderFactory."); - } - 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 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) { return fetchData(url, /* asTypedArray = */ this.isCompressed).then(data => { @@ -181,32 +110,6 @@ class DOMCMapReaderFactory extends BaseCMapReaderFactory { } } -class BaseStandardFontDataFactory { - constructor({ baseUrl = null }) { - if (this.constructor === BaseStandardFontDataFactory) { - unreachable("Cannot initialize BaseStandardFontDataFactory."); - } - this.baseUrl = baseUrl; - } - - async fetch({ filename }) { - if (!this.baseUrl) { - throw new Error( - 'The standard font "baseUrl" parameter must be specified, ensure that ' + - 'the "standardFontDataUrl" API parameter is provided.' - ); - } - if (!filename) { - throw new Error("Font filename must be specified."); - } - const url = this.baseUrl + filename + ".pfb"; - - return this._fetchData(url).catch(reason => { - throw new Error(`Unable to load font data at: ${url}`); - }); - } -} - class DOMStandardFontDataFactory extends BaseStandardFontDataFactory { _fetchData(url) { return fetchData(url, /* asTypedArray = */ true); @@ -740,9 +643,6 @@ class PDFDateString { export { addLinkAttributes, - BaseCanvasFactory, - BaseCMapReaderFactory, - BaseStandardFontDataFactory, DEFAULT_LINK_REL, deprecated, DOMCanvasFactory, diff --git a/src/display/node_utils.js b/src/display/node_utils.js index f40155b83..cf8a387c5 100644 --- a/src/display/node_utils.js +++ b/src/display/node_utils.js @@ -18,23 +18,10 @@ import { BaseCanvasFactory, BaseCMapReaderFactory, BaseStandardFontDataFactory, -} from "./display_utils.js"; +} from "./base_factory.js"; import { isNodeJS } from "../shared/is_node.js"; import { unreachable } from "../shared/util.js"; -function fetchData(url) { - 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(new Uint8Array(data)); - }); - }); -} - let NodeCanvasFactory = class { constructor() { unreachable("Not implemented: NodeCanvasFactory"); @@ -54,6 +41,19 @@ let NodeStandardFontDataFactory = class { }; if ((typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) && isNodeJS) { + const fetchData = function (url) { + 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(new Uint8Array(data)); + }); + }); + }; + NodeCanvasFactory = class extends BaseCanvasFactory { create(width, height) { if (width <= 0 || height <= 0) { diff --git a/test/driver.js b/test/driver.js index 70eacaab9..f4c7bde42 100644 --- a/test/driver.js +++ b/test/driver.js @@ -19,7 +19,7 @@ const WAITING_TIME = 100; // ms const PDF_TO_CSS_UNITS = 96.0 / 72.0; -const CMAP_URL = "../external/bcmaps/"; +const CMAP_URL = "/build/generic/web/cmaps/"; const CMAP_PACKED = true; const STANDARD_FONT_DATA_URL = "/build/generic/web/standard_fonts/"; const IMAGE_RESOURCES_PATH = "/web/images/"; diff --git a/test/test_manifest.json b/test/test_manifest.json index b84fbd7ac..4eb7c0cbf 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -4046,6 +4046,15 @@ "lastPage": 1, "type": "eq" }, + { "id": "mao-main_thread_fetch", + "file": "pdfs/mao.pdf", + "md5": "797093d67c4d4d4231ac6e1fb66bf6c3", + "rounds": 1, + "link": true, + "lastPage": 1, + "type": "eq", + "useWorkerFetch": false + }, { "id": "mao-text", "file": "pdfs/mao.pdf", "md5": "797093d67c4d4d4231ac6e1fb66bf6c3",