From 9b16b8ef7138ff4a165c2d5e0d075aafb11f449e Mon Sep 17 00:00:00 2001 From: Jonathan Grimes Date: Mon, 27 Jul 2020 17:22:45 +0000 Subject: [PATCH] Allow loading pdf fonts into another document. --- src/display/api.js | 15 ++++- src/display/display_utils.js | 7 ++- src/display/font_loader.js | 36 ++++++----- src/display/text_layer.js | 3 +- test/unit/custom_spec.js | 116 +++++++++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+), 19 deletions(-) diff --git a/src/display/api.js b/src/display/api.js index b1fb4ba612807..cbe0d62e5327f 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -152,6 +152,9 @@ function setPDFNetworkStreamFactory(pdfNetworkStreamFactory) { * parsed font data from the worker-thread. This may be useful for debugging * purposes (and backwards compatibility), but note that it will lead to * increased memory usage. The default value is `false`. + * @property {HTMLDocument} [ownerDocument] - Specify an explicit document + * context to create elements with and to load resources, such as fonts, + * into. Defaults to the current document. * @property {boolean} [disableRange] - Disable range request loading * of PDF files. When enabled, and if the server supports partial content * requests, then the PDF will be fetched in chunks. @@ -268,6 +271,9 @@ function getDocument(src) { if (typeof params.disableFontFace !== "boolean") { params.disableFontFace = apiCompatibilityParams.disableFontFace || false; } + if (typeof params.ownerDocument === "undefined") { + params.ownerDocument = globalThis.document; + } if (typeof params.disableRange !== "boolean") { params.disableRange = false; @@ -907,9 +913,10 @@ class PDFDocumentProxy { * @alias PDFPageProxy */ class PDFPageProxy { - constructor(pageIndex, pageInfo, transport, pdfBug = false) { + constructor(pageIndex, pageInfo, transport, ownerDocument, pdfBug = false) { this._pageIndex = pageIndex; this._pageInfo = pageInfo; + this._ownerDocument = ownerDocument; this._transport = transport; this._stats = pdfBug ? new StatTimer() : null; this._pdfBug = pdfBug; @@ -1036,7 +1043,9 @@ class PDFPageProxy { intentState.streamReaderCancelTimeout = null; } - const canvasFactoryInstance = canvasFactory || new DefaultCanvasFactory(); + const canvasFactoryInstance = + canvasFactory || + new DefaultCanvasFactory({ ownerDocument: this._ownerDocument }); const webGLContext = new WebGLContext({ enable: enableWebGL, }); @@ -1944,6 +1953,7 @@ class WorkerTransport { this.fontLoader = new FontLoader({ docId: loadingTask.docId, onUnsupportedFeature: this._onUnsupportedFeature.bind(this), + ownerDocument: params.ownerDocument, }); this._params = params; this.CMapReaderFactory = new params.CMapReaderFactory({ @@ -2387,6 +2397,7 @@ class WorkerTransport { pageIndex, pageInfo, this, + this._params.ownerDocument, this._params.pdfBug ); this.pageCache[pageIndex] = page; diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 80bb9e59a5087..51ae2aeb337ca 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -65,11 +65,16 @@ class BaseCanvasFactory { } class DOMCanvasFactory extends BaseCanvasFactory { + constructor({ ownerDocument = globalThis.document } = {}) { + super(); + this._document = ownerDocument; + } + create(width, height) { if (width <= 0 || height <= 0) { throw new Error("Invalid canvas size"); } - const canvas = document.createElement("canvas"); + const canvas = this._document.createElement("canvas"); const context = canvas.getContext("2d"); canvas.width = width; canvas.height = height; diff --git a/src/display/font_loader.js b/src/display/font_loader.js index dcd51230f8457..3adbcc476ad54 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -25,12 +25,17 @@ import { } from "../shared/util.js"; class BaseFontLoader { - constructor({ docId, onUnsupportedFeature }) { + constructor({ + docId, + onUnsupportedFeature, + ownerDocument = globalThis.document, + }) { if (this.constructor === BaseFontLoader) { unreachable("Cannot initialize BaseFontLoader."); } this.docId = docId; this._onUnsupportedFeature = onUnsupportedFeature; + this._document = ownerDocument; this.nativeFontFaces = []; this.styleElement = null; @@ -38,15 +43,15 @@ class BaseFontLoader { addNativeFontFace(nativeFontFace) { this.nativeFontFaces.push(nativeFontFace); - document.fonts.add(nativeFontFace); + this._document.fonts.add(nativeFontFace); } insertRule(rule) { let styleElement = this.styleElement; if (!styleElement) { - styleElement = this.styleElement = document.createElement("style"); + styleElement = this.styleElement = this._document.createElement("style"); styleElement.id = `PDFJS_FONT_STYLE_TAG_${this.docId}`; - document.documentElement + this._document.documentElement .getElementsByTagName("head")[0] .appendChild(styleElement); } @@ -56,8 +61,8 @@ class BaseFontLoader { } clear() { - this.nativeFontFaces.forEach(function (nativeFontFace) { - document.fonts.delete(nativeFontFace); + this.nativeFontFaces.forEach(nativeFontFace => { + this._document.fonts.delete(nativeFontFace); }); this.nativeFontFaces.length = 0; @@ -116,7 +121,8 @@ class BaseFontLoader { } get isFontLoadingAPISupported() { - const supported = typeof document !== "undefined" && !!document.fonts; + const supported = + typeof this._document !== "undefined" && !!this._document.fonts; return shadow(this, "isFontLoadingAPISupported", supported); } @@ -146,8 +152,8 @@ if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { // PDFJSDev.test('CHROME || GENERIC') FontLoader = class GenericFontLoader extends BaseFontLoader { - constructor(docId) { - super(docId); + constructor(params) { + super(params); this.loadingContext = { requests: [], nextRequestId: 0, @@ -254,7 +260,7 @@ if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { let i, ii; // The temporary canvas is used to determine if fonts are loaded. - const canvas = document.createElement("canvas"); + const canvas = this._document.createElement("canvas"); canvas.width = 1; canvas.height = 1; const ctx = canvas.getContext("2d"); @@ -316,22 +322,22 @@ if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { } names.push(loadTestFontId); - const div = document.createElement("div"); + const div = this._document.createElement("div"); div.style.visibility = "hidden"; div.style.width = div.style.height = "10px"; div.style.position = "absolute"; div.style.top = div.style.left = "0px"; for (i = 0, ii = names.length; i < ii; ++i) { - const span = document.createElement("span"); + const span = this._document.createElement("span"); span.textContent = "Hi"; span.style.fontFamily = names[i]; div.appendChild(span); } - document.body.appendChild(div); + this._document.body.appendChild(div); - isFontReady(loadTestFontId, function () { - document.body.removeChild(div); + isFontReady(loadTestFontId, () => { + this._document.body.removeChild(div); request.complete(); }); /** Hack end */ diff --git a/src/display/text_layer.js b/src/display/text_layer.js index ecf29efd351f7..f0210f02c2386 100644 --- a/src/display/text_layer.js +++ b/src/display/text_layer.js @@ -510,6 +510,7 @@ var renderTextLayer = (function renderTextLayerClosure() { this._textContent = textContent; this._textContentStream = textContentStream; this._container = container; + this._document = container.ownerDocument; this._viewport = viewport; this._textDivs = textDivs || []; this._textContentItemsStr = textContentItemsStr || []; @@ -614,7 +615,7 @@ var renderTextLayer = (function renderTextLayerClosure() { let styleCache = Object.create(null); // The temporary canvas is used to measure text length in the DOM. - const canvas = document.createElement("canvas"); + const canvas = this._document.createElement("canvas"); if ( typeof PDFJSDev === "undefined" || PDFJSDev.test("MOZCENTRAL || GENERIC") diff --git a/test/unit/custom_spec.js b/test/unit/custom_spec.js index 02e85609c2d07..5061b05456720 100644 --- a/test/unit/custom_spec.js +++ b/test/unit/custom_spec.js @@ -107,3 +107,119 @@ describe("custom canvas rendering", function () { .catch(done.fail); }); }); + +describe("alternate document context", function () { + const FontFace = global.FontFace; + + let altDocument; + let CanvasFactory; + let elements; + + beforeEach(() => { + global.FontFace = function MockFontFace(name) { + this.family = name; + }; + + elements = []; + const createElement = name => { + const element = { + tagName: name, + remove() { + this.remove.called = true; + }, + }; + if (name === "style") { + element.sheet = { + cssRules: [], + insertRule(rule) { + this.cssRules.push(rule); + }, + }; + } + elements.push(element); + return element; + }; + altDocument = { + fonts: new Set(), + createElement, + documentElement: { + getElementsByTagName: () => [{ appendChild: () => {} }], + }, + }; + + CanvasFactory = isNodeJS + ? new NodeCanvasFactory() + : new DOMCanvasFactory({ ownerDocument: altDocument }); + }); + + afterEach(() => { + global.FontFace = FontFace; + CanvasFactory = null; + elements = null; + }); + + it("should use given document for loading fonts (with Font Loading API)", async function () { + const getDocumentParams = buildGetDocumentParams( + "TrueType_without_cmap.pdf", + { + disableFontFace: false, + ownerDocument: altDocument, + } + ); + + const loadingTask = getDocument(getDocumentParams); + const doc = await loadingTask.promise; + const page = await doc.getPage(1); + + const viewport = page.getViewport({ scale: 1 }); + const canvasAndCtx = CanvasFactory.create(viewport.width, viewport.height); + + await page.render({ + canvasContext: canvasAndCtx.context, + viewport, + }).promise; + + expect(elements).toEqual([]); + expect(altDocument.fonts.size).toBe(1); + const [font] = Array.from(altDocument.fonts); + expect(font.family).toMatch(/g_d\d+_f1/); + + await doc.destroy(); + await loadingTask.destroy(); + CanvasFactory.destroy(canvasAndCtx); + expect(altDocument.fonts.size).toBe(0); + }); + + it("should use given document for loading fonts (with CSS rules)", async function () { + altDocument.fonts = null; + const getDocumentParams = buildGetDocumentParams( + "TrueType_without_cmap.pdf", + { + disableFontFace: false, + ownerDocument: altDocument, + } + ); + + const loadingTask = getDocument(getDocumentParams); + const doc = await loadingTask.promise; + const page = await doc.getPage(1); + + const viewport = page.getViewport({ scale: 1 }); + const canvasAndCtx = CanvasFactory.create(viewport.width, viewport.height); + + await page.render({ + canvasContext: canvasAndCtx.context, + viewport, + }).promise; + + const style = elements.find(element => element.tagName === "style"); + expect(style.sheet.cssRules.length).toBe(1); + expect(style.sheet.cssRules[0]).toMatch( + /^@font-face {font-family:"g_d\d+_f1";src:/ + ); + await doc.destroy(); + await loadingTask.destroy(); + CanvasFactory.destroy(canvasAndCtx); + expect(style.remove.called).toBe(true); + }); +});