Skip to content

Commit

Permalink
Merge pull request #13146 from calixteman/xfa_fonts
Browse files Browse the repository at this point in the history
XFA -- Load fonts permanently from the pdf
  • Loading branch information
brendandahl authored Apr 16, 2021
2 parents 6db10d1 + 7e95790 commit ac3fa1e
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 21 deletions.
66 changes: 66 additions & 0 deletions src/core/core_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
bytesToString,
objectSize,
stringToPDFString,
warn,
} from "../shared/util.js";
import { Dict, isName, isRef, isStream, RefSet } from "./primitives.js";

Expand Down Expand Up @@ -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 <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 {
collectActions,
encodeToXmlString,
Expand All @@ -391,6 +456,7 @@ export {
readUint16,
readUint32,
toRomanNumerals,
validateCSSFont,
XRefEntryException,
XRefParseException,
};
67 changes: 67 additions & 0 deletions src/core/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ import {
isName,
isRef,
isStream,
Name,
Ref,
} from "./primitives.js";
import {
collectActions,
getInheritableProperty,
isWhiteSpace,
MissingDataException,
validateCSSFont,
XRefEntryException,
XRefParseException,
} from "./core_utils.js";
Expand Down Expand Up @@ -861,6 +863,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,
Expand Down
21 changes: 18 additions & 3 deletions src/core/evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -3572,6 +3586,7 @@ class PartialEvaluator {
flags: descriptor.get("Flags"),
italicAngle: descriptor.get("ItalicAngle"),
isType3Font: false,
cssFontInfo: preEvaluatedFont.cssFontInfo,
};

if (composite) {
Expand Down
42 changes: 26 additions & 16 deletions src/core/fonts.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const EXPORT_DATA_PROPERTIES = [
"bold",
"charProcOperatorList",
"composite",
"cssFontInfo",
"data",
"defaultVMetrics",
"defaultWidth",
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions src/core/pdf_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
11 changes: 11 additions & 0 deletions src/core/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}

Expand Down
28 changes: 26 additions & 2 deletions src/display/font_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
Loading

0 comments on commit ac3fa1e

Please sign in to comment.