From 7e9579045fb8030ac407ce6e08a66217070e58bc Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Fri, 26 Mar 2021 09:28:18 +0100 Subject: [PATCH] XFA -- Load fonts permanently from the pdf - Different fonts can be used in xfa and some of them are embedded in the pdf. - Load all the fonts in window.document. Update src/core/document.js Co-authored-by: Jonas Jenwald Update src/core/worker.js Co-authored-by: Jonas Jenwald --- src/core/core_utils.js | 66 +++++++++++++++++++++++ src/core/document.js | 67 +++++++++++++++++++++++ src/core/evaluator.js | 21 ++++++-- src/core/fonts.js | 42 +++++++++------ src/core/pdf_manager.js | 4 ++ src/core/worker.js | 11 ++++ src/display/font_loader.js | 28 +++++++++- test/unit/core_utils_spec.js | 100 +++++++++++++++++++++++++++++++++++ 8 files changed, 318 insertions(+), 21 deletions(-) diff --git a/src/core/core_utils.js b/src/core/core_utils.js index c80dc4b52..efe7d1197 100644 --- a/src/core/core_utils.js +++ b/src/core/core_utils.js @@ -19,6 +19,7 @@ import { bytesToString, objectSize, stringToPDFString, + warn, } from "../shared/util.js"; import { Dict, isName, isRef, isStream, RefSet } from "./primitives.js"; @@ -376,6 +377,70 @@ function encodeToXmlString(str) { return buffer.join(""); } +function validateCSSFont(cssFontInfo) { + // See https://developer.mozilla.org/en-US/docs/Web/CSS/font-style. + const DEFAULT_CSS_FONT_OBLIQUE = "14"; + // See https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight. + const DEFAULT_CSS_FONT_WEIGHT = "400"; + const CSS_FONT_WEIGHT_VALUES = new Set([ + "100", + "200", + "300", + "400", + "500", + "600", + "700", + "800", + "900", + "1000", + "normal", + "bold", + "bolder", + "lighter", + ]); + + const { fontFamily, fontWeight, italicAngle } = cssFontInfo; + + // See https://developer.mozilla.org/en-US/docs/Web/CSS/string. + if (/^".*"$/.test(fontFamily)) { + if (/[^\\]"/.test(fontFamily.slice(1, fontFamily.length - 1))) { + warn(`XFA - FontFamily contains some unescaped ": ${fontFamily}.`); + return false; + } + } else if (/^'.*'$/.test(fontFamily)) { + if (/[^\\]'/.test(fontFamily.slice(1, fontFamily.length - 1))) { + warn(`XFA - FontFamily contains some unescaped ': ${fontFamily}.`); + return false; + } + } else { + // See https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident. + for (const ident of fontFamily.split(/[ \t]+/)) { + if ( + /^([0-9]|(-([0-9]|-)))/.test(ident) || + !/^[a-zA-Z0-9\-_\\]+$/.test(ident) + ) { + warn( + `XFA - FontFamily contains some invalid : ${fontFamily}.` + ); + return false; + } + } + } + + const weight = fontWeight ? fontWeight.toString() : ""; + cssFontInfo.fontWeight = CSS_FONT_WEIGHT_VALUES.has(weight) + ? weight + : DEFAULT_CSS_FONT_WEIGHT; + + const angle = parseFloat(italicAngle); + cssFontInfo.italicAngle = + isNaN(angle) || angle < -90 || angle > 90 + ? DEFAULT_CSS_FONT_OBLIQUE + : italicAngle.toString(); + + return true; +} + export { collectActions, encodeToXmlString, @@ -391,6 +456,7 @@ export { readUint16, readUint32, toRomanNumerals, + validateCSSFont, XRefEntryException, XRefParseException, }; diff --git a/src/core/document.js b/src/core/document.js index 27cbede28..881c97491 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -42,6 +42,7 @@ import { isName, isRef, isStream, + Name, Ref, } from "./primitives.js"; import { @@ -49,6 +50,7 @@ import { getInheritableProperty, isWhiteSpace, MissingDataException, + validateCSSFont, XRefEntryException, XRefParseException, } from "./core_utils.js"; @@ -854,6 +856,71 @@ class PDFDocument { return this.xfaFactory !== null; } + async loadXfaFonts(handler, task) { + const acroForm = await this.pdfManager.ensureCatalog("acroForm"); + + const resources = await acroForm.getAsync("DR"); + if (!(resources instanceof Dict)) { + return; + } + const objectLoader = new ObjectLoader(resources, ["Font"], this.xref); + await objectLoader.load(); + + const fontRes = resources.get("Font"); + if (!(fontRes instanceof Dict)) { + return; + } + + const partialEvaluator = new PartialEvaluator({ + xref: this.xref, + handler, + pageIndex: -1, + idFactory: this._globalIdFactory, + fontCache: this.catalog.fontCache, + builtInCMapCache: this.catalog.builtInCMapCache, + }); + const operatorList = new OperatorList(); + const initialState = { + font: null, + clone() { + return this; + }, + }; + + const fonts = new Map(); + fontRes.forEach((fontName, font) => { + fonts.set(fontName, font); + }); + const promises = []; + + for (const [fontName, font] of fonts) { + const descriptor = font.get("FontDescriptor"); + if (descriptor instanceof Dict) { + const fontFamily = descriptor.get("FontFamily"); + const fontWeight = descriptor.get("FontWeight"); + const italicAngle = descriptor.get("ItalicAngle"); + const cssFontInfo = { fontFamily, fontWeight, italicAngle }; + + if (!validateCSSFont(cssFontInfo)) { + continue; + } + + const promise = partialEvaluator.handleSetFont( + resources, + [Name.get(fontName), 1], + /* fontRef = */ null, + operatorList, + task, + initialState, + /* fallbackFontDict = */ null, + /* cssFontInfo = */ cssFontInfo + ); + promises.push(promise.catch(() => {})); + } + } + await Promise.all(promises); + } + get formInfo() { const formInfo = { hasFields: false, diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 3c49f9783..391ba5f84 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -792,12 +792,19 @@ class PartialEvaluator { operatorList, task, state, - fallbackFontDict = null + fallbackFontDict = null, + cssFontInfo = null ) { const fontName = fontArgs && fontArgs[0] instanceof Name ? fontArgs[0].name : null; - return this.loadFont(fontName, fontRef, resources, fallbackFontDict) + return this.loadFont( + fontName, + fontRef, + resources, + fallbackFontDict, + cssFontInfo + ) .then(translated => { if (!translated.font.isType3Font) { return translated; @@ -986,7 +993,13 @@ class PartialEvaluator { }); } - loadFont(fontName, font, resources, fallbackFontDict = null) { + loadFont( + fontName, + font, + resources, + fallbackFontDict = null, + cssFontInfo = null + ) { const errorFont = async () => { return new TranslatedFont({ loadedName: "g_font_error", @@ -1055,6 +1068,7 @@ class PartialEvaluator { let preEvaluatedFont; try { preEvaluatedFont = this.preEvaluateFont(font); + preEvaluatedFont.cssFontInfo = cssFontInfo; } catch (reason) { warn(`loadFont - preEvaluateFont failed: "${reason}".`); return errorFont(); @@ -3529,6 +3543,7 @@ class PartialEvaluator { flags: descriptor.get("Flags"), italicAngle: descriptor.get("ItalicAngle"), isType3Font: false, + cssFontInfo: preEvaluatedFont.cssFontInfo, }; if (composite) { diff --git a/src/core/fonts.js b/src/core/fonts.js index eec37b85d..e38553c6c 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -95,6 +95,7 @@ const EXPORT_DATA_PROPERTIES = [ "bold", "charProcOperatorList", "composite", + "cssFontInfo", "data", "defaultVMetrics", "defaultWidth", @@ -565,6 +566,7 @@ var Font = (function FontClosure() { this.loadedName = properties.loadedName; this.isType3Font = properties.isType3Font; this.missingFile = false; + this.cssFontInfo = properties.cssFontInfo; this.glyphCache = Object.create(null); @@ -2963,23 +2965,31 @@ var Font = (function FontClosure() { glyphZeroId = 0; } - // Converting glyphs and ids into font's cmap table - var newMapping = adjustMapping(charCodeToGlyphId, hasGlyph, glyphZeroId); - this.toFontChar = newMapping.toFontChar; - tables.cmap = { - tag: "cmap", - data: createCmapTable(newMapping.charCodeToGlyphId, numGlyphsOut), - }; - - if (!tables["OS/2"] || !validateOS2Table(tables["OS/2"], font)) { - tables["OS/2"] = { - tag: "OS/2", - data: createOS2Table( - properties, - newMapping.charCodeToGlyphId, - metricsOverride - ), + // When `cssFontInfo` is set, the font is used to render text in the HTML + // view (e.g. with Xfa) so nothing must be moved in the private area use. + if (!properties.cssFontInfo) { + // Converting glyphs and ids into font's cmap table + var newMapping = adjustMapping( + charCodeToGlyphId, + hasGlyph, + glyphZeroId + ); + this.toFontChar = newMapping.toFontChar; + tables.cmap = { + tag: "cmap", + data: createCmapTable(newMapping.charCodeToGlyphId, numGlyphsOut), }; + + if (!tables["OS/2"] || !validateOS2Table(tables["OS/2"], font)) { + tables["OS/2"] = { + tag: "OS/2", + data: createOS2Table( + properties, + newMapping.charCodeToGlyphId, + metricsOverride + ), + }; + } } if (!isTrueType) { diff --git a/src/core/pdf_manager.js b/src/core/pdf_manager.js index 2d851219d..e3932a0da 100644 --- a/src/core/pdf_manager.js +++ b/src/core/pdf_manager.js @@ -73,6 +73,10 @@ class BasePdfManager { return this.pdfDocument.fontFallback(id, handler); } + loadXfaFonts(handler, task) { + return this.pdfDocument.loadXfaFonts(handler, task); + } + cleanup(manuallyTriggered = false) { return this.pdfDocument.cleanup(manuallyTriggered); } diff --git a/src/core/worker.js b/src/core/worker.js index 2011deb4c..11000981f 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -193,6 +193,17 @@ class WorkerMessageHandler { pdfManager.ensureDoc("fingerprint"), pdfManager.ensureDoc("isPureXfa"), ]); + + if (isPureXfa) { + const task = new WorkerTask("Load fonts for Xfa"); + startWorkerTask(task); + await pdfManager + .loadXfaFonts(handler, task) + .catch(reason => { + // Ignore errors, to allow the document to load. + }) + .then(() => finishWorkerTask(task)); + } return { numPages, fingerprint, isPureXfa }; } diff --git a/src/display/font_loader.js b/src/display/font_loader.js index 904168ccb..63d680e7d 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -370,7 +370,22 @@ class FontFaceObject { if (!this.data || this.disableFontFace) { return null; } - const nativeFontFace = new FontFace(this.loadedName, this.data, {}); + let nativeFontFace; + if (!this.cssFontInfo) { + nativeFontFace = new FontFace(this.loadedName, this.data, {}); + } else { + const css = { + weight: this.cssFontInfo.fontWeight, + }; + if (this.cssFontInfo.italicAngle) { + css.style = `oblique ${this.cssFontInfo.italicAngle}deg`; + } + nativeFontFace = new FontFace( + this.cssFontInfo.fontFamily, + this.data, + css + ); + } if (this.fontRegistry) { this.fontRegistry.registerFont(this); @@ -385,7 +400,16 @@ class FontFaceObject { const data = bytesToString(new Uint8Array(this.data)); // Add the @font-face rule to the document. const url = `url(data:${this.mimetype};base64,${btoa(data)});`; - const rule = `@font-face {font-family:"${this.loadedName}";src:${url}}`; + let rule; + if (!this.cssFontInfo) { + rule = `@font-face {font-family:"${this.loadedName}";src:${url}}`; + } else { + let css = `font-weight: ${this.cssFontInfo.fontWeight};`; + if (this.cssFontInfo.italicAngle) { + css += `font-style: oblique ${this.cssFontInfo.italicAngle}deg;`; + } + rule = `@font-face {font-family:"${this.cssFontInfo.fontFamily}";${css}src:${url}}`; + } if (this.fontRegistry) { this.fontRegistry.registerFont(this, url); diff --git a/test/unit/core_utils_spec.js b/test/unit/core_utils_spec.js index 4a597d075..cc5c9e69e 100644 --- a/test/unit/core_utils_spec.js +++ b/test/unit/core_utils_spec.js @@ -22,6 +22,7 @@ import { log2, parseXFAPath, toRomanNumerals, + validateCSSFont, } from "../../src/core/core_utils.js"; import { XRefMock } from "./test_utils.js"; @@ -233,4 +234,103 @@ describe("core_utils", function () { expect(encodeToXmlString(str)).toEqual(str); }); }); + + describe("validateCSSFont", function () { + it("Check font family", function () { + const cssFontInfo = { + fontFamily: `"blah blah " blah blah"`, + fontWeight: 0, + italicAngle: 0, + }; + + expect(validateCSSFont(cssFontInfo)).toEqual(false); + + cssFontInfo.fontFamily = `"blah blah \\" blah blah"`; + expect(validateCSSFont(cssFontInfo)).toEqual(true); + + cssFontInfo.fontFamily = `'blah blah ' blah blah'`; + expect(validateCSSFont(cssFontInfo)).toEqual(false); + + cssFontInfo.fontFamily = `'blah blah \\' blah blah'`; + expect(validateCSSFont(cssFontInfo)).toEqual(true); + + cssFontInfo.fontFamily = `"blah blah `; + expect(validateCSSFont(cssFontInfo)).toEqual(false); + + cssFontInfo.fontFamily = `blah blah"`; + expect(validateCSSFont(cssFontInfo)).toEqual(false); + + cssFontInfo.fontFamily = `'blah blah `; + expect(validateCSSFont(cssFontInfo)).toEqual(false); + + cssFontInfo.fontFamily = `blah blah'`; + expect(validateCSSFont(cssFontInfo)).toEqual(false); + + cssFontInfo.fontFamily = "blah blah blah"; + expect(validateCSSFont(cssFontInfo)).toEqual(true); + + cssFontInfo.fontFamily = "blah 0blah blah"; + expect(validateCSSFont(cssFontInfo)).toEqual(false); + + cssFontInfo.fontFamily = "blah blah -0blah"; + expect(validateCSSFont(cssFontInfo)).toEqual(false); + + cssFontInfo.fontFamily = "blah blah --blah"; + expect(validateCSSFont(cssFontInfo)).toEqual(false); + + cssFontInfo.fontFamily = "blah blah -blah"; + expect(validateCSSFont(cssFontInfo)).toEqual(true); + + cssFontInfo.fontFamily = "blah fdqAJqjHJK23kl23__--Kj blah"; + expect(validateCSSFont(cssFontInfo)).toEqual(true); + + cssFontInfo.fontFamily = "blah fdqAJqjH$JK23kl23__--Kj blah"; + expect(validateCSSFont(cssFontInfo)).toEqual(false); + }); + + it("Check font weight", function () { + const cssFontInfo = { + fontFamily: "blah", + fontWeight: 100, + italicAngle: 0, + }; + + validateCSSFont(cssFontInfo); + expect(cssFontInfo.fontWeight).toEqual("100"); + + cssFontInfo.fontWeight = "700"; + validateCSSFont(cssFontInfo); + expect(cssFontInfo.fontWeight).toEqual("700"); + + cssFontInfo.fontWeight = "normal"; + validateCSSFont(cssFontInfo); + expect(cssFontInfo.fontWeight).toEqual("normal"); + + cssFontInfo.fontWeight = 314; + validateCSSFont(cssFontInfo); + expect(cssFontInfo.fontWeight).toEqual("400"); + }); + + it("Check italic angle", function () { + const cssFontInfo = { + fontFamily: "blah", + fontWeight: 100, + italicAngle: 10, + }; + validateCSSFont(cssFontInfo); + expect(cssFontInfo.italicAngle).toEqual("10"); + + cssFontInfo.italicAngle = -123; + validateCSSFont(cssFontInfo); + expect(cssFontInfo.italicAngle).toEqual("14"); + + cssFontInfo.italicAngle = "91"; + validateCSSFont(cssFontInfo); + expect(cssFontInfo.italicAngle).toEqual("14"); + + cssFontInfo.italicAngle = 2.718; + validateCSSFont(cssFontInfo); + expect(cssFontInfo.italicAngle).toEqual("2.718"); + }); + }); });