Compare commits

...

6 Commits

Author SHA1 Message Date
calixteman
1cb14a02d3
Merge pull request #21314 from calixteman/issue21312
Recover CFF FontBBox with negative coordinates encoded as unsigned 16-bit
2026-05-25 08:57:43 +02:00
Calixte Denizet
9391296036 Recover CFF FontBBox with negative coordinates encoded as unsigned 16-bit
It fixes #21312.
2026-05-25 08:36:18 +02:00
calixteman
5f2691e77d
Merge pull request #21328 from timvandermeij/updates
Update dependencies to the most recent versions
2026-05-25 08:33:34 +02:00
Tim van der Meij
33d8f45fc5
Fix vulnerabilities in dependency versions
This patch is generated automatically using `npm audit fix`, and brings
the number of reported vulnerabilities back to zero by patching:

- GHSA-jxxr-4gwj-5jf2: "brace-expansion: Large numeric range defeats
  documented `max` DoS protection"
- GHSA-58qx-3vcg-4xpx: "ws: Uninitialized memory disclosure"
2026-05-24 19:34:07 +02:00
Tim van der Meij
f1f8e2c04e
Upgrade postcss-values-parser to version 8.0.0
This is a major version bump, but the changelog at
17304bbe9c
doesn't indicate any breaking changes that should impact us.
2026-05-24 19:29:57 +02:00
Tim van der Meij
0fa006997b
Update dependencies to the most recent versions 2026-05-24 19:29:09 +02:00
7 changed files with 250 additions and 1074 deletions

1168
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,15 +11,15 @@
"@metalsmith/layouts": "^3.0.0",
"@metalsmith/markdown": "^1.10.0",
"@napi-rs/canvas": "^1.0.0",
"@types/node": "^25.6.2",
"@types/node": "^25.9.1",
"autoprefixer": "^10.5.0",
"babel-loader": "^10.1.1",
"babel-plugin-add-header-comment": "^1.0.3",
"babel-plugin-istanbul": "^8.0.0",
"cached-iterable": "^0.3.0",
"caniuse-lite": "^1.0.30001792",
"caniuse-lite": "^1.0.30001793",
"core-js": "^3.49.0",
"eslint": "^10.3.0",
"eslint": "^10.4.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import-x": "^4.16.2",
"eslint-plugin-jasmine": "^4.2.2",
@ -46,12 +46,12 @@
"metalsmith-html-relative": "^2.0.12",
"ordered-read-streams": "^2.0.0",
"pngjs": "^7.0.0",
"postcss": "^8.5.14",
"postcss": "^8.5.15",
"postcss-discard-comments": "^8.0.0",
"postcss-values-parser": "^7.0.0",
"postcss-values-parser": "^8.0.0",
"prettier": "^3.8.3",
"puppeteer": "^24.43.1",
"stylelint": "^17.11.0",
"stylelint": "^17.12.0",
"stylelint-prettier": "^5.0.3",
"svglint": "^4.2.1",
"terser-webpack-plugin": "^5.6.0",
@ -59,7 +59,7 @@
"ttest": "^4.0.0",
"typescript": "^6.0.3",
"vinyl": "^3.0.1",
"webpack": "^5.106.2",
"webpack": "^5.107.1",
"webpack-stream": "^7.0.0"
},
"repository": {

View File

@ -17,6 +17,7 @@ import {
bytesToString,
FormatError,
info,
isArrayEqual,
shadow,
stringToBytes,
Util,
@ -33,6 +34,20 @@ import { DataBuilder } from "./data_builder.js";
// Maximum subroutine call depth of type 2 charstrings. Matches OTS.
const MAX_SUBR_NESTING = 10;
function looksLikeUnsigned16BitNegative(coord) {
return coord > 0x7fff && coord <= 0xffff;
}
function recoverSigned16BitBBox(bbox, onlyLowerLeft = false) {
return Util.normalizeRect(
bbox.map((coord, i) =>
(!onlyLowerLeft || i < 2) && looksLikeUnsigned16BitNegative(coord)
? coord - 0x10000
: coord
)
);
}
/**
* The CFF class takes a Type1 file and wrap it into a
* 'Compact Font Format' which itself embed Type2 charstrings.
@ -268,13 +283,36 @@ class CFFParser {
}
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
)
);
const descriptorBBox = properties.bbox?.some(coord => coord !== 0)
? recoverSigned16BitBBox(properties.bbox)
: null;
const cffBBoxHasUnsignedLowerLeft = fontBBox
?.slice(0, 2)
.some(looksLikeUnsigned16BitNegative);
const cffBBoxHasUnsignedCoords = fontBBox?.some(
looksLikeUnsigned16BitNegative
);
if (fontBBox?.every(coord => coord === 0) && descriptorBBox) {
// The CFF FontBBox is empty, hence fall back to the FontDescriptor bbox.
fontBBox = descriptorBBox;
topDict.setByName("FontBBox", fontBBox);
} else if (cffBBoxHasUnsignedCoords) {
const recoveredFontBBox = recoverSigned16BitBBox(fontBBox);
const descriptorCorroborates =
descriptorBBox &&
properties.bbox.some(coord => coord < 0) &&
!properties.bbox.some(looksLikeUnsigned16BitNegative) &&
isArrayEqual(recoveredFontBBox, descriptorBBox);
if (descriptorCorroborates || cffBBoxHasUnsignedLowerLeft) {
// Some Ghostscript-generated CFF fonts encode negative lower-left
// coordinates as unsigned 16-bit values. Preserve large upper-right
// coordinates unless the descriptor independently confirms the repair.
fontBBox = descriptorCorroborates
? recoveredFontBBox
: recoverSigned16BitBBox(fontBBox, /* onlyLowerLeft = */ true);
topDict.setByName("FontBBox", fontBBox);
}
}
if (fontBBox?.some(coord => coord !== 0)) {
// adjusting ascent/descent

View File

@ -923,3 +923,4 @@
!issue18032.pdf
!Embedded_font.pdf
!issue18548_reduced.pdf
!issue_cff_unsigned_bbox.pdf

Binary file not shown.

View File

@ -14320,5 +14320,12 @@
"firstPage": 1,
"lastPage": 1,
"type": "eq"
},
{
"id": "issue_cff_unsigned_bbox",
"file": "pdfs/issue_cff_unsigned_bbox.pdf",
"md5": "d2606e2c6cc9e679b8b88c2800c6e1a9",
"rounds": 1,
"type": "eq"
}
]

