diff --git a/src/display/api.js b/src/display/api.js index e4493c6ba..c2c32197d 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -163,6 +163,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. The default value is `false`. @@ -282,6 +285,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; @@ -976,9 +982,10 @@ class PDFDocumentProxy { * Proxy to a `PDFPage` in the worker thread. */ 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; @@ -1111,7 +1118,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, }); @@ -2028,6 +2037,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({ @@ -2484,6 +2494,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 aa744a487..fd5610fe8 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 dcd51230f..3adbcc476 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 544d841dc..fe010d47f 100644 --- a/src/display/text_layer.js +++ b/src/display/text_layer.js @@ -521,6 +521,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 || []; @@ -625,7 +626,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 02e85609c..2839af4a9 100644 --- a/test/unit/custom_spec.js +++ b/test/unit/custom_spec.js @@ -107,3 +107,127 @@ describe("custom canvas rendering", function () { .catch(done.fail); }); }); + +describe("custom ownerDocument", function () { + const FontFace = globalThis.FontFace; + + const checkFont = font => /g_d\d+_f1/.test(font.family); + const checkFontFaceRule = rule => + /^@font-face {font-family:"g_d\d+_f1";src:/.test(rule); + + beforeEach(() => { + globalThis.FontFace = function MockFontFace(name) { + this.family = name; + }; + }); + + afterEach(() => { + globalThis.FontFace = FontFace; + }); + + function getMocks() { + const elements = []; + const createElement = name => { + let element = + typeof document !== "undefined" && document.createElement(name); + if (name === "style") { + element = { + tagName: name, + sheet: { + cssRules: [], + insertRule(rule) { + this.cssRules.push(rule); + }, + }, + }; + Object.assign(element, { + remove() { + this.remove.called = true; + }, + }); + } + elements.push(element); + return element; + }; + const ownerDocument = { + fonts: new Set(), + createElement, + documentElement: { + getElementsByTagName: () => [{ appendChild: () => {} }], + }, + }; + + const CanvasFactory = isNodeJS + ? new NodeCanvasFactory() + : new DOMCanvasFactory({ ownerDocument }); + return { + elements, + ownerDocument, + CanvasFactory, + }; + } + + it("should use given document for loading fonts (with Font Loading API)", async function () { + const { ownerDocument, elements, CanvasFactory } = getMocks(); + const getDocumentParams = buildGetDocumentParams( + "TrueType_without_cmap.pdf", + { + disableFontFace: false, + ownerDocument, + } + ); + + 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).toBeFalsy(); + expect(ownerDocument.fonts.size).toBeGreaterThanOrEqual(1); + expect(Array.from(ownerDocument.fonts).find(checkFont)).toBeTruthy(); + await doc.destroy(); + await loadingTask.destroy(); + CanvasFactory.destroy(canvasAndCtx); + expect(ownerDocument.fonts.size).toBe(0); + }); + + it("should use given document for loading fonts (with CSS rules)", async function () { + const { ownerDocument, elements, CanvasFactory } = getMocks(); + ownerDocument.fonts = null; + const getDocumentParams = buildGetDocumentParams( + "TrueType_without_cmap.pdf", + { + disableFontFace: false, + ownerDocument, + } + ); + + 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).toBeGreaterThanOrEqual(1); + expect(style.sheet.cssRules.find(checkFontFaceRule)).toBeTruthy(); + await doc.destroy(); + await loadingTask.destroy(); + CanvasFactory.destroy(canvasAndCtx); + expect(style.remove.called).toBe(true); + }); +});