Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XFA -- Load fonts permanently from the pdf #13146

Merged
merged 1 commit into from
Apr 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -42,13 +42,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 @@ -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,
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 @@ -3529,6 +3543,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};`;
calixteman marked this conversation as resolved.
Show resolved Hide resolved
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