mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-19 06:38:07 +02:00
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.
This commit is contained in:
parent
06f3b2d0a6
commit
4e12906061
24 changed files with 1038 additions and 919 deletions
12
gulpfile.mjs
12
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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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<Uint8Array>}
|
||||
*/
|
||||
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<Uint8Array>}
|
||||
*/
|
||||
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,
|
||||
};
|
92
src/display/canvas_factory.js
Normal file
92
src/display/canvas_factory.js
Normal file
|
@ -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 };
|
75
src/display/cmap_reader_factory.js
Normal file
75
src/display/cmap_reader_factory.js
Normal file
|
@ -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<Uint8Array>}
|
||||
*/
|
||||
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 };
|
|
@ -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 `<base>`-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<number>} 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,
|
||||
};
|
||||
|
|
|
@ -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";
|
||||
|
||||
/**
|
||||
|
|
508
src/display/filter_factory.js
Normal file
508
src/display/filter_factory.js
Normal file
|
@ -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 `<base>`-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 };
|
|
@ -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(
|
||||
|
|
65
src/display/standard_fontdata_factory.js
Normal file
65
src/display/standard_fontdata_factory.js
Normal file
|
@ -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<Uint8Array>}
|
||||
*/
|
||||
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 };
|
|
@ -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,
|
||||
|
|
71
src/display/svg_factory.js
Normal file
71
src/display/svg_factory.js
Normal file
|
@ -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 };
|
|
@ -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";
|
||||
|
|
111
test/unit/canvas_factory_spec.js
Normal file
111
test/unit/canvas_factory_spec.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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",
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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";
|
||||
|
|
72
test/unit/svg_factory_spec.js
Normal file
72
test/unit/svg_factory_spec.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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",
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -59,6 +59,8 @@ See https://github.com/adobe-type-tools/cmap-resources
|
|||
"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",
|
||||
|
|
|
@ -62,6 +62,8 @@ See https://github.com/adobe-type-tools/cmap-resources
|
|||
"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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue