From 4e12906061a3a6022d542936ba927931c494f33d Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 1 Nov 2024 12:34:39 +0100 Subject: [PATCH] Move the various DOM-factories into their own files - Over time the number and size of these factories have increased, especially the `DOMFilterFactory` class, and this split should thus aid readability/maintainability of the code. - By introducing a couple of new import maps we can avoid bundling the `DOMCMapReaderFactory`/`DOMStandardFontDataFactory` classes in the Firefox PDF Viewer, since they are dead code there given that worker-thread fetching is always being used. - This patch has been successfully tested, by running `$ ./mach test toolkit/components/pdfjs/`, in a local Firefox artifact-build. *Note:* This patch reduces the size of the `gulp mozcentral` output by `1.3` kilo-bytes, which isn't a lot but still cannot hurt. --- gulpfile.mjs | 12 + src/display/annotation_layer.js | 7 +- src/display/api.js | 8 +- src/display/base_factory.js | 234 ---------- src/display/canvas_factory.js | 92 ++++ src/display/cmap_reader_factory.js | 75 ++++ src/display/display_utils.js | 521 +---------------------- src/display/draw_layer.js | 2 +- src/display/filter_factory.js | 508 ++++++++++++++++++++++ src/display/node_utils.js | 10 +- src/display/standard_fontdata_factory.js | 65 +++ src/display/stubs.js | 4 + src/display/svg_factory.js | 71 +++ src/pdf.js | 2 +- test/unit/canvas_factory_spec.js | 111 +++++ test/unit/clitests.json | 2 + test/unit/display_utils_spec.js | 147 ------- test/unit/jasmine-boot.js | 2 + test/unit/pdf_spec.js | 2 +- test/unit/svg_factory_spec.js | 72 ++++ test/unit/unit_test.html | 2 + tsconfig.json | 4 + web/viewer-geckoview.html | 2 + web/viewer.html | 2 + 24 files changed, 1038 insertions(+), 919 deletions(-) delete mode 100644 src/display/base_factory.js create mode 100644 src/display/canvas_factory.js create mode 100644 src/display/cmap_reader_factory.js create mode 100644 src/display/filter_factory.js create mode 100644 src/display/standard_fontdata_factory.js create mode 100644 src/display/svg_factory.js create mode 100644 test/unit/canvas_factory_spec.js create mode 100644 test/unit/svg_factory_spec.js diff --git a/gulpfile.mjs b/gulpfile.mjs index 8c97bf4fe5df3..45b621d0dd6b1 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -191,6 +191,8 @@ function createWebpackAlias(defines) { "fluent-dom": "node_modules/@fluent/dom/esm/index.js", }; const libraryAlias = { + "display-cmap_reader_factory": "src/display/stubs.js", + "display-standard_fontdata_factory": "src/display/stubs.js", "display-fetch_stream": "src/display/stubs.js", "display-network": "src/display/stubs.js", "display-node_stream": "src/display/stubs.js", @@ -219,6 +221,10 @@ function createWebpackAlias(defines) { }; if (defines.CHROME) { + libraryAlias["display-cmap_reader_factory"] = + "src/display/cmap_reader_factory.js"; + libraryAlias["display-standard_fontdata_factory"] = + "src/display/standard_fontdata_factory.js"; libraryAlias["display-fetch_stream"] = "src/display/fetch_stream.js"; libraryAlias["display-network"] = "src/display/network.js"; @@ -231,6 +237,10 @@ function createWebpackAlias(defines) { // Aliases defined here must also be replicated in the paths section of // the tsconfig.json file for the type generation to work. // In the tsconfig.json files, the .js extension must be omitted. + libraryAlias["display-cmap_reader_factory"] = + "src/display/cmap_reader_factory.js"; + libraryAlias["display-standard_fontdata_factory"] = + "src/display/standard_fontdata_factory.js"; libraryAlias["display-fetch_stream"] = "src/display/fetch_stream.js"; libraryAlias["display-network"] = "src/display/network.js"; libraryAlias["display-node_stream"] = "src/display/node_stream.js"; @@ -1573,6 +1583,8 @@ function buildLibHelper(bundleDefines, inputStream, outputDir) { defines: bundleDefines, map: { "pdfjs-lib": "../pdf.js", + "display-cmap_reader_factory": "./cmap_reader_factory.js", + "display-standard_fontdata_factory": "./standard_fontdata_factory.js", "display-fetch_stream": "./fetch_stream.js", "display-network": "./network.js", "display-node_stream": "./node_stream.js", diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 955b16f363303..e2dc138a8ded4 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -37,13 +37,10 @@ import { Util, warn, } from "../shared/util.js"; -import { - DOMSVGFactory, - PDFDateString, - setLayerDimensions, -} from "./display_utils.js"; +import { PDFDateString, setLayerDimensions } from "./display_utils.js"; import { AnnotationStorage } from "./annotation_storage.js"; import { ColorConverters } from "../shared/scripting_utils.js"; +import { DOMSVGFactory } from "./svg_factory.js"; import { XfaLayer } from "./xfa_layer.js"; const DEFAULT_TAB_INDEX = 1000; diff --git a/src/display/api.js b/src/display/api.js index b19a89790c4b7..b9e08f6fd2a20 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -45,10 +45,6 @@ import { } from "./annotation_storage.js"; import { deprecated, - DOMCanvasFactory, - DOMCMapReaderFactory, - DOMFilterFactory, - DOMStandardFontDataFactory, isDataScheme, isValidFetchUrl, PageViewport, @@ -64,6 +60,10 @@ import { NodeStandardFontDataFactory, } from "display-node_utils"; import { CanvasGraphics } from "./canvas.js"; +import { DOMCanvasFactory } from "./canvas_factory.js"; +import { DOMCMapReaderFactory } from "display-cmap_reader_factory"; +import { DOMFilterFactory } from "./filter_factory.js"; +import { DOMStandardFontDataFactory } from "display-standard_fontdata_factory"; import { GlobalWorkerOptions } from "./worker_options.js"; import { MessageHandler } from "../shared/message_handler.js"; import { Metadata } from "./metadata.js"; diff --git a/src/display/base_factory.js b/src/display/base_factory.js deleted file mode 100644 index 7d72ac49ab267..0000000000000 --- a/src/display/base_factory.js +++ /dev/null @@ -1,234 +0,0 @@ -/* 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 { unreachable } from "../shared/util.js"; - -class BaseFilterFactory { - constructor() { - if ( - (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && - this.constructor === BaseFilterFactory - ) { - unreachable("Cannot initialize BaseFilterFactory."); - } - } - - addFilter(maps) { - return "none"; - } - - addHCMFilter(fgColor, bgColor) { - return "none"; - } - - addAlphaFilter(map) { - return "none"; - } - - addLuminosityFilter(map) { - return "none"; - } - - addHighlightHCMFilter(filterName, fgColor, bgColor, newFgColor, newBgColor) { - return "none"; - } - - destroy(keepHCM = false) {} -} - -class BaseCanvasFactory { - #enableHWA = false; - - constructor({ enableHWA = false }) { - if ( - (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && - this.constructor === BaseCanvasFactory - ) { - unreachable("Cannot initialize BaseCanvasFactory."); - } - this.#enableHWA = enableHWA; - } - - create(width, height) { - if (width <= 0 || height <= 0) { - throw new Error("Invalid canvas size"); - } - const canvas = this._createCanvas(width, height); - return { - canvas, - context: canvas.getContext("2d", { - willReadFrequently: !this.#enableHWA, - }), - }; - } - - 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; - } - - /** - * @ignore - */ - _createCanvas(width, height) { - unreachable("Abstract method `_createCanvas` called."); - } -} - -class BaseCMapReaderFactory { - constructor({ baseUrl = null, isCompressed = true }) { - if ( - (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && - this.constructor === BaseCMapReaderFactory - ) { - unreachable("Cannot initialize BaseCMapReaderFactory."); - } - this.baseUrl = baseUrl; - this.isCompressed = isCompressed; - } - - async fetch({ name }) { - if (!this.baseUrl) { - throw new Error( - "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" : ""); - - return this._fetch(url) - .then(cMapData => ({ cMapData, isCompressed: this.isCompressed })) - .catch(reason => { - throw new Error( - `Unable to load ${this.isCompressed ? "binary " : ""}CMap at: ${url}` - ); - }); - } - - /** - * @ignore - * @returns {Promise} - */ - async _fetch(url) { - unreachable("Abstract method `_fetch` called."); - } -} - -class BaseStandardFontDataFactory { - constructor({ baseUrl = null }) { - if ( - (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && - this.constructor === BaseStandardFontDataFactory - ) { - unreachable("Cannot initialize BaseStandardFontDataFactory."); - } - this.baseUrl = baseUrl; - } - - async fetch({ filename }) { - if (!this.baseUrl) { - throw new Error( - "Ensure that the `standardFontDataUrl` API parameter is provided." - ); - } - if (!filename) { - throw new Error("Font filename must be specified."); - } - const url = `${this.baseUrl}${filename}`; - - return this._fetch(url).catch(reason => { - throw new Error(`Unable to load font data at: ${url}`); - }); - } - - /** - * @ignore - * @returns {Promise} - */ - async _fetch(url) { - unreachable("Abstract method `_fetch` called."); - } -} - -class BaseSVGFactory { - constructor() { - if ( - (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && - this.constructor === BaseSVGFactory - ) { - unreachable("Cannot initialize BaseSVGFactory."); - } - } - - create(width, height, skipDimensions = false) { - if (width <= 0 || height <= 0) { - throw new Error("Invalid SVG dimensions"); - } - const svg = this._createSVG("svg:svg"); - svg.setAttribute("version", "1.1"); - - if (!skipDimensions) { - svg.setAttribute("width", `${width}px`); - svg.setAttribute("height", `${height}px`); - } - - svg.setAttribute("preserveAspectRatio", "none"); - svg.setAttribute("viewBox", `0 0 ${width} ${height}`); - - return svg; - } - - createElement(type) { - if (typeof type !== "string") { - throw new Error("Invalid SVG element type"); - } - return this._createSVG(type); - } - - /** - * @ignore - */ - _createSVG(type) { - unreachable("Abstract method `_createSVG` called."); - } -} - -export { - BaseCanvasFactory, - BaseCMapReaderFactory, - BaseFilterFactory, - BaseStandardFontDataFactory, - BaseSVGFactory, -}; diff --git a/src/display/canvas_factory.js b/src/display/canvas_factory.js new file mode 100644 index 0000000000000..988e764859828 --- /dev/null +++ b/src/display/canvas_factory.js @@ -0,0 +1,92 @@ +/* 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 { unreachable } from "../shared/util.js"; + +class BaseCanvasFactory { + #enableHWA = false; + + constructor({ enableHWA = false }) { + if ( + (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && + this.constructor === BaseCanvasFactory + ) { + unreachable("Cannot initialize BaseCanvasFactory."); + } + this.#enableHWA = enableHWA; + } + + create(width, height) { + if (width <= 0 || height <= 0) { + throw new Error("Invalid canvas size"); + } + const canvas = this._createCanvas(width, height); + return { + canvas, + context: canvas.getContext("2d", { + willReadFrequently: !this.#enableHWA, + }), + }; + } + + 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; + } + + /** + * @ignore + */ + _createCanvas(width, height) { + unreachable("Abstract method `_createCanvas` called."); + } +} + +class DOMCanvasFactory extends BaseCanvasFactory { + constructor({ ownerDocument = globalThis.document, enableHWA = false }) { + super({ enableHWA }); + this._document = ownerDocument; + } + + /** + * @ignore + */ + _createCanvas(width, height) { + const canvas = this._document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + return canvas; + } +} + +export { BaseCanvasFactory, DOMCanvasFactory }; diff --git a/src/display/cmap_reader_factory.js b/src/display/cmap_reader_factory.js new file mode 100644 index 0000000000000..5246fbb23dfa3 --- /dev/null +++ b/src/display/cmap_reader_factory.js @@ -0,0 +1,75 @@ +/* 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 { stringToBytes, unreachable } from "../shared/util.js"; +import { fetchData } from "./display_utils.js"; + +class BaseCMapReaderFactory { + constructor({ baseUrl = null, isCompressed = true }) { + if ( + (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && + this.constructor === BaseCMapReaderFactory + ) { + unreachable("Cannot initialize BaseCMapReaderFactory."); + } + this.baseUrl = baseUrl; + this.isCompressed = isCompressed; + } + + async fetch({ name }) { + if (!this.baseUrl) { + throw new Error( + "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" : ""); + + return this._fetch(url) + .then(cMapData => ({ cMapData, isCompressed: this.isCompressed })) + .catch(reason => { + throw new Error( + `Unable to load ${this.isCompressed ? "binary " : ""}CMap at: ${url}` + ); + }); + } + + /** + * @ignore + * @returns {Promise} + */ + async _fetch(url) { + unreachable("Abstract method `_fetch` called."); + } +} + +class DOMCMapReaderFactory extends BaseCMapReaderFactory { + /** + * @ignore + */ + async _fetch(url) { + const data = await fetchData( + url, + /* type = */ this.isCompressed ? "arraybuffer" : "text" + ); + return data instanceof ArrayBuffer + ? new Uint8Array(data) + : stringToBytes(data); + } +} + +export { BaseCMapReaderFactory, DOMCMapReaderFactory }; diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 60b4587ebacf1..218a0f33bdd03 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -13,18 +13,10 @@ * limitations under the License. */ -import { - BaseCanvasFactory, - BaseCMapReaderFactory, - BaseFilterFactory, - BaseStandardFontDataFactory, - BaseSVGFactory, -} from "./base_factory.js"; import { BaseException, FeatureTest, shadow, - stringToBytes, Util, warn, } from "../shared/util.js"; @@ -39,479 +31,6 @@ class PixelsPerInch { static PDF_TO_CSS_UNITS = this.CSS / this.PDF; } -/** - * FilterFactory aims to create some SVG filters we can use when drawing an - * image (or whatever) on a canvas. - * Filters aren't applied with ctx.putImageData because it just overwrites the - * underlying pixels. - * With these filters, it's possible for example to apply some transfer maps on - * an image without the need to apply them on the pixel arrays: the renderer - * does the magic for us. - */ -class DOMFilterFactory extends BaseFilterFactory { - #baseUrl; - - #_cache; - - #_defs; - - #docId; - - #document; - - #_hcmCache; - - #id = 0; - - constructor({ docId, ownerDocument = globalThis.document }) { - super(); - this.#docId = docId; - this.#document = ownerDocument; - } - - get #cache() { - return (this.#_cache ||= new Map()); - } - - get #hcmCache() { - return (this.#_hcmCache ||= new Map()); - } - - get #defs() { - if (!this.#_defs) { - const div = this.#document.createElement("div"); - const { style } = div; - style.visibility = "hidden"; - style.contain = "strict"; - style.width = style.height = 0; - style.position = "absolute"; - style.top = style.left = 0; - style.zIndex = -1; - - const svg = this.#document.createElementNS(SVG_NS, "svg"); - svg.setAttribute("width", 0); - svg.setAttribute("height", 0); - this.#_defs = this.#document.createElementNS(SVG_NS, "defs"); - div.append(svg); - svg.append(this.#_defs); - this.#document.body.append(div); - } - return this.#_defs; - } - - #createTables(maps) { - if (maps.length === 1) { - const mapR = maps[0]; - const buffer = new Array(256); - for (let i = 0; i < 256; i++) { - buffer[i] = mapR[i] / 255; - } - - const table = buffer.join(","); - return [table, table, table]; - } - - const [mapR, mapG, mapB] = maps; - const bufferR = new Array(256); - const bufferG = new Array(256); - const bufferB = new Array(256); - for (let i = 0; i < 256; i++) { - bufferR[i] = mapR[i] / 255; - bufferG[i] = mapG[i] / 255; - bufferB[i] = mapB[i] / 255; - } - return [bufferR.join(","), bufferG.join(","), bufferB.join(",")]; - } - - #createUrl(id) { - if (this.#baseUrl === undefined) { - // Unless a ``-element is present a relative URL should work. - this.#baseUrl = ""; - - const url = this.#document.URL; - if (url !== this.#document.baseURI) { - if (isDataScheme(url)) { - warn('#createUrl: ignore "data:"-URL for performance reasons.'); - } else { - this.#baseUrl = url.split("#", 1)[0]; - } - } - } - return `url(${this.#baseUrl}#${id})`; - } - - addFilter(maps) { - if (!maps) { - return "none"; - } - - // When a page is zoomed the page is re-drawn but the maps are likely - // the same. - let value = this.#cache.get(maps); - if (value) { - return value; - } - - const [tableR, tableG, tableB] = this.#createTables(maps); - const key = maps.length === 1 ? tableR : `${tableR}${tableG}${tableB}`; - - value = this.#cache.get(key); - if (value) { - this.#cache.set(maps, value); - return value; - } - - // We create a SVG filter: feComponentTransferElement - // https://www.w3.org/TR/SVG11/filters.html#feComponentTransferElement - - const id = `g_${this.#docId}_transfer_map_${this.#id++}`; - const url = this.#createUrl(id); - this.#cache.set(maps, url); - this.#cache.set(key, url); - - const filter = this.#createFilter(id); - this.#addTransferMapConversion(tableR, tableG, tableB, filter); - - return url; - } - - addHCMFilter(fgColor, bgColor) { - const key = `${fgColor}-${bgColor}`; - const filterName = "base"; - let info = this.#hcmCache.get(filterName); - if (info?.key === key) { - return info.url; - } - - if (info) { - info.filter?.remove(); - info.key = key; - info.url = "none"; - info.filter = null; - } else { - info = { - key, - url: "none", - filter: null, - }; - this.#hcmCache.set(filterName, info); - } - - if (!fgColor || !bgColor) { - return info.url; - } - - const fgRGB = this.#getRGB(fgColor); - fgColor = Util.makeHexColor(...fgRGB); - const bgRGB = this.#getRGB(bgColor); - bgColor = Util.makeHexColor(...bgRGB); - this.#defs.style.color = ""; - - if ( - (fgColor === "#000000" && bgColor === "#ffffff") || - fgColor === bgColor - ) { - return info.url; - } - - // https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_Colors_and_Luminance - // - // Relative luminance: - // https://www.w3.org/TR/WCAG20/#relativeluminancedef - // - // We compute the rounded luminance of the default background color. - // Then for every color in the pdf, if its rounded luminance is the - // same as the background one then it's replaced by the new - // background color else by the foreground one. - const map = new Array(256); - for (let i = 0; i <= 255; i++) { - const x = i / 255; - map[i] = x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4; - } - const table = map.join(","); - - const id = `g_${this.#docId}_hcm_filter`; - const filter = (info.filter = this.#createFilter(id)); - this.#addTransferMapConversion(table, table, table, filter); - this.#addGrayConversion(filter); - - const getSteps = (c, n) => { - const start = fgRGB[c] / 255; - const end = bgRGB[c] / 255; - const arr = new Array(n + 1); - for (let i = 0; i <= n; i++) { - arr[i] = start + (i / n) * (end - start); - } - return arr.join(","); - }; - this.#addTransferMapConversion( - getSteps(0, 5), - getSteps(1, 5), - getSteps(2, 5), - filter - ); - - info.url = this.#createUrl(id); - return info.url; - } - - addAlphaFilter(map) { - // When a page is zoomed the page is re-drawn but the maps are likely - // the same. - let value = this.#cache.get(map); - if (value) { - return value; - } - - const [tableA] = this.#createTables([map]); - const key = `alpha_${tableA}`; - - value = this.#cache.get(key); - if (value) { - this.#cache.set(map, value); - return value; - } - - const id = `g_${this.#docId}_alpha_map_${this.#id++}`; - const url = this.#createUrl(id); - this.#cache.set(map, url); - this.#cache.set(key, url); - - const filter = this.#createFilter(id); - this.#addTransferMapAlphaConversion(tableA, filter); - - return url; - } - - addLuminosityFilter(map) { - // When a page is zoomed the page is re-drawn but the maps are likely - // the same. - let value = this.#cache.get(map || "luminosity"); - if (value) { - return value; - } - - let tableA, key; - if (map) { - [tableA] = this.#createTables([map]); - key = `luminosity_${tableA}`; - } else { - key = "luminosity"; - } - - value = this.#cache.get(key); - if (value) { - this.#cache.set(map, value); - return value; - } - - const id = `g_${this.#docId}_luminosity_map_${this.#id++}`; - const url = this.#createUrl(id); - this.#cache.set(map, url); - this.#cache.set(key, url); - - const filter = this.#createFilter(id); - this.#addLuminosityConversion(filter); - if (map) { - this.#addTransferMapAlphaConversion(tableA, filter); - } - - return url; - } - - addHighlightHCMFilter(filterName, fgColor, bgColor, newFgColor, newBgColor) { - const key = `${fgColor}-${bgColor}-${newFgColor}-${newBgColor}`; - let info = this.#hcmCache.get(filterName); - if (info?.key === key) { - return info.url; - } - - if (info) { - info.filter?.remove(); - info.key = key; - info.url = "none"; - info.filter = null; - } else { - info = { - key, - url: "none", - filter: null, - }; - this.#hcmCache.set(filterName, info); - } - - if (!fgColor || !bgColor) { - return info.url; - } - - const [fgRGB, bgRGB] = [fgColor, bgColor].map(this.#getRGB.bind(this)); - let fgGray = Math.round( - 0.2126 * fgRGB[0] + 0.7152 * fgRGB[1] + 0.0722 * fgRGB[2] - ); - let bgGray = Math.round( - 0.2126 * bgRGB[0] + 0.7152 * bgRGB[1] + 0.0722 * bgRGB[2] - ); - let [newFgRGB, newBgRGB] = [newFgColor, newBgColor].map( - this.#getRGB.bind(this) - ); - if (bgGray < fgGray) { - [fgGray, bgGray, newFgRGB, newBgRGB] = [ - bgGray, - fgGray, - newBgRGB, - newFgRGB, - ]; - } - this.#defs.style.color = ""; - - // Now we can create the filters to highlight some canvas parts. - // The colors in the pdf will almost be Canvas and CanvasText, hence we - // want to filter them to finally get Highlight and HighlightText. - // Since we're in HCM the background color and the foreground color should - // be really different when converted to grayscale (if they're not then it - // means that we've a poor contrast). Once the canvas colors are converted - // to grayscale we can easily map them on their new colors. - // The grayscale step is important because if we've something like: - // fgColor = #FF.... - // bgColor = #FF.... - // then we are enable to map the red component on the new red components - // which can be different. - - const getSteps = (fg, bg, n) => { - const arr = new Array(256); - const step = (bgGray - fgGray) / n; - const newStart = fg / 255; - const newStep = (bg - fg) / (255 * n); - let prev = 0; - for (let i = 0; i <= n; i++) { - const k = Math.round(fgGray + i * step); - const value = newStart + i * newStep; - for (let j = prev; j <= k; j++) { - arr[j] = value; - } - prev = k + 1; - } - for (let i = prev; i < 256; i++) { - arr[i] = arr[prev - 1]; - } - return arr.join(","); - }; - - const id = `g_${this.#docId}_hcm_${filterName}_filter`; - const filter = (info.filter = this.#createFilter(id)); - - this.#addGrayConversion(filter); - this.#addTransferMapConversion( - getSteps(newFgRGB[0], newBgRGB[0], 5), - getSteps(newFgRGB[1], newBgRGB[1], 5), - getSteps(newFgRGB[2], newBgRGB[2], 5), - filter - ); - - info.url = this.#createUrl(id); - return info.url; - } - - destroy(keepHCM = false) { - if (keepHCM && this.#hcmCache.size !== 0) { - return; - } - if (this.#_defs) { - this.#_defs.parentNode.parentNode.remove(); - this.#_defs = null; - } - if (this.#_cache) { - this.#_cache.clear(); - this.#_cache = null; - } - this.#id = 0; - } - - #addLuminosityConversion(filter) { - const feColorMatrix = this.#document.createElementNS( - SVG_NS, - "feColorMatrix" - ); - feColorMatrix.setAttribute("type", "matrix"); - feColorMatrix.setAttribute( - "values", - "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0.59 0.11 0 0" - ); - filter.append(feColorMatrix); - } - - #addGrayConversion(filter) { - const feColorMatrix = this.#document.createElementNS( - SVG_NS, - "feColorMatrix" - ); - feColorMatrix.setAttribute("type", "matrix"); - feColorMatrix.setAttribute( - "values", - "0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0" - ); - filter.append(feColorMatrix); - } - - #createFilter(id) { - const filter = this.#document.createElementNS(SVG_NS, "filter"); - filter.setAttribute("color-interpolation-filters", "sRGB"); - filter.setAttribute("id", id); - this.#defs.append(filter); - - return filter; - } - - #appendFeFunc(feComponentTransfer, func, table) { - const feFunc = this.#document.createElementNS(SVG_NS, func); - feFunc.setAttribute("type", "discrete"); - feFunc.setAttribute("tableValues", table); - feComponentTransfer.append(feFunc); - } - - #addTransferMapConversion(rTable, gTable, bTable, filter) { - const feComponentTransfer = this.#document.createElementNS( - SVG_NS, - "feComponentTransfer" - ); - filter.append(feComponentTransfer); - this.#appendFeFunc(feComponentTransfer, "feFuncR", rTable); - this.#appendFeFunc(feComponentTransfer, "feFuncG", gTable); - this.#appendFeFunc(feComponentTransfer, "feFuncB", bTable); - } - - #addTransferMapAlphaConversion(aTable, filter) { - const feComponentTransfer = this.#document.createElementNS( - SVG_NS, - "feComponentTransfer" - ); - filter.append(feComponentTransfer); - this.#appendFeFunc(feComponentTransfer, "feFuncA", aTable); - } - - #getRGB(color) { - this.#defs.style.color = color; - return getRGB(getComputedStyle(this.#defs).getPropertyValue("color")); - } -} - -class DOMCanvasFactory extends BaseCanvasFactory { - constructor({ ownerDocument = globalThis.document, enableHWA = false }) { - super({ enableHWA }); - this._document = ownerDocument; - } - - /** - * @ignore - */ - _createCanvas(width, height) { - const canvas = this._document.createElement("canvas"); - canvas.width = width; - canvas.height = height; - return canvas; - } -} - async function fetchData(url, type = "text") { if ( (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) || @@ -560,40 +79,6 @@ async function fetchData(url, type = "text") { }); } -class DOMCMapReaderFactory extends BaseCMapReaderFactory { - /** - * @ignore - */ - async _fetch(url) { - const data = await fetchData( - url, - /* type = */ this.isCompressed ? "arraybuffer" : "text" - ); - return data instanceof ArrayBuffer - ? new Uint8Array(data) - : stringToBytes(data); - } -} - -class DOMStandardFontDataFactory extends BaseStandardFontDataFactory { - /** - * @ignore - */ - async _fetch(url) { - const data = await fetchData(url, /* type = */ "arraybuffer"); - return new Uint8Array(data); - } -} - -class DOMSVGFactory extends BaseSVGFactory { - /** - * @ignore - */ - _createSVG(type) { - return document.createElementNS(SVG_NS, type); - } -} - /** * @typedef {Object} PageViewportParameters * @property {Array} viewBox - The xMin, yMin, xMax and @@ -1152,11 +637,6 @@ class OutputScale { export { deprecated, - DOMCanvasFactory, - DOMCMapReaderFactory, - DOMFilterFactory, - DOMStandardFontDataFactory, - DOMSVGFactory, fetchData, getColorValues, getCurrentTransform, @@ -1176,4 +656,5 @@ export { RenderingCancelledException, setLayerDimensions, StatTimer, + SVG_NS, }; diff --git a/src/display/draw_layer.js b/src/display/draw_layer.js index 34e4c02053d41..4e649f93e9085 100644 --- a/src/display/draw_layer.js +++ b/src/display/draw_layer.js @@ -13,7 +13,7 @@ * limitations under the License. */ -import { DOMSVGFactory } from "./display_utils.js"; +import { DOMSVGFactory } from "./svg_factory.js"; import { shadow } from "../shared/util.js"; /** diff --git a/src/display/filter_factory.js b/src/display/filter_factory.js new file mode 100644 index 0000000000000..84260464ef61a --- /dev/null +++ b/src/display/filter_factory.js @@ -0,0 +1,508 @@ +/* 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 { getRGB, isDataScheme, SVG_NS } from "./display_utils.js"; +import { unreachable, Util, warn } from "../shared/util.js"; + +class BaseFilterFactory { + constructor() { + if ( + (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && + this.constructor === BaseFilterFactory + ) { + unreachable("Cannot initialize BaseFilterFactory."); + } + } + + addFilter(maps) { + return "none"; + } + + addHCMFilter(fgColor, bgColor) { + return "none"; + } + + addAlphaFilter(map) { + return "none"; + } + + addLuminosityFilter(map) { + return "none"; + } + + addHighlightHCMFilter(filterName, fgColor, bgColor, newFgColor, newBgColor) { + return "none"; + } + + destroy(keepHCM = false) {} +} + +/** + * FilterFactory aims to create some SVG filters we can use when drawing an + * image (or whatever) on a canvas. + * Filters aren't applied with ctx.putImageData because it just overwrites the + * underlying pixels. + * With these filters, it's possible for example to apply some transfer maps on + * an image without the need to apply them on the pixel arrays: the renderer + * does the magic for us. + */ +class DOMFilterFactory extends BaseFilterFactory { + #baseUrl; + + #_cache; + + #_defs; + + #docId; + + #document; + + #_hcmCache; + + #id = 0; + + constructor({ docId, ownerDocument = globalThis.document }) { + super(); + this.#docId = docId; + this.#document = ownerDocument; + } + + get #cache() { + return (this.#_cache ||= new Map()); + } + + get #hcmCache() { + return (this.#_hcmCache ||= new Map()); + } + + get #defs() { + if (!this.#_defs) { + const div = this.#document.createElement("div"); + const { style } = div; + style.visibility = "hidden"; + style.contain = "strict"; + style.width = style.height = 0; + style.position = "absolute"; + style.top = style.left = 0; + style.zIndex = -1; + + const svg = this.#document.createElementNS(SVG_NS, "svg"); + svg.setAttribute("width", 0); + svg.setAttribute("height", 0); + this.#_defs = this.#document.createElementNS(SVG_NS, "defs"); + div.append(svg); + svg.append(this.#_defs); + this.#document.body.append(div); + } + return this.#_defs; + } + + #createTables(maps) { + if (maps.length === 1) { + const mapR = maps[0]; + const buffer = new Array(256); + for (let i = 0; i < 256; i++) { + buffer[i] = mapR[i] / 255; + } + + const table = buffer.join(","); + return [table, table, table]; + } + + const [mapR, mapG, mapB] = maps; + const bufferR = new Array(256); + const bufferG = new Array(256); + const bufferB = new Array(256); + for (let i = 0; i < 256; i++) { + bufferR[i] = mapR[i] / 255; + bufferG[i] = mapG[i] / 255; + bufferB[i] = mapB[i] / 255; + } + return [bufferR.join(","), bufferG.join(","), bufferB.join(",")]; + } + + #createUrl(id) { + if (this.#baseUrl === undefined) { + // Unless a ``-element is present a relative URL should work. + this.#baseUrl = ""; + + const url = this.#document.URL; + if (url !== this.#document.baseURI) { + if (isDataScheme(url)) { + warn('#createUrl: ignore "data:"-URL for performance reasons.'); + } else { + this.#baseUrl = url.split("#", 1)[0]; + } + } + } + return `url(${this.#baseUrl}#${id})`; + } + + addFilter(maps) { + if (!maps) { + return "none"; + } + + // When a page is zoomed the page is re-drawn but the maps are likely + // the same. + let value = this.#cache.get(maps); + if (value) { + return value; + } + + const [tableR, tableG, tableB] = this.#createTables(maps); + const key = maps.length === 1 ? tableR : `${tableR}${tableG}${tableB}`; + + value = this.#cache.get(key); + if (value) { + this.#cache.set(maps, value); + return value; + } + + // We create a SVG filter: feComponentTransferElement + // https://www.w3.org/TR/SVG11/filters.html#feComponentTransferElement + + const id = `g_${this.#docId}_transfer_map_${this.#id++}`; + const url = this.#createUrl(id); + this.#cache.set(maps, url); + this.#cache.set(key, url); + + const filter = this.#createFilter(id); + this.#addTransferMapConversion(tableR, tableG, tableB, filter); + + return url; + } + + addHCMFilter(fgColor, bgColor) { + const key = `${fgColor}-${bgColor}`; + const filterName = "base"; + let info = this.#hcmCache.get(filterName); + if (info?.key === key) { + return info.url; + } + + if (info) { + info.filter?.remove(); + info.key = key; + info.url = "none"; + info.filter = null; + } else { + info = { + key, + url: "none", + filter: null, + }; + this.#hcmCache.set(filterName, info); + } + + if (!fgColor || !bgColor) { + return info.url; + } + + const fgRGB = this.#getRGB(fgColor); + fgColor = Util.makeHexColor(...fgRGB); + const bgRGB = this.#getRGB(bgColor); + bgColor = Util.makeHexColor(...bgRGB); + this.#defs.style.color = ""; + + if ( + (fgColor === "#000000" && bgColor === "#ffffff") || + fgColor === bgColor + ) { + return info.url; + } + + // https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_Colors_and_Luminance + // + // Relative luminance: + // https://www.w3.org/TR/WCAG20/#relativeluminancedef + // + // We compute the rounded luminance of the default background color. + // Then for every color in the pdf, if its rounded luminance is the + // same as the background one then it's replaced by the new + // background color else by the foreground one. + const map = new Array(256); + for (let i = 0; i <= 255; i++) { + const x = i / 255; + map[i] = x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4; + } + const table = map.join(","); + + const id = `g_${this.#docId}_hcm_filter`; + const filter = (info.filter = this.#createFilter(id)); + this.#addTransferMapConversion(table, table, table, filter); + this.#addGrayConversion(filter); + + const getSteps = (c, n) => { + const start = fgRGB[c] / 255; + const end = bgRGB[c] / 255; + const arr = new Array(n + 1); + for (let i = 0; i <= n; i++) { + arr[i] = start + (i / n) * (end - start); + } + return arr.join(","); + }; + this.#addTransferMapConversion( + getSteps(0, 5), + getSteps(1, 5), + getSteps(2, 5), + filter + ); + + info.url = this.#createUrl(id); + return info.url; + } + + addAlphaFilter(map) { + // When a page is zoomed the page is re-drawn but the maps are likely + // the same. + let value = this.#cache.get(map); + if (value) { + return value; + } + + const [tableA] = this.#createTables([map]); + const key = `alpha_${tableA}`; + + value = this.#cache.get(key); + if (value) { + this.#cache.set(map, value); + return value; + } + + const id = `g_${this.#docId}_alpha_map_${this.#id++}`; + const url = this.#createUrl(id); + this.#cache.set(map, url); + this.#cache.set(key, url); + + const filter = this.#createFilter(id); + this.#addTransferMapAlphaConversion(tableA, filter); + + return url; + } + + addLuminosityFilter(map) { + // When a page is zoomed the page is re-drawn but the maps are likely + // the same. + let value = this.#cache.get(map || "luminosity"); + if (value) { + return value; + } + + let tableA, key; + if (map) { + [tableA] = this.#createTables([map]); + key = `luminosity_${tableA}`; + } else { + key = "luminosity"; + } + + value = this.#cache.get(key); + if (value) { + this.#cache.set(map, value); + return value; + } + + const id = `g_${this.#docId}_luminosity_map_${this.#id++}`; + const url = this.#createUrl(id); + this.#cache.set(map, url); + this.#cache.set(key, url); + + const filter = this.#createFilter(id); + this.#addLuminosityConversion(filter); + if (map) { + this.#addTransferMapAlphaConversion(tableA, filter); + } + + return url; + } + + addHighlightHCMFilter(filterName, fgColor, bgColor, newFgColor, newBgColor) { + const key = `${fgColor}-${bgColor}-${newFgColor}-${newBgColor}`; + let info = this.#hcmCache.get(filterName); + if (info?.key === key) { + return info.url; + } + + if (info) { + info.filter?.remove(); + info.key = key; + info.url = "none"; + info.filter = null; + } else { + info = { + key, + url: "none", + filter: null, + }; + this.#hcmCache.set(filterName, info); + } + + if (!fgColor || !bgColor) { + return info.url; + } + + const [fgRGB, bgRGB] = [fgColor, bgColor].map(this.#getRGB.bind(this)); + let fgGray = Math.round( + 0.2126 * fgRGB[0] + 0.7152 * fgRGB[1] + 0.0722 * fgRGB[2] + ); + let bgGray = Math.round( + 0.2126 * bgRGB[0] + 0.7152 * bgRGB[1] + 0.0722 * bgRGB[2] + ); + let [newFgRGB, newBgRGB] = [newFgColor, newBgColor].map( + this.#getRGB.bind(this) + ); + if (bgGray < fgGray) { + [fgGray, bgGray, newFgRGB, newBgRGB] = [ + bgGray, + fgGray, + newBgRGB, + newFgRGB, + ]; + } + this.#defs.style.color = ""; + + // Now we can create the filters to highlight some canvas parts. + // The colors in the pdf will almost be Canvas and CanvasText, hence we + // want to filter them to finally get Highlight and HighlightText. + // Since we're in HCM the background color and the foreground color should + // be really different when converted to grayscale (if they're not then it + // means that we've a poor contrast). Once the canvas colors are converted + // to grayscale we can easily map them on their new colors. + // The grayscale step is important because if we've something like: + // fgColor = #FF.... + // bgColor = #FF.... + // then we are enable to map the red component on the new red components + // which can be different. + + const getSteps = (fg, bg, n) => { + const arr = new Array(256); + const step = (bgGray - fgGray) / n; + const newStart = fg / 255; + const newStep = (bg - fg) / (255 * n); + let prev = 0; + for (let i = 0; i <= n; i++) { + const k = Math.round(fgGray + i * step); + const value = newStart + i * newStep; + for (let j = prev; j <= k; j++) { + arr[j] = value; + } + prev = k + 1; + } + for (let i = prev; i < 256; i++) { + arr[i] = arr[prev - 1]; + } + return arr.join(","); + }; + + const id = `g_${this.#docId}_hcm_${filterName}_filter`; + const filter = (info.filter = this.#createFilter(id)); + + this.#addGrayConversion(filter); + this.#addTransferMapConversion( + getSteps(newFgRGB[0], newBgRGB[0], 5), + getSteps(newFgRGB[1], newBgRGB[1], 5), + getSteps(newFgRGB[2], newBgRGB[2], 5), + filter + ); + + info.url = this.#createUrl(id); + return info.url; + } + + destroy(keepHCM = false) { + if (keepHCM && this.#hcmCache.size !== 0) { + return; + } + if (this.#_defs) { + this.#_defs.parentNode.parentNode.remove(); + this.#_defs = null; + } + if (this.#_cache) { + this.#_cache.clear(); + this.#_cache = null; + } + this.#id = 0; + } + + #addLuminosityConversion(filter) { + const feColorMatrix = this.#document.createElementNS( + SVG_NS, + "feColorMatrix" + ); + feColorMatrix.setAttribute("type", "matrix"); + feColorMatrix.setAttribute( + "values", + "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0.59 0.11 0 0" + ); + filter.append(feColorMatrix); + } + + #addGrayConversion(filter) { + const feColorMatrix = this.#document.createElementNS( + SVG_NS, + "feColorMatrix" + ); + feColorMatrix.setAttribute("type", "matrix"); + feColorMatrix.setAttribute( + "values", + "0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0" + ); + filter.append(feColorMatrix); + } + + #createFilter(id) { + const filter = this.#document.createElementNS(SVG_NS, "filter"); + filter.setAttribute("color-interpolation-filters", "sRGB"); + filter.setAttribute("id", id); + this.#defs.append(filter); + + return filter; + } + + #appendFeFunc(feComponentTransfer, func, table) { + const feFunc = this.#document.createElementNS(SVG_NS, func); + feFunc.setAttribute("type", "discrete"); + feFunc.setAttribute("tableValues", table); + feComponentTransfer.append(feFunc); + } + + #addTransferMapConversion(rTable, gTable, bTable, filter) { + const feComponentTransfer = this.#document.createElementNS( + SVG_NS, + "feComponentTransfer" + ); + filter.append(feComponentTransfer); + this.#appendFeFunc(feComponentTransfer, "feFuncR", rTable); + this.#appendFeFunc(feComponentTransfer, "feFuncG", gTable); + this.#appendFeFunc(feComponentTransfer, "feFuncB", bTable); + } + + #addTransferMapAlphaConversion(aTable, filter) { + const feComponentTransfer = this.#document.createElementNS( + SVG_NS, + "feComponentTransfer" + ); + filter.append(feComponentTransfer); + this.#appendFeFunc(feComponentTransfer, "feFuncA", aTable); + } + + #getRGB(color) { + this.#defs.style.color = color; + return getRGB(getComputedStyle(this.#defs).getPropertyValue("color")); + } +} + +export { BaseFilterFactory, DOMFilterFactory }; diff --git a/src/display/node_utils.js b/src/display/node_utils.js index c0b1e6e2ce529..037bea2419649 100644 --- a/src/display/node_utils.js +++ b/src/display/node_utils.js @@ -13,13 +13,11 @@ * limitations under the License. */ -import { - BaseCanvasFactory, - BaseCMapReaderFactory, - BaseFilterFactory, - BaseStandardFontDataFactory, -} from "./base_factory.js"; import { isNodeJS, warn } from "../shared/util.js"; +import { BaseCanvasFactory } from "./canvas_factory.js"; +import { BaseCMapReaderFactory } from "./cmap_reader_factory.js"; +import { BaseFilterFactory } from "./filter_factory.js"; +import { BaseStandardFontDataFactory } from "./standard_fontdata_factory.js"; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { throw new Error( diff --git a/src/display/standard_fontdata_factory.js b/src/display/standard_fontdata_factory.js new file mode 100644 index 0000000000000..bfd8af3c0cf68 --- /dev/null +++ b/src/display/standard_fontdata_factory.js @@ -0,0 +1,65 @@ +/* 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 { fetchData } from "./display_utils.js"; +import { unreachable } from "../shared/util.js"; + +class BaseStandardFontDataFactory { + constructor({ baseUrl = null }) { + if ( + (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && + this.constructor === BaseStandardFontDataFactory + ) { + unreachable("Cannot initialize BaseStandardFontDataFactory."); + } + this.baseUrl = baseUrl; + } + + async fetch({ filename }) { + if (!this.baseUrl) { + throw new Error( + "Ensure that the `standardFontDataUrl` API parameter is provided." + ); + } + if (!filename) { + throw new Error("Font filename must be specified."); + } + const url = `${this.baseUrl}${filename}`; + + return this._fetch(url).catch(reason => { + throw new Error(`Unable to load font data at: ${url}`); + }); + } + + /** + * @ignore + * @returns {Promise} + */ + async _fetch(url) { + unreachable("Abstract method `_fetch` called."); + } +} + +class DOMStandardFontDataFactory extends BaseStandardFontDataFactory { + /** + * @ignore + */ + async _fetch(url) { + const data = await fetchData(url, /* type = */ "arraybuffer"); + return new Uint8Array(data); + } +} + +export { BaseStandardFontDataFactory, DOMStandardFontDataFactory }; diff --git a/src/display/stubs.js b/src/display/stubs.js index 0b3b9a0bbb788..bea46b67edfe1 100644 --- a/src/display/stubs.js +++ b/src/display/stubs.js @@ -13,6 +13,8 @@ * limitations under the License. */ +const DOMCMapReaderFactory = null; +const DOMStandardFontDataFactory = null; const NodeCanvasFactory = null; const NodeCMapReaderFactory = null; const NodeFilterFactory = null; @@ -23,6 +25,8 @@ const PDFNetworkStream = null; const PDFNodeStream = null; export { + DOMCMapReaderFactory, + DOMStandardFontDataFactory, NodeCanvasFactory, NodeCMapReaderFactory, NodeFilterFactory, diff --git a/src/display/svg_factory.js b/src/display/svg_factory.js new file mode 100644 index 0000000000000..f9591a818499f --- /dev/null +++ b/src/display/svg_factory.js @@ -0,0 +1,71 @@ +/* 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 { SVG_NS } from "./display_utils.js"; +import { unreachable } from "../shared/util.js"; + +class BaseSVGFactory { + constructor() { + if ( + (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && + this.constructor === BaseSVGFactory + ) { + unreachable("Cannot initialize BaseSVGFactory."); + } + } + + create(width, height, skipDimensions = false) { + if (width <= 0 || height <= 0) { + throw new Error("Invalid SVG dimensions"); + } + const svg = this._createSVG("svg:svg"); + svg.setAttribute("version", "1.1"); + + if (!skipDimensions) { + svg.setAttribute("width", `${width}px`); + svg.setAttribute("height", `${height}px`); + } + + svg.setAttribute("preserveAspectRatio", "none"); + svg.setAttribute("viewBox", `0 0 ${width} ${height}`); + + return svg; + } + + createElement(type) { + if (typeof type !== "string") { + throw new Error("Invalid SVG element type"); + } + return this._createSVG(type); + } + + /** + * @ignore + */ + _createSVG(type) { + unreachable("Abstract method `_createSVG` called."); + } +} + +class DOMSVGFactory extends BaseSVGFactory { + /** + * @ignore + */ + _createSVG(type) { + return document.createElementNS(SVG_NS, type); + } +} + +export { BaseSVGFactory, DOMSVGFactory }; diff --git a/src/pdf.js b/src/pdf.js index 1267cf9c3ff2e..2febc4227393f 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -49,7 +49,6 @@ import { version, } from "./display/api.js"; import { - DOMSVGFactory, fetchData, getFilenameFromUrl, getPdfFilenameFromUrl, @@ -67,6 +66,7 @@ import { AnnotationEditorLayer } from "./display/editor/annotation_editor_layer. import { AnnotationEditorUIManager } from "./display/editor/tools.js"; import { AnnotationLayer } from "./display/annotation_layer.js"; import { ColorPicker } from "./display/editor/color_picker.js"; +import { DOMSVGFactory } from "./display/svg_factory.js"; import { DrawLayer } from "./display/draw_layer.js"; import { GlobalWorkerOptions } from "./display/worker_options.js"; import { HighlightOutliner } from "./display/editor/drawers/highlight.js"; diff --git a/test/unit/canvas_factory_spec.js b/test/unit/canvas_factory_spec.js new file mode 100644 index 0000000000000..23a1dba610680 --- /dev/null +++ b/test/unit/canvas_factory_spec.js @@ -0,0 +1,111 @@ +/* Copyright 2017 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 { DOMCanvasFactory } from "../../src/display/canvas_factory.js"; +import { isNodeJS } from "../../src/shared/util.js"; + +describe("canvas_factory", function () { + describe("DOMCanvasFactory", function () { + let canvasFactory; + + beforeAll(function () { + canvasFactory = new DOMCanvasFactory({}); + }); + + afterAll(function () { + canvasFactory = null; + }); + + it("`create` should throw an error if the dimensions are invalid", function () { + // Invalid width. + expect(function () { + return canvasFactory.create(-1, 1); + }).toThrow(new Error("Invalid canvas size")); + + // Invalid height. + expect(function () { + return canvasFactory.create(1, -1); + }).toThrow(new Error("Invalid canvas size")); + }); + + it("`create` should return a canvas if the dimensions are valid", function () { + if (isNodeJS) { + pending("Document is not supported in Node.js."); + } + + const { canvas, context } = canvasFactory.create(20, 40); + expect(canvas instanceof HTMLCanvasElement).toBe(true); + expect(context instanceof CanvasRenderingContext2D).toBe(true); + expect(canvas.width).toBe(20); + expect(canvas.height).toBe(40); + }); + + it("`reset` should throw an error if no canvas is provided", function () { + const canvasAndContext = { canvas: null, context: null }; + + expect(function () { + return canvasFactory.reset(canvasAndContext, 20, 40); + }).toThrow(new Error("Canvas is not specified")); + }); + + it("`reset` should throw an error if the dimensions are invalid", function () { + const canvasAndContext = { canvas: "foo", context: "bar" }; + + // Invalid width. + expect(function () { + return canvasFactory.reset(canvasAndContext, -1, 1); + }).toThrow(new Error("Invalid canvas size")); + + // Invalid height. + expect(function () { + return canvasFactory.reset(canvasAndContext, 1, -1); + }).toThrow(new Error("Invalid canvas size")); + }); + + it("`reset` should alter the canvas/context if the dimensions are valid", function () { + if (isNodeJS) { + pending("Document is not supported in Node.js."); + } + + const canvasAndContext = canvasFactory.create(20, 40); + canvasFactory.reset(canvasAndContext, 60, 80); + + const { canvas, context } = canvasAndContext; + expect(canvas instanceof HTMLCanvasElement).toBe(true); + expect(context instanceof CanvasRenderingContext2D).toBe(true); + expect(canvas.width).toBe(60); + expect(canvas.height).toBe(80); + }); + + it("`destroy` should throw an error if no canvas is provided", function () { + expect(function () { + return canvasFactory.destroy({}); + }).toThrow(new Error("Canvas is not specified")); + }); + + it("`destroy` should clear the canvas/context", function () { + if (isNodeJS) { + pending("Document is not supported in Node.js."); + } + + const canvasAndContext = canvasFactory.create(20, 40); + canvasFactory.destroy(canvasAndContext); + + const { canvas, context } = canvasAndContext; + expect(canvas).toBe(null); + expect(context).toBe(null); + }); + }); +}); diff --git a/test/unit/clitests.json b/test/unit/clitests.json index 0516f6def31a6..492687985c2c3 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -9,6 +9,7 @@ "api_spec.js", "app_options_spec.js", "bidi_spec.js", + "canvas_factory_spec.js", "cff_parser_spec.js", "cmap_spec.js", "colorspace_spec.js", @@ -42,6 +43,7 @@ "primitives_spec.js", "stream_spec.js", "struct_tree_spec.js", + "svg_factory_spec.js", "text_layer_spec.js", "type1_parser_spec.js", "ui_utils_spec.js", diff --git a/test/unit/display_utils_spec.js b/test/unit/display_utils_spec.js index e152d5d9c5677..8b3f2bb93e47e 100644 --- a/test/unit/display_utils_spec.js +++ b/test/unit/display_utils_spec.js @@ -15,8 +15,6 @@ import { bytesToString, isNodeJS } from "../../src/shared/util.js"; import { - DOMCanvasFactory, - DOMSVGFactory, getFilenameFromUrl, getPdfFilenameFromUrl, isValidFetchUrl, @@ -24,151 +22,6 @@ import { } from "../../src/display/display_utils.js"; describe("display_utils", function () { - describe("DOMCanvasFactory", function () { - let canvasFactory; - - beforeAll(function () { - canvasFactory = new DOMCanvasFactory({}); - }); - - afterAll(function () { - canvasFactory = null; - }); - - it("`create` should throw an error if the dimensions are invalid", function () { - // Invalid width. - expect(function () { - return canvasFactory.create(-1, 1); - }).toThrow(new Error("Invalid canvas size")); - - // Invalid height. - expect(function () { - return canvasFactory.create(1, -1); - }).toThrow(new Error("Invalid canvas size")); - }); - - it("`create` should return a canvas if the dimensions are valid", function () { - if (isNodeJS) { - pending("Document is not supported in Node.js."); - } - - const { canvas, context } = canvasFactory.create(20, 40); - expect(canvas instanceof HTMLCanvasElement).toBe(true); - expect(context instanceof CanvasRenderingContext2D).toBe(true); - expect(canvas.width).toBe(20); - expect(canvas.height).toBe(40); - }); - - it("`reset` should throw an error if no canvas is provided", function () { - const canvasAndContext = { canvas: null, context: null }; - - expect(function () { - return canvasFactory.reset(canvasAndContext, 20, 40); - }).toThrow(new Error("Canvas is not specified")); - }); - - it("`reset` should throw an error if the dimensions are invalid", function () { - const canvasAndContext = { canvas: "foo", context: "bar" }; - - // Invalid width. - expect(function () { - return canvasFactory.reset(canvasAndContext, -1, 1); - }).toThrow(new Error("Invalid canvas size")); - - // Invalid height. - expect(function () { - return canvasFactory.reset(canvasAndContext, 1, -1); - }).toThrow(new Error("Invalid canvas size")); - }); - - it("`reset` should alter the canvas/context if the dimensions are valid", function () { - if (isNodeJS) { - pending("Document is not supported in Node.js."); - } - - const canvasAndContext = canvasFactory.create(20, 40); - canvasFactory.reset(canvasAndContext, 60, 80); - - const { canvas, context } = canvasAndContext; - expect(canvas instanceof HTMLCanvasElement).toBe(true); - expect(context instanceof CanvasRenderingContext2D).toBe(true); - expect(canvas.width).toBe(60); - expect(canvas.height).toBe(80); - }); - - it("`destroy` should throw an error if no canvas is provided", function () { - expect(function () { - return canvasFactory.destroy({}); - }).toThrow(new Error("Canvas is not specified")); - }); - - it("`destroy` should clear the canvas/context", function () { - if (isNodeJS) { - pending("Document is not supported in Node.js."); - } - - const canvasAndContext = canvasFactory.create(20, 40); - canvasFactory.destroy(canvasAndContext); - - const { canvas, context } = canvasAndContext; - expect(canvas).toBe(null); - expect(context).toBe(null); - }); - }); - - describe("DOMSVGFactory", function () { - let svgFactory; - - beforeAll(function () { - svgFactory = new DOMSVGFactory(); - }); - - afterAll(function () { - svgFactory = null; - }); - - it("`create` should throw an error if the dimensions are invalid", function () { - // Invalid width. - expect(function () { - return svgFactory.create(-1, 0); - }).toThrow(new Error("Invalid SVG dimensions")); - - // Invalid height. - expect(function () { - return svgFactory.create(0, -1); - }).toThrow(new Error("Invalid SVG dimensions")); - }); - - it("`create` should return an SVG element if the dimensions are valid", function () { - if (isNodeJS) { - pending("Document is not supported in Node.js."); - } - - const svg = svgFactory.create(20, 40); - expect(svg instanceof SVGSVGElement).toBe(true); - expect(svg.getAttribute("version")).toBe("1.1"); - expect(svg.getAttribute("width")).toBe("20px"); - expect(svg.getAttribute("height")).toBe("40px"); - expect(svg.getAttribute("preserveAspectRatio")).toBe("none"); - expect(svg.getAttribute("viewBox")).toBe("0 0 20 40"); - }); - - it("`createElement` should throw an error if the type is not a string", function () { - expect(function () { - return svgFactory.createElement(true); - }).toThrow(new Error("Invalid SVG element type")); - }); - - it("`createElement` should return an SVG element if the type is valid", function () { - if (isNodeJS) { - pending("Document is not supported in Node.js."); - } - - const svg = svgFactory.createElement("svg:rect"); - expect(svg instanceof SVGRectElement).toBe(true); - }); - }); - describe("getFilenameFromUrl", function () { it("should get the filename from an absolute URL", function () { const url = "https://server.org/filename.pdf"; diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index da98ceeb31377..4c0967dc4bfcc 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -52,6 +52,7 @@ async function initializePDFJS(callback) { "pdfjs-test/unit/api_spec.js", "pdfjs-test/unit/app_options_spec.js", "pdfjs-test/unit/bidi_spec.js", + "pdfjs-test/unit/canvas_factory_spec.js", "pdfjs-test/unit/cff_parser_spec.js", "pdfjs-test/unit/cmap_spec.js", "pdfjs-test/unit/colorspace_spec.js", @@ -86,6 +87,7 @@ async function initializePDFJS(callback) { "pdfjs-test/unit/scripting_spec.js", "pdfjs-test/unit/stream_spec.js", "pdfjs-test/unit/struct_tree_spec.js", + "pdfjs-test/unit/svg_factory_spec.js", "pdfjs-test/unit/text_layer_spec.js", "pdfjs-test/unit/type1_parser_spec.js", "pdfjs-test/unit/ui_utils_spec.js", diff --git a/test/unit/pdf_spec.js b/test/unit/pdf_spec.js index 269a26ff7c60e..f64b97a36bead 100644 --- a/test/unit/pdf_spec.js +++ b/test/unit/pdf_spec.js @@ -41,7 +41,6 @@ import { version, } from "../../src/display/api.js"; import { - DOMSVGFactory, fetchData, getFilenameFromUrl, getPdfFilenameFromUrl, @@ -59,6 +58,7 @@ import { AnnotationEditorLayer } from "../../src/display/editor/annotation_edito import { AnnotationEditorUIManager } from "../../src/display/editor/tools.js"; import { AnnotationLayer } from "../../src/display/annotation_layer.js"; import { ColorPicker } from "../../src/display/editor/color_picker.js"; +import { DOMSVGFactory } from "../../src/display/svg_factory.js"; import { DrawLayer } from "../../src/display/draw_layer.js"; import { GlobalWorkerOptions } from "../../src/display/worker_options.js"; import { TextLayer } from "../../src/display/text_layer.js"; diff --git a/test/unit/svg_factory_spec.js b/test/unit/svg_factory_spec.js new file mode 100644 index 0000000000000..77c5f19dbe379 --- /dev/null +++ b/test/unit/svg_factory_spec.js @@ -0,0 +1,72 @@ +/* Copyright 2017 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 { DOMSVGFactory } from "../../src/display/svg_factory.js"; +import { isNodeJS } from "../../src/shared/util.js"; + +describe("svg_factory", function () { + describe("DOMSVGFactory", function () { + let svgFactory; + + beforeAll(function () { + svgFactory = new DOMSVGFactory(); + }); + + afterAll(function () { + svgFactory = null; + }); + + it("`create` should throw an error if the dimensions are invalid", function () { + // Invalid width. + expect(function () { + return svgFactory.create(-1, 0); + }).toThrow(new Error("Invalid SVG dimensions")); + + // Invalid height. + expect(function () { + return svgFactory.create(0, -1); + }).toThrow(new Error("Invalid SVG dimensions")); + }); + + it("`create` should return an SVG element if the dimensions are valid", function () { + if (isNodeJS) { + pending("Document is not supported in Node.js."); + } + + const svg = svgFactory.create(20, 40); + expect(svg instanceof SVGSVGElement).toBe(true); + expect(svg.getAttribute("version")).toBe("1.1"); + expect(svg.getAttribute("width")).toBe("20px"); + expect(svg.getAttribute("height")).toBe("40px"); + expect(svg.getAttribute("preserveAspectRatio")).toBe("none"); + expect(svg.getAttribute("viewBox")).toBe("0 0 20 40"); + }); + + it("`createElement` should throw an error if the type is not a string", function () { + expect(function () { + return svgFactory.createElement(true); + }).toThrow(new Error("Invalid SVG element type")); + }); + + it("`createElement` should return an SVG element if the type is valid", function () { + if (isNodeJS) { + pending("Document is not supported in Node.js."); + } + + const svg = svgFactory.createElement("svg:rect"); + expect(svg instanceof SVGRectElement).toBe(true); + }); + }); +}); diff --git a/test/unit/unit_test.html b/test/unit/unit_test.html index 2ddbd960a8be1..29c5c93e93406 100644 --- a/test/unit/unit_test.html +++ b/test/unit/unit_test.html @@ -20,6 +20,8 @@ "fluent-dom": "../../node_modules/@fluent/dom/esm/index.js", "cached-iterable": "../../node_modules/cached-iterable/src/index.mjs", + "display-cmap_reader_factory": "../../src/display/cmap_reader_factory.js", + "display-standard_fontdata_factory": "../../src/display/standard_fontdata_factory.js", "display-fetch_stream": "../../src/display/fetch_stream.js", "display-network": "../../src/display/network.js", "display-node_stream": "../../src/display/stubs.js", diff --git a/tsconfig.json b/tsconfig.json index 21d240d7d23dd..3d74d78fc978c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,10 @@ "moduleResolution": "node", "paths": { "pdfjs-lib": ["./src/pdf"], + "display-cmap_reader_factory": ["./src/display/cmap_reader_factory"], + "display-standard_fontdata_factory": [ + "./src/display/standard_fontdata_factory" + ], "display-fetch_stream": ["./src/display/fetch_stream"], "display-network": ["./src/display/network"], "display-node_stream": ["./src/display/node_stream"], diff --git a/web/viewer-geckoview.html b/web/viewer-geckoview.html index e80f528fd9016..80941bd81cd6c 100644 --- a/web/viewer-geckoview.html +++ b/web/viewer-geckoview.html @@ -59,6 +59,8 @@ "fluent-dom": "../node_modules/@fluent/dom/esm/index.js", "cached-iterable": "../node_modules/cached-iterable/src/index.mjs", + "display-cmap_reader_factory": "../src/display/cmap_reader_factory.js", + "display-standard_fontdata_factory": "../src/display/standard_fontdata_factory.js", "display-fetch_stream": "../src/display/fetch_stream.js", "display-network": "../src/display/network.js", "display-node_stream": "../src/display/stubs.js", diff --git a/web/viewer.html b/web/viewer.html index 92345ad211d44..1a8eb7f10dc13 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -62,6 +62,8 @@ "fluent-dom": "../node_modules/@fluent/dom/esm/index.js", "cached-iterable": "../node_modules/cached-iterable/src/index.mjs", + "display-cmap_reader_factory": "../src/display/cmap_reader_factory.js", + "display-standard_fontdata_factory": "../src/display/standard_fontdata_factory.js", "display-fetch_stream": "../src/display/fetch_stream.js", "display-network": "../src/display/network.js", "display-node_stream": "../src/display/stubs.js",