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 <jonas.jenwald@gmail.com>

Update src/core/worker.js

Co-authored-by: Jonas Jenwald <jonas.jenwald@gmail.com>
This commit is contained in:
Calixte Denizet 2021-03-26 09:28:18 +01:00
parent 6cf3070008
commit 7e9579045f
8 changed files with 318 additions and 21 deletions

View File

@ -19,6 +19,7 @@ import {
bytesToString, bytesToString,
objectSize, objectSize,
stringToPDFString, stringToPDFString,
warn,
} from "../shared/util.js"; } from "../shared/util.js";
import { Dict, isName, isRef, isStream, RefSet } from "./primitives.js"; import { Dict, isName, isRef, isStream, RefSet } from "./primitives.js";
@ -376,6 +377,70 @@ function encodeToXmlString(str) {
return buffer.join(""); 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 <custom-ident>: ${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 { export {
collectActions, collectActions,
encodeToXmlString, encodeToXmlString,
@ -391,6 +456,7 @@ export {
readUint16, readUint16,
readUint32, readUint32,
toRomanNumerals, toRomanNumerals,
validateCSSFont,
XRefEntryException, XRefEntryException,
XRefParseException, XRefParseException,
}; };

View File

@ -42,6 +42,7 @@ import {
isName, isName,
isRef, isRef,
isStream, isStream,
Name,
Ref, Ref,
} from "./primitives.js"; } from "./primitives.js";
import { import {
@ -49,6 +50,7 @@ import {
getInheritableProperty, getInheritableProperty,
isWhiteSpace, isWhiteSpace,
MissingDataException, MissingDataException,
validateCSSFont,
XRefEntryException, XRefEntryException,
XRefParseException, XRefParseException,
} from "./core_utils.js"; } from "./core_utils.js";
@ -854,6 +856,71 @@ class PDFDocument {
return this.xfaFactory !== null; 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() { get formInfo() {
const formInfo = { const formInfo = {
hasFields: false, hasFields: false,

View File

@ -792,12 +792,19 @@ class PartialEvaluator {
operatorList, operatorList,
task, task,
state, state,
fallbackFontDict = null fallbackFontDict = null,
cssFontInfo = null
) { ) {
const fontName = const fontName =
fontArgs && fontArgs[0] instanceof Name ? fontArgs[0].name : null; 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 => { .then(translated => {
if (!translated.font.isType3Font) { if (!translated.font.isType3Font) {
return translated; 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 () => { const errorFont = async () => {
return new TranslatedFont({ return new TranslatedFont({
loadedName: "g_font_error", loadedName: "g_font_error",
@ -1055,6 +1068,7 @@ class PartialEvaluator {
let preEvaluatedFont; let preEvaluatedFont;
try { try {
preEvaluatedFont = this.preEvaluateFont(font); preEvaluatedFont = this.preEvaluateFont(font);
preEvaluatedFont.cssFontInfo = cssFontInfo;
} catch (reason) { } catch (reason) {
warn(`loadFont - preEvaluateFont failed: "${reason}".`); warn(`loadFont - preEvaluateFont failed: "${reason}".`);
return errorFont(); return errorFont();
@ -3529,6 +3543,7 @@ class PartialEvaluator {
flags: descriptor.get("Flags"), flags: descriptor.get("Flags"),
italicAngle: descriptor.get("ItalicAngle"), italicAngle: descriptor.get("ItalicAngle"),
isType3Font: false, isType3Font: false,
cssFontInfo: preEvaluatedFont.cssFontInfo,
}; };
if (composite) { if (composite) {

View File

@ -95,6 +95,7 @@ const EXPORT_DATA_PROPERTIES = [
"bold", "bold",
"charProcOperatorList", "charProcOperatorList",
"composite", "composite",
"cssFontInfo",
"data", "data",
"defaultVMetrics", "defaultVMetrics",
"defaultWidth", "defaultWidth",
@ -565,6 +566,7 @@ var Font = (function FontClosure() {
this.loadedName = properties.loadedName; this.loadedName = properties.loadedName;
this.isType3Font = properties.isType3Font; this.isType3Font = properties.isType3Font;
this.missingFile = false; this.missingFile = false;
this.cssFontInfo = properties.cssFontInfo;
this.glyphCache = Object.create(null); this.glyphCache = Object.create(null);
@ -2963,23 +2965,31 @@ var Font = (function FontClosure() {
glyphZeroId = 0; glyphZeroId = 0;
} }
// Converting glyphs and ids into font's cmap table // When `cssFontInfo` is set, the font is used to render text in the HTML
var newMapping = adjustMapping(charCodeToGlyphId, hasGlyph, glyphZeroId); // view (e.g. with Xfa) so nothing must be moved in the private area use.
this.toFontChar = newMapping.toFontChar; if (!properties.cssFontInfo) {
tables.cmap = { // Converting glyphs and ids into font's cmap table
tag: "cmap", var newMapping = adjustMapping(
data: createCmapTable(newMapping.charCodeToGlyphId, numGlyphsOut), charCodeToGlyphId,
}; hasGlyph,
glyphZeroId
if (!tables["OS/2"] || !validateOS2Table(tables["OS/2"], font)) { );
tables["OS/2"] = { this.toFontChar = newMapping.toFontChar;
tag: "OS/2", tables.cmap = {
data: createOS2Table( tag: "cmap",
properties, data: createCmapTable(newMapping.charCodeToGlyphId, numGlyphsOut),
newMapping.charCodeToGlyphId,
metricsOverride
),
}; };
if (!tables["OS/2"] || !validateOS2Table(tables["OS/2"], font)) {
tables["OS/2"] = {
tag: "OS/2",
data: createOS2Table(
properties,
newMapping.charCodeToGlyphId,
metricsOverride
),
};
}
} }
if (!isTrueType) { if (!isTrueType) {

View File

@ -73,6 +73,10 @@ class BasePdfManager {
return this.pdfDocument.fontFallback(id, handler); return this.pdfDocument.fontFallback(id, handler);
} }
loadXfaFonts(handler, task) {
return this.pdfDocument.loadXfaFonts(handler, task);
}
cleanup(manuallyTriggered = false) { cleanup(manuallyTriggered = false) {
return this.pdfDocument.cleanup(manuallyTriggered); return this.pdfDocument.cleanup(manuallyTriggered);
} }

View File

@ -193,6 +193,17 @@ class WorkerMessageHandler {
pdfManager.ensureDoc("fingerprint"), pdfManager.ensureDoc("fingerprint"),
pdfManager.ensureDoc("isPureXfa"), 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 }; return { numPages, fingerprint, isPureXfa };
} }

View File

@ -370,7 +370,22 @@ class FontFaceObject {
if (!this.data || this.disableFontFace) { if (!this.data || this.disableFontFace) {
return null; 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) { if (this.fontRegistry) {
this.fontRegistry.registerFont(this); this.fontRegistry.registerFont(this);
@ -385,7 +400,16 @@ class FontFaceObject {
const data = bytesToString(new Uint8Array(this.data)); const data = bytesToString(new Uint8Array(this.data));
// Add the @font-face rule to the document. // Add the @font-face rule to the document.
const url = `url(data:${this.mimetype};base64,${btoa(data)});`; 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) { if (this.fontRegistry) {
this.fontRegistry.registerFont(this, url); this.fontRegistry.registerFont(this, url);

View File

@ -22,6 +22,7 @@ import {
log2, log2,
parseXFAPath, parseXFAPath,
toRomanNumerals, toRomanNumerals,
validateCSSFont,
} from "../../src/core/core_utils.js"; } from "../../src/core/core_utils.js";
import { XRefMock } from "./test_utils.js"; import { XRefMock } from "./test_utils.js";
@ -233,4 +234,103 @@ describe("core_utils", function () {
expect(encodeToXmlString(str)).toEqual(str); 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");
});
});
}); });