Merge pull request #21343 from calixteman/issue9437

Clamp out-of-range BlueScale to Adobe's valid window
This commit is contained in:
calixteman 2026-05-29 08:58:05 +02:00 committed by GitHub
commit c7a32c3db6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 126 additions and 0 deletions

View File

@ -30,6 +30,7 @@ import {
} from "./charsets.js";
import { ExpertEncoding, StandardEncoding } from "./encodings.js";
import { DataBuilder } from "./data_builder.js";
import { MathClamp } from "../shared/math_clamp.js";
// Maximum subroutine call depth of type 2 charstrings. Matches OTS.
const MAX_SUBR_NESTING = 10;
@ -865,6 +866,43 @@ class CFFParser {
// 15289), hence we just reset it to its default value.
privateDict.setByName("ExpansionFactor", DEFAULT_EXPANSION_FACTOR);
}
if (blueScale > 0) {
// Adobe's font validator (AFDKO, see `absfont.cpp`) flags BlueScale as
// out-of-range when `BlueScale * maxZoneHeight` is below 0.5 or above 1.
// The Type 2 hinting engine in coretype/FreeType disables the lower
// clamp at render time because library fonts with small zones and a
// default BlueScale (0.039625) trip the threshold even though they
// render correctly. To avoid changing those fonts here, only apply
// the lower clamp when BlueScale is also smaller than the default,
// i.e. when the font genuinely deviates from the standard value.
// The upper clamp matches what FreeType already enforces (psblues.c)
// and is safe to apply unconditionally.
let maxZoneHeight = 0;
for (const zones of [
privateDict.getByName("BlueValues"),
privateDict.getByName("OtherBlues"),
]) {
if (!zones) {
continue;
}
// BlueValues/OtherBlues are stored as deltas where the odd-indexed
// entries are the heights of each zone.
for (let i = 1; i < zones.length; i += 2) {
if (zones[i] > maxZoneHeight) {
maxZoneHeight = zones[i];
}
}
}
if (maxZoneHeight > 0) {
const minBlueScale =
blueScale < DEFAULT_BLUE_SCALE ? 0.5 / maxZoneHeight : -Infinity;
const maxBlueScale = 1 / maxZoneHeight;
const clamped = MathClamp(blueScale, minBlueScale, maxBlueScale);
if (clamped !== blueScale) {
privateDict.setByName("BlueScale", clamped);
}
}
}
// Parse the Subrs index also since it's relative to the private dict.
if (!privateDict.getByName("Subrs")) {

View File

@ -269,6 +269,94 @@ describe("CFFParser", function () {
expect(privateDict.getByName("ExpansionFactor")).toEqual(0.06);
});
it("clamps a too-small BlueScale up to 0.5 / maxZoneHeight", function () {
cff.topDict.privateDict = new CFFPrivateDict(cff.strings);
// Zones (deltas): heights are the odd-indexed entries (all 20 here).
cff.topDict.privateDict.setByName(
"BlueValues",
[-20, 20, 530, 20, 220, 20, 30, 20]
);
cff.topDict.privateDict.setByName("OtherBlues", [-270, 20]);
cff.topDict.privateDict.setByName("BlueScale", 0.016666999);
cff.topDict.setByName("Private", [0, 0]);
const fontDataWithSmallBlueScale = new CFFCompiler(cff).compile();
const reparsedCff = new CFFParser(
new Stream(fontDataWithSmallBlueScale),
{},
SEAC_ANALYSIS_ENABLED
).parse();
// maxZoneHeight = 20 -> minBlueScale = 0.5 / 20 = 0.025.
expect(reparsedCff.topDict.privateDict.getByName("BlueScale")).toEqual(
0.025
);
});
it("clamps a too-large BlueScale down to 1 / maxZoneHeight", function () {
cff.topDict.privateDict = new CFFPrivateDict(cff.strings);
cff.topDict.privateDict.setByName(
"BlueValues",
[-20, 20, 530, 20, 220, 20, 30, 20]
);
cff.topDict.privateDict.setByName("BlueScale", 0.1);
cff.topDict.setByName("Private", [0, 0]);
const fontDataWithLargeBlueScale = new CFFCompiler(cff).compile();
const reparsedCff = new CFFParser(
new Stream(fontDataWithLargeBlueScale),
{},
SEAC_ANALYSIS_ENABLED
).parse();
// maxZoneHeight = 20 -> maxBlueScale = 1 / 20 = 0.05.
expect(reparsedCff.topDict.privateDict.getByName("BlueScale")).toEqual(
0.05
);
});
it("preserves a BlueScale that is already inside the valid range", function () {
cff.topDict.privateDict = new CFFPrivateDict(cff.strings);
cff.topDict.privateDict.setByName(
"BlueValues",
[-20, 20, 530, 20, 220, 20, 30, 20]
);
cff.topDict.privateDict.setByName("BlueScale", 0.039625);
cff.topDict.setByName("Private", [0, 0]);
const fontDataWithNormalBlueScale = new CFFCompiler(cff).compile();
const reparsedCff = new CFFParser(
new Stream(fontDataWithNormalBlueScale),
{},
SEAC_ANALYSIS_ENABLED
).parse();
expect(reparsedCff.topDict.privateDict.getByName("BlueScale")).toEqual(
0.039625
);
});
it("preserves the default BlueScale even when zones are very small", function () {
// Foundry fonts (e.g. Eurostile LT Std Medium, maxZoneHeight 6) ship the
// default BlueScale of 0.039625 together with small zones; that combination
// technically violates AFDKO's lower bound but is the rendered intent.
cff.topDict.privateDict = new CFFPrivateDict(cff.strings);
cff.topDict.privateDict.setByName("BlueValues", [-12, 6, 530, 6]);
cff.topDict.privateDict.setByName("BlueScale", 0.039625);
cff.topDict.setByName("Private", [0, 0]);
const fontDataDefaultBlueScale = new CFFCompiler(cff).compile();
const reparsedCff = new CFFParser(
new Stream(fontDataDefaultBlueScale),
{},
SEAC_ANALYSIS_ENABLED
).parse();
expect(reparsedCff.topDict.privateDict.getByName("BlueScale")).toEqual(
0.039625
);
});
it("refuses to add topDict key with invalid value (bug 1068432)", function () {
const topDict = cff.topDict;
const defaultValue = topDict.getByName("UnderlinePosition");