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:
parent
6cf3070008
commit
7e9579045f
@ -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 <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,
|
||||
@ -391,6 +456,7 @@ export {
|
||||
readUint16,
|
||||
readUint32,
|
||||
toRomanNumerals,
|
||||
validateCSSFont,
|
||||
XRefEntryException,
|
||||
XRefParseException,
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 };
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user