diff --git a/external/standard_fonts/FoxitSans.pfb b/external/standard_fonts/FoxitSans.pfb deleted file mode 100644 index 37f244bd9..000000000 Binary files a/external/standard_fonts/FoxitSans.pfb and /dev/null differ diff --git a/external/standard_fonts/FoxitSansBold.pfb b/external/standard_fonts/FoxitSansBold.pfb deleted file mode 100644 index affcf316d..000000000 Binary files a/external/standard_fonts/FoxitSansBold.pfb and /dev/null differ diff --git a/external/standard_fonts/FoxitSansBoldItalic.pfb b/external/standard_fonts/FoxitSansBoldItalic.pfb deleted file mode 100644 index e1f60b754..000000000 Binary files a/external/standard_fonts/FoxitSansBoldItalic.pfb and /dev/null differ diff --git a/external/standard_fonts/FoxitSansItalic.pfb b/external/standard_fonts/FoxitSansItalic.pfb deleted file mode 100644 index c04b0a5ae..000000000 Binary files a/external/standard_fonts/FoxitSansItalic.pfb and /dev/null differ diff --git a/src/core/core_utils.js b/src/core/core_utils.js index de73473f4..54b032bfe 100644 --- a/src/core/core_utils.js +++ b/src/core/core_utils.js @@ -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 : ${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,24 +472,8 @@ 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}.`); - 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 : ${fontFamily}.` - ); - return false; - } - } + if (!validateFontName(fontFamily, true)) { + return false; } const weight = fontWeight ? fontWeight.toString() : ""; @@ -617,6 +626,7 @@ export { stringToUTF16String, toRomanNumerals, validateCSSFont, + validateFontName, XRefEntryException, XRefParseException, }; diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 313259320..c2c7f1e0d 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -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) { diff --git a/src/core/font_substitutions.js b/src/core/font_substitutions.js new file mode 100644 index 000000000..584bef6d3 --- /dev/null +++ b/src/core/font_substitutions.js @@ -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} prepend A list of font names to search first. + * @param {Array|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 }; diff --git a/src/core/fonts.js b/src/core/fonts.js index 12803fcd2..62502cfa1 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -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; diff --git a/src/core/standard_fonts.js b/src/core/standard_fonts.js index 977d4c0ab..9f025adf3 100644 --- a/src/core/standard_fonts.js +++ b/src/core/standard_fonts.js @@ -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"; diff --git a/src/display/canvas.js b/src/display/canvas.js index 50ce776b0..f3ba13825 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -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 diff --git a/src/display/font_loader.js b/src/display/font_loader.js index 307c68838..515c8fc33 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -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) { diff --git a/web/app_options.js b/web/app_options.js index 7fd75e9ee..f3350f462 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -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, },