View File

@ -154,6 +154,90 @@ describe("CFFParser", function () {
expect(properties.ascentScaled).toEqual(true);
});
it("repairs a FontBBox with unsigned-encoded negative coordinates", function () {
// [-456, -305, 2158, 989] encoded as unsigned 16-bit values; produced
// by some Ghostscript-generated CFF fonts.
cff.topDict.setByName("FontBBox", [65080, 65231, 2158, 989]);
const fontDataRepaired = new CFFCompiler(cff).compile();
const properties = {
bbox: [-456, -305, 2158, 989],
};
const reparsedCff = new CFFParser(
new Stream(fontDataRepaired),
properties,
SEAC_ANALYSIS_ENABLED
).parse();
expect(reparsedCff.topDict.getByName("FontBBox")).toEqual([
-456, -305, 2158, 989,
]);
expect(properties.ascent).toEqual(989);
expect(properties.descent).toEqual(-305);
expect(properties.ascentScaled).toEqual(true);
});
it("doesn't replace a repairable FontBBox with an empty descriptor bbox", function () {
cff.topDict.setByName("FontBBox", [65080, 65231, 2158, 989]);
const fontDataRepaired = new CFFCompiler(cff).compile();
const properties = {
bbox: [0, 0, 0, 0],
};
const reparsedCff = new CFFParser(
new Stream(fontDataRepaired),
properties,
SEAC_ANALYSIS_ENABLED
).parse();
expect(reparsedCff.topDict.getByName("FontBBox")).toEqual([
-456, -305, 2158, 989,
]);
expect(properties.ascent).toEqual(989);
expect(properties.descent).toEqual(-305);
expect(properties.ascentScaled).toEqual(true);
});
it("repairs unsigned-encoded negative FontBBox without descriptor data", function () {
cff.topDict.setByName("FontBBox", [65080, 65231, 2158, 989]);
const fontDataRepaired = new CFFCompiler(cff).compile();
const properties = {};
const reparsedCff = new CFFParser(
new Stream(fontDataRepaired),
properties,
SEAC_ANALYSIS_ENABLED
).parse();
expect(reparsedCff.topDict.getByName("FontBBox")).toEqual([
-456, -305, 2158, 989,
]);
expect(properties.ascent).toEqual(989);
expect(properties.descent).toEqual(-305);
expect(properties.ascentScaled).toEqual(true);
});
it("preserves large positive upper FontBBox coordinates", function () {
cff.topDict.setByName("FontBBox", [0, -305, 40000, 989]);
const fontDataRepaired = new CFFCompiler(cff).compile();
const properties = {
bbox: [0, -305, 40000, 989],
};
const reparsedCff = new CFFParser(
new Stream(fontDataRepaired),
properties,
SEAC_ANALYSIS_ENABLED
).parse();
expect(reparsedCff.topDict.getByName("FontBBox")).toEqual([
0, -305, 40000, 989,
]);
expect(properties.ascent).toEqual(989);
expect(properties.descent).toEqual(-305);
expect(properties.ascentScaled).toEqual(true);
});
it("repairs likely Ghostscript-zeroed FDArray private defaults", function () {
cff.isCIDFont = true;
cff.topDict.setByName("ROS", [0, 0, 0]);