Merge pull request #16363 from calixteman/use_local_font

[api-minor] Use a local font or fallback on an embedded one (if it exists) for non-embedded fonts (bug 1766039)
This commit is contained in:
calixteman 2023-05-10 14:19:05 +02:00 committed by GitHub
commit 2d2f7b315e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 584 additions and 28 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -423,6 +423,31 @@ function encodeToXmlString(str) {
return buffer.join("");
}
function validateFontName(fontFamily, mustWarn = false) {
// See https://developer.mozilla.org/en-US/docs/Web/CSS/string.
const m = /^("|').*("|')$/.exec(fontFamily);
if (m && m[1] === m[2]) {
const re = new RegExp(`[^\\\\]${m[1]}`);
if (re.test(fontFamily.slice(1, -1))) {
if (mustWarn) {
warn(`FontFamily contains unescaped ${m[1]}: ${fontFamily}.`);
}
return false;
}
} else {
// See https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident.
for (const ident of fontFamily.split(/[ \t]+/)) {
if (/^(\d|(-(\d|-)))/.test(ident) || !/^[\w-\\]+$/.test(ident)) {
if (mustWarn) {
warn(`FontFamily contains invalid <custom-ident>: ${fontFamily}.`);
}
return false;
}
}
}
return true;
}
function validateCSSFont(cssFontInfo) {
// See https://developer.mozilla.org/en-US/docs/Web/CSS/font-style.
const DEFAULT_CSS_FONT_OBLIQUE = "14";
@ -447,25 +472,9 @@ function validateCSSFont(cssFontInfo) {
const { fontFamily, fontWeight, italicAngle } = cssFontInfo;
// See https://developer.mozilla.org/en-US/docs/Web/CSS/string.
const m = /^("|').*("|')$/.exec(fontFamily);
if (m && m[1] === m[2]) {
const re = new RegExp(`[^\\\\]${m[1]}`);
if (re.test(fontFamily.slice(1, -1))) {
warn(`XFA - FontFamily contains unescaped ${m[1]}: ${fontFamily}.`);
if (!validateFontName(fontFamily, true)) {
return false;
}
} else {
// See https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident.
for (const ident of fontFamily.split(/[ \t]+/)) {
if (/^(\d|(-(\d|-)))/.test(ident) || !/^[\w-\\]+$/.test(ident)) {
warn(
`XFA - FontFamily contains invalid <custom-ident>: ${fontFamily}.`
);
return false;
}
}
}
const weight = fontWeight ? fontWeight.toString() : "";
cssFontInfo.fontWeight = CSS_FONT_WEIGHT_VALUES.has(weight)
@ -617,6 +626,7 @@ export {
stringToUTF16String,
toRomanNumerals,
validateCSSFont,
validateFontName,
XRefEntryException,
XRefParseException,
};

View File

@ -68,6 +68,7 @@ import { bidi } from "./bidi.js";
import { ColorSpace } from "./colorspace.js";
import { DecodeStream } from "./decode_stream.js";
import { FontFlags } from "./fonts_utils.js";
import { getFontSubstitution } from "./font_substitutions.js";
import { getGlyphsUnicode } from "./glyphlist.js";
import { getLookupTableFactory } from "./core_utils.js";
import { getMetrics } from "./metrics.js";
@ -4174,6 +4175,7 @@ class PartialEvaluator {
type,
name: baseFontName,
loadedName: baseDict.loadedName,
systemFontInfo: null,
widths: metrics.widths,
defaultWidth: metrics.defaultWidth,
isSimulatedFlags: true,
@ -4193,6 +4195,14 @@ class PartialEvaluator {
if (standardFontName) {
file = await this.fetchStandardFontData(standardFontName);
properties.isInternalFont = !!file;
if (!properties.isInternalFont && this.options.useSystemFonts) {
properties.systemFontInfo = getFontSubstitution(
this.idFactory,
this.options.standardFontDataUrl,
baseFontName,
standardFontName
);
}
}
return this.extractDataStructures(dict, dict, properties).then(
newProperties => {
@ -4264,6 +4274,7 @@ class PartialEvaluator {
}
let isInternalFont = false;
let glyphScaleFactors = null;
let systemFontInfo = null;
if (fontFile) {
if (fontFile.dict) {
const subtypeEntry = fontFile.dict.get("Subtype");
@ -4296,6 +4307,14 @@ class PartialEvaluator {
if (standardFontName) {
fontFile = await this.fetchStandardFontData(standardFontName);
isInternalFont = !!fontFile;
if (!isInternalFont && this.options.useSystemFonts) {
systemFontInfo = getFontSubstitution(
this.idFactory,
this.options.standardFontDataUrl,
fontName.name,
standardFontName
);
}
}
}
@ -4325,6 +4344,7 @@ class PartialEvaluator {
isType3Font,
cssFontInfo,
scaleFactors: glyphScaleFactors,
systemFontInfo,
};
if (composite) {

View File

@ -0,0 +1,478 @@
/* Copyright 2023 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 { normalizeFontName } from "./fonts_utils.js";
import { validateFontName } from "./core_utils.js";
const NORMAL = {
style: "normal",
weight: "normal",
};
const BOLD = {
style: "normal",
weight: "bold",
};
const ITALIC = {
style: "italic",
weight: "normal",
};
const BOLDITALIC = {
style: "italic",
weight: "bold",
};
const substitutionMap = new Map([
[
"Times-Roman",
{
local: [
"Times New Roman",
"Times-Roman",
"Times",
"Liberation Serif",
"Nimbus Roman",
"Nimbus Roman L",
"Tinos",
"Thorndale",
"TeX Gyre Termes",
"FreeSerif",
"DejaVu Serif",
"Bitstream Vera Serif",
"Ubuntu",
],
style: NORMAL,
ultimate: "serif",
},
],
[
"Times-Bold",
{
local: {
alias: "Times-Roman",
append: "Bold",
},
style: BOLD,
ultimate: "serif",
},
],
[
"Times-Italic",
{
local: {
alias: "Times-Roman",
append: "Italic",
},
style: ITALIC,
ultimate: "serif",
},
],
[
"Times-BoldItalic",
{
local: {
alias: "Times-Roman",
append: "Bold Italic",
},
style: BOLDITALIC,
ultimate: "serif",
},
],
[
"Helvetica",
{
local: [
"Helvetica",
"Helvetica Neue",
"Arial",
"Arial Nova",
"Liberation Sans",
"Arimo",
"Nimbus Sans",
"Nimbus Sans L",
"A030",
"TeX Gyre Heros",
"FreeSans",
"DejaVu Sans",
"Albany",
"Bitstream Vera Sans",
"Arial Unicode MS",
"Microsoft Sans Serif",
"Apple Symbols",
"Cantarell",
],
path: "LiberationSans-Regular.ttf",
style: NORMAL,
ultimate: "sans-serif",
},
],
[
"Helvetica-Bold",
{
local: {
alias: "Helvetica",
append: "Bold",
},
path: "LiberationSans-Bold.ttf",
style: BOLD,
ultimate: "sans-serif",
},
],
[
"Helvetica-Oblique",
{
local: {
alias: "Helvetica",
append: "Italic",
},
path: "LiberationSans-Italic.ttf",
style: ITALIC,
ultimate: "sans-serif",
},
],
[
"Helvetica-BoldOblique",
{
local: {
alias: "Helvetica",
append: "Bold Italic",
},
path: "LiberationSans-BoldItalic.ttf",
style: BOLDITALIC,
ultimate: "sans-serif",
},
],
[
"Courier",
{
local: [
"Courier",
"Courier New",
"Liberation Mono",
"Nimbus Mono",
"Nimbus Mono L",
"Cousine",
"Cumberland",
"TeX Gyre Cursor",
"FreeMono",
],
style: NORMAL,
ultimate: "monospace",
},
],
[
"Courier-Bold",
{
local: {
alias: "Courier",
append: "Bold",
},
style: BOLD,
ultimate: "monospace",
},
],
[
"Courier-Oblique",
{
local: {
alias: "Courier",
append: "Italic",
},
style: ITALIC,
ultimate: "monospace",
},
],
[
"Courier-BoldOblique",
{
local: {
alias: "Courier",
append: "Bold Italic",
},
style: BOLDITALIC,
ultimate: "monospace",
},
],
[
"ArialBlack",
{
prepend: ["Arial Black"],
style: {
style: "normal",
weight: "900",
},
fallback: "Helvetica-Bold",
},
],
[
"ArialBlack-Bold",
{
alias: "ArialBlack",
},
],
[
"ArialBlack-Italic",
{
prepend: ["Arial Black Italic"],
local: {
alias: "ArialBlack",
append: "Italic",
},
style: {
style: "italic",
weight: "900",
},
fallback: "Helvetica-BoldOblique",
},
],
[
"ArialBlack-BoldItalic",
{
alias: "ArialBlack-Italic",
},
],
[
"ArialNarrow",
{
prepend: [
"Arial Narrow",
"Liberation Sans Narrow",
"Helvetica Condensed",
"Nimbus Sans Narrow",
"TeX Gyre Heros Cn",
],
style: NORMAL,
fallback: "Helvetica",
},
],
[
"ArialNarrow-Bold",
{
local: {
alias: "ArialNarrow",
append: "Bold",
},
style: BOLD,
fallback: "Helvetica-Bold",
},
],
[
"ArialNarrow-Italic",
{
local: {
alias: "ArialNarrow",
append: "Italic",
},
style: ITALIC,
fallback: "Helvetica-Oblique",
},
],
[
"ArialNarrow-BoldItalic",
{
local: {
alias: "ArialNarrow",
append: "Bold Italic",
},
style: BOLDITALIC,
fallback: "Helvetica-BoldOblique",
},
],
[
"Calibri",
{
prepend: ["Calibri", "Carlito"],
style: NORMAL,
fallback: "Helvetica",
},
],
[
"Calibri-Bold",
{
local: {
alias: "Calibri",
append: "Bold",
},
style: BOLD,
fallback: "Helvetica-Bold",
},
],
[
"Calibri-Italic",
{
local: {
alias: "Calibri",
append: "Italic",
},
style: ITALIC,
fallback: "Helvetica-Oblique",
},
],
[
"Calibri-BoldItalic",
{
local: {
alias: "Calibri",
append: "Bold Italic",
},
style: BOLDITALIC,
fallback: "Helvetica-BoldOblique",
},
],
]);
const fontAliases = new Map([["Arial-Black", "ArialBlack"]]);
/**
* Create the src path to use to load a font (see FontFace).
* @param {Array<String>} prepend A list of font names to search first.
* @param {Array<String>|Object} local A list of font names to search. If an
* Object is passed, then local.alias is the name of an other substition font
* and local.append is a String to append to the list of fonts in the alias.
* For example if local.alias is "Foo" and local.append is "Bold" then the
* list of fonts will be "FooSubst1 Bold", "FooSubst2 Bold", etc.
* @returns an String with the local fonts.
*/
function makeLocal(prepend, local) {
let append = "";
if (!Array.isArray(local)) {
// We are getting our list of fonts in the alias and we'll append Bold,
// Italic or both.
append = ` ${local.append}`;
local = substitutionMap.get(local.alias).local;
}
let prependedPaths = "";
if (prepend) {
prependedPaths = prepend.map(name => `local(${name})`).join(",") + ",";
}
return (
prependedPaths + local.map(name => `local(${name}${append})`).join(",")
);
}
/**
* Get a font substitution for a given font.
* The general idea is to have enough information to create a CSS rule like
* this:
* @font-face {
* font-family: 'Times';
* src: local('Times New Roman'), local('Subst1'), local('Subst2'),
* url(.../TimesNewRoman.ttf)
* font-weight: normal;
* font-style: normal;
* }
* or use the FontFace API.
*
* @param {Object} idFactory The ids factory.
* @param {String} localFontPath Path to the fonts directory.
* @param {String} baseFontName The font name to be substituted.
* @param {String} standardFontName The standard font name to use if the base
* font is not available.
* @returns an Object with the CSS, the loaded name, the src and the style.
*/
function getFontSubstitution(
idFactory,
localFontPath,
baseFontName,
standardFontName
) {
let mustAddBaseFont = false;
// It's possible to have a font name with spaces, commas or dashes, hence we
// just replace them by a dash.
baseFontName = normalizeFontName(baseFontName);
// First, check if we've a substitution for the base font.
let substitution = substitutionMap.get(baseFontName);
if (!substitution) {
// Check if we've an alias for the base font, Arial-Black is the same as
// ArialBlack
for (const [alias, subst] of fontAliases) {
if (baseFontName.startsWith(alias)) {
baseFontName = `${subst}${baseFontName.substring(alias.length)}`;
substitution = substitutionMap.get(baseFontName);
break;
}
}
}
if (!substitution) {
// If not, check if we've a substitution for the standard font.
substitution = substitutionMap.get(standardFontName);
mustAddBaseFont = true;
}
const loadedName = `${idFactory.getDocId()}_sf_${idFactory.createFontId()}`;
if (!substitution) {
if (!validateFontName(baseFontName)) {
// If the baseFontName is not valid we don't want to use it.
return null;
}
// Maybe we'll be lucky and the OS will have the font.
const bold = /bold/gi.test(baseFontName);
const italic = /oblique|italic/gi.test(baseFontName);
const style =
(bold && italic && BOLDITALIC) ||
(bold && BOLD) ||
(italic && ITALIC) ||
NORMAL;
return {
css: `${loadedName},sans-serif`,
loadedName,
src: `local(${baseFontName})`,
style,
};
}
while (substitution.alias) {
// If we've an alias, use the substitution for the alias.
// For example, ArialBlack-Bold is an alias for ArialBlack because the bold
// version of Arial Black is not available.
substitution = substitutionMap.get(substitution.alias);
}
const { fallback, style } = substitution;
// Prepend the fonts to test before the fallback font.
let prepend = substitution.prepend;
if (fallback) {
// We've a fallback font: this one is a standard font we want to use in case
// nothing has been found from the prepend list.
prepend ||= substitutionMap.get(substitution.local.alias).prepend;
substitution = substitutionMap.get(fallback);
}
const { local, path, ultimate } = substitution;
let src = makeLocal(prepend, local);
if (path && localFontPath !== null) {
// PDF.js embeds some fonts we can use.
src += `,url(${localFontPath}${path})`;
}
// Maybe the OS will have the exact font we want so just prepend it to the
// list.
if (mustAddBaseFont && validateFontName(baseFontName)) {
src = `local(${baseFontName}),${src}`;
}
return {
css: `${loadedName},${ultimate}`,
loadedName,
src,
style,
};
}
export { getFontSubstitution };

View File

@ -98,6 +98,7 @@ const EXPORT_DATA_PROPERTIES = [
"name",
"remeasure",
"subtype",
"systemFontInfo",
"type",
"vertical",
];
@ -998,6 +999,7 @@ class Font {
this.fallbackName = "sans-serif";
}
this.systemFontInfo = properties.systemFontInfo;
this.differences = properties.differences;
this.widths = properties.widths;
this.defaultWidth = properties.defaultWidth;

View File

@ -100,10 +100,10 @@ const getFontNameToFileMap = getLookupTableFactory(function (t) {
t["Courier-Bold"] = "FoxitFixedBold.pfb";
t["Courier-BoldOblique"] = "FoxitFixedBoldItalic.pfb";
t["Courier-Oblique"] = "FoxitFixedItalic.pfb";
t.Helvetica = "FoxitSans.pfb";
t["Helvetica-Bold"] = "FoxitSansBold.pfb";
t["Helvetica-BoldOblique"] = "FoxitSansBoldItalic.pfb";
t["Helvetica-Oblique"] = "FoxitSansItalic.pfb";
t.Helvetica = "LiberationSans-Regular.ttf";
t["Helvetica-Bold"] = "LiberationSans-Bold.ttf";
t["Helvetica-BoldOblique"] = "LiberationSans-BoldItalic.ttf";
t["Helvetica-Oblique"] = "LiberationSans-Italic.ttf";
t["Times-Roman"] = "FoxitSerif.pfb";
t["Times-Bold"] = "FoxitSerifBold.pfb";
t["Times-BoldItalic"] = "FoxitSerifBoldItalic.pfb";

View File

@ -1950,6 +1950,8 @@ class CanvasGraphics {
}
const name = fontObj.loadedName || "sans-serif";
const typeface =
fontObj.systemFontInfo?.css || `"${name}", ${fontObj.fallbackName}`;
let bold = "normal";
if (fontObj.black) {
@ -1958,7 +1960,6 @@ class CanvasGraphics {
bold = "bold";
}
const italic = fontObj.italic ? "italic" : "normal";
const typeface = `"${name}", ${fontObj.fallbackName}`;
// Some font backends cannot handle fonts below certain size.
// Keeping the font at minimal size and using the fontSizeScale to change

View File

@ -19,6 +19,7 @@ import {
FeatureTest,
shadow,
string32,
unreachable,
warn,
} from "../shared/util.js";
import { isNodeJS } from "../shared/is_node.js";
@ -30,7 +31,7 @@ class FontLoader {
}) {
this._document = ownerDocument;
this.nativeFontFaces = [];
this.nativeFontFaces = new Set();
this.styleElement =
typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")
? styleElement
@ -43,10 +44,15 @@ class FontLoader {
}
addNativeFontFace(nativeFontFace) {
this.nativeFontFaces.push(nativeFontFace);
this.nativeFontFaces.add(nativeFontFace);
this._document.fonts.add(nativeFontFace);
}
removeNativeFontFace(nativeFontFace) {
this.nativeFontFaces.delete(nativeFontFace);
this._document.fonts.delete(nativeFontFace);
}
insertRule(rule) {
if (!this.styleElement) {
this.styleElement = this._document.createElement("style");
@ -62,7 +68,7 @@ class FontLoader {
for (const nativeFontFace of this.nativeFontFaces) {
this._document.fonts.delete(nativeFontFace);
}
this.nativeFontFaces.length = 0;
this.nativeFontFaces.clear();
if (this.styleElement) {
// Note: ChildNode.remove doesn't throw if the parentNode is undefined.
@ -71,13 +77,44 @@ class FontLoader {
}
}
async loadSystemFont(info) {
assert(
!this.disableFontFace,
"loadSystemFont shouldn't be called when `disableFontFace` is set."
);
if (this.isFontLoadingAPISupported) {
const { loadedName, src, style } = info;
const fontFace = new FontFace(loadedName, src, style);
this.addNativeFontFace(fontFace);
try {
await fontFace.load();
} catch {
warn(
`Cannot load system font: ${loadedName} for style ${style.style} and weight ${style.weight}.`
);
this.removeNativeFontFace(fontFace);
}
return;
}
unreachable(
"Not implemented: loadSystemFont without the Font Loading API."
);
}
async bind(font) {
// Add the font to the DOM only once; skip if the font is already loaded.
if (font.attached || font.missingFile) {
if (font.attached || (font.missingFile && !font.systemFontInfo)) {
return;
}
font.attached = true;
if (font.systemFontInfo) {
await this.loadSystemFont(font.systemFontInfo);
return;
}
if (this.isFontLoadingAPISupported) {
const nativeFontFace = font.createNativeFontFace();
if (nativeFontFace) {

View File

@ -210,7 +210,12 @@ const defaultOptions = {
cMapUrl: {
/** @type {string} */
value:
typeof PDFJSDev === "undefined" ? "../external/bcmaps/" : "../web/cmaps/",
// eslint-disable-next-line no-nested-ternary
typeof PDFJSDev === "undefined"
? "../external/bcmaps/"
: PDFJSDev.test("MOZCENTRAL")
? "resource://pdf.js/web/cmaps/"
: "../web/cmaps/",
kind: OptionKind.API,
},
disableAutoFetch: {
@ -271,8 +276,11 @@ const defaultOptions = {
standardFontDataUrl: {
/** @type {string} */
value:
// eslint-disable-next-line no-nested-ternary
typeof PDFJSDev === "undefined"
? "../external/standard_fonts/"
: PDFJSDev.test("MOZCENTRAL")
? "resource://pdf.js/web/standard_fonts/"
: "../web/standard_fonts/",
kind: OptionKind.API,
},