Recover CFF private dict defaults zeroed by Ghostscript

It fixes the issue #20633.
This commit is contained in:
calixteman 2026-05-17 15:53:45 +02:00
parent 3450e95179
commit cd8a78c4e2
No known key found for this signature in database
GPG Key ID: 0C5442631EE0691F
2 changed files with 112 additions and 8 deletions

View File

@ -108,6 +108,11 @@ const CFFStandardStrings = [
const NUM_STANDARD_CFF_STRINGS = 391;
const DEFAULT_BLUE_SCALE = 0.039625;
const DEFAULT_BLUE_SHIFT = 7;
const DEFAULT_BLUE_FUZZ = 1;
const DEFAULT_EXPANSION_FACTOR = 0.06;
const CharstringValidationData = [
/* 0 */ null,
/* 1 */ { id: "hstem", min: 2, stackClearing: true, stem: true },
@ -262,8 +267,16 @@ class CFFParser {
properties.fontMatrix = fontMatrix;
}
const fontBBox = topDict.getByName("FontBBox");
if (fontBBox) {
let fontBBox = topDict.getByName("FontBBox");
if (fontBBox?.every(coord => coord === 0) && properties.bbox) {
fontBBox = Util.normalizeRect(
properties.bbox.map(coord =>
coord > 0x7fff && coord <= 0xffff ? coord - 0x10000 : coord
)
);
topDict.setByName("FontBBox", fontBBox);
}
if (fontBBox?.some(coord => coord !== 0)) {
// adjusting ascent/descent
properties.ascent = Math.max(fontBBox[3], fontBBox[1]);
properties.descent = Math.min(fontBBox[1], fontBBox[3]);
@ -785,10 +798,28 @@ class CFFParser {
);
parentDict.privateDict = privateDict;
if (privateDict.getByName("ExpansionFactor") === 0) {
const blueScale = privateDict.getByName("BlueScale");
const blueShift = privateDict.getByName("BlueShift");
const blueFuzz = privateDict.getByName("BlueFuzz");
const expansionFactor = privateDict.getByName("ExpansionFactor");
if (
blueScale === 0 &&
blueShift === 0 &&
blueFuzz === 0 &&
expansionFactor === 0
) {
// Ghostscript can fail to initialize Private DICT defaults before
// writing them, which leaves omitted blue zone values as explicit
// zeroes. This has been seen in FDArray entries.
privateDict.setByName("BlueScale", DEFAULT_BLUE_SCALE);
privateDict.setByName("BlueShift", DEFAULT_BLUE_SHIFT);
privateDict.setByName("BlueFuzz", DEFAULT_BLUE_FUZZ);
}
if (expansionFactor === 0) {
// Firefox doesn't render correctly such a font on Windows (see issue
// 15289), hence we just reset it to its default value.
privateDict.setByName("ExpansionFactor", 0.06);
privateDict.setByName("ExpansionFactor", DEFAULT_EXPANSION_FACTOR);
}
// Parse the Subrs index also since it's relative to the private dict.
@ -1247,16 +1278,16 @@ const CFFPrivateDictLayout = [
[7, "OtherBlues", "delta", null],
[8, "FamilyBlues", "delta", null],
[9, "FamilyOtherBlues", "delta", null],
[[12, 9], "BlueScale", "num", 0.039625],
[[12, 10], "BlueShift", "num", 7],
[[12, 11], "BlueFuzz", "num", 1],
[[12, 9], "BlueScale", "num", DEFAULT_BLUE_SCALE],
[[12, 10], "BlueShift", "num", DEFAULT_BLUE_SHIFT],
[[12, 11], "BlueFuzz", "num", DEFAULT_BLUE_FUZZ],
[10, "StdHW", "num", null],
[11, "StdVW", "num", null],
[[12, 12], "StemSnapH", "delta", null],
[[12, 13], "StemSnapV", "delta", null],
[[12, 14], "ForceBold", "num", 0],
[[12, 17], "LanguageGroup", "num", 0],
[[12, 18], "ExpansionFactor", "num", 0.06],
[[12, 18], "ExpansionFactor", "num", DEFAULT_EXPANSION_FACTOR],
[[12, 19], "initialRandomSeed", "num", 0],
[20, "defaultWidthX", "num", 0],
[21, "nominalWidthX", "num", 0],

View File

@ -18,7 +18,9 @@ import {
CFFCompiler,
CFFFDSelect,
CFFParser,
CFFPrivateDict,
CFFStrings,
CFFTopDict,
} from "../../src/core/cff_parser.js";
import { SEAC_ANALYSIS_ENABLED } from "../../src/core/fonts_utils.js";
import { Stream } from "../../src/core/stream.js";
@ -112,6 +114,77 @@ describe("CFFParser", function () {
expect(topDict.getByName("Private")).toEqual([45, 102]);
});
it("ignores an empty FontBBox when adjusting ascent/descent", function () {
cff.topDict.setByName("FontBBox", [0, 0, 0, 0]);
const fontDataWithEmptyBBox = new CFFCompiler(cff).compile();
const properties = {
ascent: 800,
descent: -200,
};
new CFFParser(
new Stream(fontDataWithEmptyBBox),
properties,
SEAC_ANALYSIS_ENABLED
).parse();
expect(properties.ascent).toEqual(800);
expect(properties.descent).toEqual(-200);
expect(properties.ascentScaled).toBeUndefined();
});
it("repairs an empty FontBBox from font descriptor data", function () {
cff.topDict.setByName("FontBBox", [0, 0, 0, 0]);
const fontDataWithEmptyBBox = new CFFCompiler(cff).compile();
const properties = {
bbox: [2974, -300, 64236, 900],
};
const reparsedCff = new CFFParser(
new Stream(fontDataWithEmptyBBox),
properties,
SEAC_ANALYSIS_ENABLED
).parse();
expect(reparsedCff.topDict.getByName("FontBBox")).toEqual([
-1300, -300, 2974, 900,
]);
expect(properties.ascent).toEqual(900);
expect(properties.descent).toEqual(-300);
expect(properties.ascentScaled).toEqual(true);
});
it("repairs likely Ghostscript-zeroed FDArray private defaults", function () {
cff.isCIDFont = true;
cff.topDict.setByName("ROS", [0, 0, 0]);
cff.topDict.setByName("FDSelect", 0);
cff.topDict.setByName("FDArray", 0);
const fdDict = new CFFTopDict(cff.strings);
fdDict.setByName("Private", [0, 0]);
fdDict.privateDict = new CFFPrivateDict(cff.strings);
fdDict.privateDict.setByName("BlueScale", 0);
fdDict.privateDict.setByName("BlueShift", 0);
fdDict.privateDict.setByName("BlueFuzz", 0);
fdDict.privateDict.setByName("ExpansionFactor", 0);
cff.fdArray = [fdDict];
cff.fdSelect = new CFFFDSelect(0, Array(cff.charStrings.count).fill(0));
const fontDataWithBrokenFDPrivate = new CFFCompiler(cff).compile();
const reparsedCff = new CFFParser(
new Stream(fontDataWithBrokenFDPrivate),
{},
SEAC_ANALYSIS_ENABLED
).parse();
const privateDict = reparsedCff.fdArray[0].privateDict;
expect(privateDict.getByName("BlueScale")).toEqual(0.039625);
expect(privateDict.getByName("BlueShift")).toEqual(7);
expect(privateDict.getByName("BlueFuzz")).toEqual(1);
expect(privateDict.getByName("ExpansionFactor")).toEqual(0.06);
});
it("refuses to add topDict key with invalid value (bug 1068432)", function () {
const topDict = cff.topDict;
const defaultValue = topDict.getByName("UnderlinePosition");