Skip to content

Commit

Permalink
Allow loading pdf fonts into another document.
Browse files Browse the repository at this point in the history
  • Loading branch information
jsg2021 committed Aug 7, 2020
1 parent 63e33a5 commit a32ec02
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 19 deletions.
15 changes: 13 additions & 2 deletions src/display/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,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`.
Expand Down Expand Up @@ -281,6 +284,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;
Expand Down Expand Up @@ -975,9 +981,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;
Expand Down Expand Up @@ -1110,7 +1117,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,
});
Expand Down Expand Up @@ -2026,6 +2035,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({
Expand Down Expand Up @@ -2482,6 +2492,7 @@ class WorkerTransport {
pageIndex,
pageInfo,
this,
this._params.ownerDocument,
this._params.pdfBug
);
this.pageCache[pageIndex] = page;
Expand Down
7 changes: 6 additions & 1 deletion src/display/display_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
36 changes: 21 additions & 15 deletions src/display/font_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,33 @@ 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;
}

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);
}
Expand All @@ -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;

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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 */
Expand Down
3 changes: 2 additions & 1 deletion src/display/text_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || [];
Expand Down Expand Up @@ -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")
Expand Down
159 changes: 159 additions & 0 deletions test/unit/custom_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,40 @@ import { getDocument } from "../../src/display/api.js";
import { isNodeJS } from "../../src/shared/is_node.js";
import { NodeCanvasFactory } from "../../src/display/node_utils.js";

const customMatchers = {
toContainElementMatching() {
return {
compare(actual, expected) {
const result = {};
if (typeof actual[Symbol.iterator] !== "function") {
throw new Error("Iterable value required");
}
if (typeof expected !== "function") {
throw new Error("expected a predicate matcher function");
}

actual = Array.from(actual);
const found = actual.find(expected);
result.pass = Boolean(found);

if (result.pass) {
result.message =
"Expected " +
JSON.stringify(actual) +
" not to contain matching element, but found " +
JSON.stringify(found);
} else {
result.message =
"Expected " +
JSON.stringify(actual) +
" to contain a matching element, but none was found";
}
return result;
},
};
},
};

function getTopLeftPixel(canvasContext) {
const imgData = canvasContext.getImageData(0, 0, 1, 1);
return {
Expand Down Expand Up @@ -107,3 +141,128 @@ 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(() => {
jasmine.addMatchers(customMatchers);
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(ownerDocument.fonts).toContainElementMatching(checkFont);
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).toContainElementMatching(checkFontFaceRule);
await doc.destroy();
await loadingTask.destroy();
CanvasFactory.destroy(canvasAndCtx);
expect(style.remove.called).toBe(true);
});
});

0 comments on commit a32ec02

Please sign in to comment.