From 491792f0e2d0601c499ffc20f798ecc98a8940f8 Mon Sep 17 00:00:00 2001 From: calixteman Date: Sat, 1 Aug 2026 15:11:37 +0200 Subject: [PATCH] Fix the regex used to normalize css fonts in XFA The regex was quadratic in the number of consecutive spaces, which caused performance issues when normalizing fonts with a large number of spaces. --- src/core/core_utils.js | 13 +++++++++++++ src/core/document.js | 5 ++--- test/unit/core_utils_spec.js | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/core/core_utils.js b/src/core/core_utils.js index c7d4b949d..368859ec5 100644 --- a/src/core/core_utils.js +++ b/src/core/core_utils.js @@ -574,6 +574,18 @@ function validateFontName(fontFamily, mustWarn = false) { return true; } +// Strip the spaces preceding a digit, since e.g. "Wingdings 3" is not a valid +// font name in the css specs. +// The optional trailing digit is matched as part of the space run, so that a +// failing match cannot backtrack over the spaces; otherwise the replacement +// would be quadratic in the number of consecutive spaces. +function normalizeCSSFontFamily(fontFamily) { + return fontFamily.replaceAll( + /( +)(\d)?/g, + (_, spaces, digit) => digit ?? " " + ); +} + function validateCSSFont(cssFontInfo) { // See https://developer.mozilla.org/en-US/docs/Web/CSS/font-style. const DEFAULT_CSS_FONT_OBLIQUE = "14"; @@ -750,6 +762,7 @@ export { lookupRect, MAX_INT_32, MissingDataException, + normalizeCSSFontFamily, numberToString, ParserEOFException, parseXFAPath, diff --git a/src/core/document.js b/src/core/document.js index e3214f879..de88df870 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -42,6 +42,7 @@ import { isWhiteSpace, lookupNormalRect, MissingDataException, + normalizeCSSFontFamily, PDF_VERSION_REGEXP, RESOURCES_KEYS_OPERATOR_LIST, RESOURCES_KEYS_TEXT_CONTENT, @@ -1396,9 +1397,7 @@ class PDFDocument { if (!(descriptor instanceof Dict)) { continue; } - let fontFamily = descriptor.get("FontFamily"); - // For example, "Wingdings 3" is not a valid font name in the css specs. - fontFamily = fontFamily.replaceAll(/ +(\d)/g, "$1"); + const fontFamily = normalizeCSSFontFamily(descriptor.get("FontFamily")); const fontWeight = descriptor.get("FontWeight"); // Angle is expressed in degrees counterclockwise in PDF diff --git a/test/unit/core_utils_spec.js b/test/unit/core_utils_spec.js index 24f2be1e9..ac83bf50c 100644 --- a/test/unit/core_utils_spec.js +++ b/test/unit/core_utils_spec.js @@ -24,6 +24,7 @@ import { getRotationMatrix, getSizeInBytes, isWhiteSpace, + normalizeCSSFontFamily, numberToString, parseXFAPath, recoverJsURL, @@ -325,6 +326,41 @@ describe("core_utils", function () { }); }); + describe("normalizeCSSFontFamily", function () { + it("should strip the spaces preceding a digit", function () { + expect(normalizeCSSFontFamily("Wingdings 3")).toEqual("Wingdings3"); + expect(normalizeCSSFontFamily("Wingdings 3")).toEqual("Wingdings3"); + expect(normalizeCSSFontFamily(" 1 2 3")).toEqual("123"); + expect(normalizeCSSFontFamily("MS Gothic 2 Bold 7")).toEqual( + "MS Gothic2 Bold7" + ); + }); + + it("should keep the spaces which don't precede a digit", function () { + expect(normalizeCSSFontFamily("")).toEqual(""); + expect(normalizeCSSFontFamily("Times New Roman")).toEqual( + "Times New Roman" + ); + // The runs of spaces must be preserved as-is. + expect(normalizeCSSFontFamily(" Times New Roman ")).toEqual( + " Times New Roman " + ); + // A digit which isn't preceded by a space is left alone. + expect(normalizeCSSFontFamily("Wingdings3")).toEqual("Wingdings3"); + }); + + it("should handle long runs of spaces efficiently", function () { + // Guard against a regular expression that backtracks over the spaces, + // which makes the replacement quadratic: that needs several seconds + // here, whereas a linear one needs well under a millisecond. + const fontFamily = `Wingdings${" ".repeat(100000)}`; + + const startTime = performance.now(); + expect(normalizeCSSFontFamily(fontFamily)).toEqual("Wingdings "); + expect(performance.now() - startTime).toBeLessThan(1000); + }); + }); + describe("validateCSSFont", function () { it("Check font family", function () { const cssFontInfo = {