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.
This commit is contained in:
calixteman 2026-08-01 15:11:37 +02:00
parent 7fc7072f9c
commit 491792f0e2
No known key found for this signature in database
GPG Key ID: 0C5442631EE0691F
3 changed files with 51 additions and 3 deletions

View File

@ -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,

View File

@ -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

View File

@ -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 = {