Merge pull request #13591 from calixteman/xfa_default_font

XFA - Match font family correctly
This commit is contained in:
calixteman 2021-06-21 09:28:59 +02:00 committed by GitHub
commit 2e6d3d6b00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 221 additions and 57 deletions

View File

@ -926,7 +926,9 @@ class PDFDocument {
if (!(descriptor instanceof Dict)) {
continue;
}
const fontFamily = descriptor.get("FontFamily");
let fontFamily = descriptor.get("FontFamily");
// For example, "Wingdings 3" is not a valid font name in the css specs.
fontFamily = fontFamily.replace(/[ ]+([0-9])/g, "$1");
const fontWeight = descriptor.get("FontWeight");
// Angle is expressed in degrees counterclockwise in PDF
@ -956,6 +958,7 @@ class PDFDocument {
})
);
}
await Promise.all(promises);
this.xfaFactory.setFonts(pdfFonts);
}

View File

@ -836,6 +836,7 @@ function createNameTable(name, proto) {
class Font {
constructor(name, file, properties) {
this.name = name;
this.psName = null;
this.mimetype = null;
this.disableFontFace = false;
@ -2730,6 +2731,7 @@ class Font {
// ... using existing 'name' table as prototype
const namePrototype = readNameTable(tables.name);
tables.name.data = createNameTable(name, namePrototype);
this.psName = namePrototype[0][6] || null;
}
const builder = new OpenTypeFileBuilder(header.version);

View File

@ -13,8 +13,9 @@
* limitations under the License.
*/
import { $fonts, $toHTML } from "./xfa_object.js";
import { $globalData, $toHTML } from "./xfa_object.js";
import { Binder } from "./bind.js";
import { FontFinder } from "./fonts.js";
import { warn } from "../../shared/util.js";
import { XFAParser } from "./parser.js";
@ -23,6 +24,7 @@ class XFAFactory {
try {
this.root = new XFAParser().parse(XFAFactory._createDocument(data));
this.form = new Binder(this.root).bind();
this.form[$globalData].template = this.form;
} catch (e) {
warn(`XFA - an error occured during parsing and binding: ${e}`);
}
@ -56,26 +58,7 @@ class XFAFactory {
}
setFonts(fonts) {
this.form[$fonts] = Object.create(null);
for (const font of fonts) {
const cssFontInfo = font.cssFontInfo;
const name = cssFontInfo.fontFamily;
if (!this.form[$fonts][name]) {
this.form[$fonts][name] = Object.create(null);
}
let property = "regular";
if (cssFontInfo.italicAngle !== "0") {
if (parseFloat(cssFontInfo.fontWeight) >= 700) {
property = "bolditalic";
} else {
property = "italic";
}
} else if (parseFloat(cssFontInfo.fontWeight) >= 700) {
property = "bold";
}
this.form[$fonts][name][property] = font;
}
this.form[$globalData].fontFinder = new FontFinder(fonts);
}
getPages() {

157
src/core/xfa/fonts.js Normal file
View File

@ -0,0 +1,157 @@
/* Copyright 2021 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { warn } from "../../shared/util.js";
class FontFinder {
constructor(pdfFonts) {
this.fonts = new Map();
this.cache = new Map();
this.warned = new Set();
this.defaultFont = null;
for (const pdfFont of pdfFonts) {
const cssFontInfo = pdfFont.cssFontInfo;
const name = cssFontInfo.fontFamily;
let font = this.fonts.get(name);
if (!font) {
font = Object.create(null);
this.fonts.set(name, font);
if (!this.defaultFont) {
this.defaultFont = font;
}
}
let property = "";
if (cssFontInfo.italicAngle !== "0") {
if (parseFloat(cssFontInfo.fontWeight) >= 700) {
property = "bolditalic";
} else {
property = "italic";
}
} else if (parseFloat(cssFontInfo.fontWeight) >= 700) {
property = "bold";
}
if (!property) {
if (
pdfFont.name.includes("Bold") ||
(pdfFont.psName && pdfFont.psName.includes("Bold"))
) {
property = "bold";
}
if (
pdfFont.name.includes("Italic") ||
pdfFont.name.endsWith("It") ||
(pdfFont.psName &&
(pdfFont.psName.includes("Italic") ||
pdfFont.psName.endsWith("It")))
) {
property += "italic";
}
}
if (!property) {
property = "regular";
}
font[property] = pdfFont;
}
for (const pdfFont of this.fonts.values()) {
if (!pdfFont.regular) {
pdfFont.regular = pdfFont.italic || pdfFont.bold || pdfFont.bolditalic;
}
}
}
getDefault() {
return this.defaultFont;
}
find(fontName, mustWarn = true) {
let font = this.fonts.get(fontName) || this.cache.get(fontName);
if (font) {
return font;
}
const pattern = /,|-| |bolditalic|bold|italic|regular|it/gi;
let name = fontName.replace(pattern, "");
font = this.fonts.get(name);
if (font) {
this.cache.set(fontName, font);
return font;
}
name = name.toLowerCase();
const maybe = [];
for (const [family, pdfFont] of this.fonts.entries()) {
if (family.replace(pattern, "").toLowerCase().startsWith(name)) {
maybe.push(pdfFont);
}
}
if (maybe.length === 0) {
for (const [, pdfFont] of this.fonts.entries()) {
if (
pdfFont.regular.name &&
pdfFont.regular.name
.replace(pattern, "")
.toLowerCase()
.startsWith(name)
) {
maybe.push(pdfFont);
}
}
}
if (maybe.length === 0) {
name = name.replace(/psmt|mt/gi, "");
for (const [family, pdfFont] of this.fonts.entries()) {
if (family.replace(pattern, "").toLowerCase().startsWith(name)) {
maybe.push(pdfFont);
}
}
}
if (maybe.length === 0) {
for (const pdfFont of this.fonts.values()) {
if (
pdfFont.regular.name &&
pdfFont.regular.name
.replace(pattern, "")
.toLowerCase()
.startsWith(name)
) {
maybe.push(pdfFont);
}
}
}
if (maybe.length >= 1) {
if (maybe.length !== 1 && mustWarn) {
warn(`XFA - Too many choices to guess the correct font: ${fontName}`);
}
this.cache.set(fontName, maybe[0]);
return maybe[0];
}
if (mustWarn && !this.warned.has(fontName)) {
this.warned.add(fontName);
warn(`XFA - Cannot find the font: ${fontName}`);
}
return null;
}
}
export { FontFinder };

View File

@ -190,8 +190,8 @@ function setMinMaxDimensions(node, style) {
}
}
function layoutText(text, xfaFont, fonts, width) {
const measure = new TextMeasure(xfaFont, fonts);
function layoutText(text, xfaFont, fontFinder, width) {
const measure = new TextMeasure(xfaFont, fontFinder);
if (typeof text === "string") {
measure.addString(text);
} else {
@ -448,13 +448,20 @@ function fixTextIndent(styles) {
}
}
function getFonts(family) {
function getFonts(family, fontFinder) {
if (family.startsWith("'") || family.startsWith('"')) {
family = family.slice(1, family.length - 1);
}
const fonts = [`"${family}"`, `"${family}-PdfJS-XFA"`];
return fonts.join(",");
const pdfFont = fontFinder.find(family);
if (pdfFont) {
const { fontFamily } = pdfFont.regular.cssFontInfo;
if (fontFamily !== family) {
return `"${family}","${fontFamily}"`;
}
}
return `"${family}"`;
}
export {

View File

@ -18,6 +18,7 @@ import {
$clean,
$content,
$finalize,
$globalData,
$isCDATAXml,
$nsAttributes,
$onChild,
@ -33,6 +34,7 @@ class XFAParser extends XMLParserBase {
super();
this._builder = new Builder();
this._stack = [];
this._globalData = Object.create(null);
this._ids = new Map();
this._current = this._builder.buildRoot(this._ids);
this._errorCode = XMLParserErrorCode.NoError;
@ -135,6 +137,7 @@ class XFAParser extends XMLParserBase {
namespace,
prefixes,
});
node[$globalData] = this._globalData;
if (isEmpty) {
// No children: just push the node into its parent.
@ -154,6 +157,7 @@ class XFAParser extends XMLParserBase {
const node = this._current;
if (node[$isCDATAXml]() && typeof node[$content] === "string") {
const parser = new XFAParser();
parser._globalData = this._globalData;
const root = parser.parse(node[$content]);
node[$content] = null;
node[$onChild](root);

View File

@ -23,7 +23,6 @@ import {
$extra,
$finalize,
$flushHTML,
$fonts,
$getAvailableSpace,
$getChildren,
$getContainedChildren,
@ -31,6 +30,7 @@ import {
$getParent,
$getSubformParent,
$getTemplateRoot,
$globalData,
$hasItem,
$hasSettableValue,
$ids,
@ -1441,7 +1441,7 @@ class Draw extends XFAObject {
if ((this.w === "" || this.h === "") && this.value) {
const maxWidth = this.w === "" ? availableSpace.width : this.w;
const fonts = this[$getTemplateRoot]()[$fonts];
const fontFinder = this[$globalData].fontFinder;
let font = this.font;
if (!font) {
let parent = this[$getParent]();
@ -1464,7 +1464,7 @@ class Draw extends XFAObject {
const res = layoutText(
this.value.exData[$content],
font,
fonts,
fontFinder,
maxWidth
);
width = res.width;
@ -1472,7 +1472,7 @@ class Draw extends XFAObject {
} else {
const text = this.value[$text]();
if (text) {
const res = layoutText(text, font, fonts, maxWidth);
const res = layoutText(text, font, fontFinder, maxWidth);
width = res.width;
height = res.height;
}
@ -2660,7 +2660,7 @@ class Font extends XFAObject {
style.fontSize = fontSize;
}
style.fontFamily = getFonts(this.typeface);
style.fontFamily = getFonts(this.typeface, this[$globalData].fontFinder);
if (this.underline !== 0) {
style.textDecoration = "underline";

View File

@ -17,19 +17,16 @@ const WIDTH_FACTOR = 1.2;
const HEIGHT_FACTOR = 1.2;
class FontInfo {
constructor(xfaFont, fonts) {
constructor(xfaFont, fontFinder) {
if (!xfaFont) {
[this.pdfFont, this.xfaFont] = this.defaultFont(fonts);
[this.pdfFont, this.xfaFont] = this.defaultFont(fontFinder);
return;
}
this.xfaFont = xfaFont;
let typeface = fonts[xfaFont.typeface];
const typeface = fontFinder.find(xfaFont.typeface);
if (!typeface) {
typeface = fonts[`${xfaFont.typeface}-PdfJS-XFA`];
}
if (!typeface) {
[this.pdfFont, this.xfaFont] = this.defaultFont(fonts);
[this.pdfFont, this.xfaFont] = this.defaultFont(fontFinder);
return;
}
@ -47,18 +44,17 @@ class FontInfo {
}
if (!this.pdfFont) {
[this.pdfFont, this.xfaFont] = this.defaultFont(fonts);
[this.pdfFont, this.xfaFont] = this.defaultFont(fontFinder);
}
}
defaultFont(fonts) {
defaultFont(fontFinder) {
// TODO: Add a default font based on Liberation.
const font =
fonts.Helvetica ||
fonts["Myriad Pro"] ||
fonts.Arial ||
fonts.ArialMT ||
Object.values(fonts)[0];
fontFinder.find("Helvetica", false) ||
fontFinder.find("Myriad Pro", false) ||
fontFinder.find("Arial", false) ||
fontFinder.getDefault();
if (font && font.regular) {
const pdfFont = font.regular;
const info = pdfFont.cssFontInfo;
@ -82,9 +78,9 @@ class FontInfo {
}
class FontSelector {
constructor(defaultXfaFont, fonts) {
this.fonts = fonts;
this.stack = [new FontInfo(defaultXfaFont, fonts)];
constructor(defaultXfaFont, fontFinder) {
this.fontFinder = fontFinder;
this.stack = [new FontInfo(defaultXfaFont, fontFinder)];
}
pushFont(xfaFont) {
@ -95,7 +91,7 @@ class FontSelector {
}
}
const fontInfo = new FontInfo(xfaFont, this.fonts);
const fontInfo = new FontInfo(xfaFont, this.fontFinder);
if (!fontInfo.pdfFont) {
fontInfo.pdfFont = lastFont.pdfFont;
}

View File

@ -34,7 +34,6 @@ const $dump = Symbol();
const $extra = Symbol("extra");
const $finalize = Symbol();
const $flushHTML = Symbol();
const $fonts = Symbol();
const $getAttributeIt = Symbol();
const $getAvailableSpace = Symbol();
const $getChildrenByClass = Symbol();
@ -49,6 +48,7 @@ const $getSubformParent = Symbol();
const $getParent = Symbol();
const $getTemplateRoot = Symbol();
const $global = Symbol();
const $globalData = Symbol();
const $hasItem = Symbol();
const $hasSettableValue = Symbol();
const $ids = Symbol();
@ -107,6 +107,7 @@ class XFAObject {
this[_parent] = null;
this[_children] = [];
this[$uid] = `${name}${uid++}`;
this[$globalData] = null;
}
[$onChild](child) {
@ -986,7 +987,6 @@ export {
$extra,
$finalize,
$flushHTML,
$fonts,
$getAttributeIt,
$getAvailableSpace,
$getChildren,
@ -1001,6 +1001,7 @@ export {
$getSubformParent,
$getTemplateRoot,
$global,
$globalData,
$hasItem,
$hasSettableValue,
$ids,

View File

@ -19,6 +19,7 @@ import {
$content,
$extra,
$getChildren,
$globalData,
$nodeName,
$onText,
$pushGlyphs,
@ -91,13 +92,13 @@ const StyleMapping = new Map([
["margin-right", value => measureToString(getMeasurement(value))],
["margin-top", value => measureToString(getMeasurement(value))],
["text-indent", value => measureToString(getMeasurement(value))],
["font-family", value => getFonts(value)],
["font-family", (value, fontFinder) => getFonts(value, fontFinder)],
]);
const spacesRegExp = /\s+/g;
const crlfRegExp = /[\r\n]+/g;
function mapStyle(styleStr) {
function mapStyle(styleStr, fontFinder) {
const style = Object.create(null);
if (!styleStr) {
return style;
@ -112,7 +113,7 @@ function mapStyle(styleStr) {
if (typeof mapping === "string") {
newValue = mapping;
} else {
newValue = mapping(value);
newValue = mapping(value, fontFinder);
}
}
if (key.endsWith("scale")) {
@ -218,7 +219,7 @@ class XhtmlObject extends XmlObject {
name: this[$nodeName],
attributes: {
href: this.href,
style: mapStyle(this.style),
style: mapStyle(this.style, this[$globalData].fontFinder),
},
children,
value: this[$content] || "",

View File

@ -0,0 +1 @@
https://bugzilla.mozilla.org/attachment.cgi?id=9227656

View File

@ -938,6 +938,14 @@
"enableXfa": true,
"type": "eq"
},
{ "id": "xfa_bug1716980",
"file": "pdfs/xfa_bug1716980.pdf",
"md5": "3d7598b9548d78f209d013c485162e9a",
"link": true,
"rounds": 1,
"enableXfa": true,
"type": "eq"
},
{ "id": "xfa_bug1716809",
"file": "pdfs/xfa_bug1716809.pdf",
"md5": "7192f9e27e8b84776d107f57cbe353d5",

View File

@ -73,6 +73,7 @@ describe("XFAFactory", function () {
</xdp:xdp>
`;
const factory = new XFAFactory({ "xdp:xdp": xml });
factory.setFonts([]);
expect(factory.numberPages).toEqual(2);
@ -116,7 +117,7 @@ describe("XFAFactory", function () {
]);
expect(draw.attributes.style).toEqual({
color: "#0c1722",
fontFamily: '"FooBar","FooBar-PdfJS-XFA"',
fontFamily: '"FooBar"',
fontSize: "6.93px",
margin: "1px 4px 2px 3px",
verticalAlign: "2px",