Skip to content

Commit 1cb14a0

Browse files
authored
Merge pull request #21314 from calixteman/issue21312
Recover CFF FontBBox with negative coordinates encoded as unsigned 16-bit
2 parents 5f2691e + 9391296 commit 1cb14a0

5 files changed

Lines changed: 136 additions & 6 deletions

File tree

src/core/cff_parser.js

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
bytesToString,
1818
FormatError,
1919
info,
20+
isArrayEqual,
2021
shadow,
2122
stringToBytes,
2223
Util,
@@ -33,6 +34,20 @@ import { DataBuilder } from "./data_builder.js";
3334
// Maximum subroutine call depth of type 2 charstrings. Matches OTS.
3435
const MAX_SUBR_NESTING = 10;
3536

37+
function looksLikeUnsigned16BitNegative(coord) {
38+
return coord > 0x7fff && coord <= 0xffff;
39+
}
40+
41+
function recoverSigned16BitBBox(bbox, onlyLowerLeft = false) {
42+
return Util.normalizeRect(
43+
bbox.map((coord, i) =>
44+
(!onlyLowerLeft || i < 2) && looksLikeUnsigned16BitNegative(coord)
45+
? coord - 0x10000
46+
: coord
47+
)
48+
);
49+
}
50+
3651
/**
3752
* The CFF class takes a Type1 file and wrap it into a
3853
* 'Compact Font Format' which itself embed Type2 charstrings.
@@ -268,13 +283,36 @@ class CFFParser {
268283
}
269284

270285
let fontBBox = topDict.getByName("FontBBox");
271-
if (fontBBox?.every(coord => coord === 0) && properties.bbox) {
272-
fontBBox = Util.normalizeRect(
273-
properties.bbox.map(coord =>
274-
coord > 0x7fff && coord <= 0xffff ? coord - 0x10000 : coord
275-
)
276-
);
286+
const descriptorBBox = properties.bbox?.some(coord => coord !== 0)
287+
? recoverSigned16BitBBox(properties.bbox)
288+
: null;
289+
const cffBBoxHasUnsignedLowerLeft = fontBBox
290+
?.slice(0, 2)
291+
.some(looksLikeUnsigned16BitNegative);
292+
const cffBBoxHasUnsignedCoords = fontBBox?.some(
293+
looksLikeUnsigned16BitNegative
294+
);
295+
if (fontBBox?.every(coord => coord === 0) && descriptorBBox) {
296+
// The CFF FontBBox is empty, hence fall back to the FontDescriptor bbox.
297+
fontBBox = descriptorBBox;
277298
topDict.setByName("FontBBox", fontBBox);
299+
} else if (cffBBoxHasUnsignedCoords) {
300+
const recoveredFontBBox = recoverSigned16BitBBox(fontBBox);
301+
const descriptorCorroborates =
302+
descriptorBBox &&
303+
properties.bbox.some(coord => coord < 0) &&
304+
!properties.bbox.some(looksLikeUnsigned16BitNegative) &&
305+
isArrayEqual(recoveredFontBBox, descriptorBBox);
306+
307+
if (descriptorCorroborates || cffBBoxHasUnsignedLowerLeft) {
308+
// Some Ghostscript-generated CFF fonts encode negative lower-left
309+
// coordinates as unsigned 16-bit values. Preserve large upper-right
310+
// coordinates unless the descriptor independently confirms the repair.
311+
fontBBox = descriptorCorroborates
312+
? recoveredFontBBox
313+
: recoverSigned16BitBBox(fontBBox, /* onlyLowerLeft = */ true);
314+
topDict.setByName("FontBBox", fontBBox);
315+
}
278316
}
279317
if (fontBBox?.some(coord => coord !== 0)) {
280318
// adjusting ascent/descent

test/pdfs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,3 +923,4 @@
923923
!issue18032.pdf
924924
!Embedded_font.pdf
925925
!issue18548_reduced.pdf
926+
!issue_cff_unsigned_bbox.pdf
2.73 KB
Binary file not shown.

test/test_manifest.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14320,5 +14320,12 @@
1432014320
"firstPage": 1,
1432114321
"lastPage": 1,
1432214322
"type": "eq"
14323+
},
14324+
{
14325+
"id": "issue_cff_unsigned_bbox",
14326+
"file": "pdfs/issue_cff_unsigned_bbox.pdf",
14327+
"md5": "d2606e2c6cc9e679b8b88c2800c6e1a9",
14328+
"rounds": 1,
14329+
"type": "eq"
1432314330
}
1432414331
]

test/unit/cff_parser_spec.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,90 @@ describe("CFFParser", function () {
154154
expect(properties.ascentScaled).toEqual(true);
155155
});
156156

157+
it("repairs a FontBBox with unsigned-encoded negative coordinates", function () {
158+
// [-456, -305, 2158, 989] encoded as unsigned 16-bit values; produced
159+
// by some Ghostscript-generated CFF fonts.
160+
cff.topDict.setByName("FontBBox", [65080, 65231, 2158, 989]);
161+
const fontDataRepaired = new CFFCompiler(cff).compile();
162+
163+
const properties = {
164+
bbox: [-456, -305, 2158, 989],
165+
};
166+
const reparsedCff = new CFFParser(
167+
new Stream(fontDataRepaired),
168+
properties,
169+
SEAC_ANALYSIS_ENABLED
170+
).parse();
171+
172+
expect(reparsedCff.topDict.getByName("FontBBox")).toEqual([
173+
-456, -305, 2158, 989,
174+
]);
175+
expect(properties.ascent).toEqual(989);
176+
expect(properties.descent).toEqual(-305);
177+
expect(properties.ascentScaled).toEqual(true);
178+
});
179+
180+
it("doesn't replace a repairable FontBBox with an empty descriptor bbox", function () {
181+
cff.topDict.setByName("FontBBox", [65080, 65231, 2158, 989]);
182+
const fontDataRepaired = new CFFCompiler(cff).compile();
183+
184+
const properties = {
185+
bbox: [0, 0, 0, 0],
186+
};
187+
const reparsedCff = new CFFParser(
188+
new Stream(fontDataRepaired),
189+
properties,
190+
SEAC_ANALYSIS_ENABLED
191+
).parse();
192+
193+
expect(reparsedCff.topDict.getByName("FontBBox")).toEqual([
194+
-456, -305, 2158, 989,
195+
]);
196+
expect(properties.ascent).toEqual(989);
197+
expect(properties.descent).toEqual(-305);
198+
expect(properties.ascentScaled).toEqual(true);
199+
});
200+
201+
it("repairs unsigned-encoded negative FontBBox without descriptor data", function () {
202+
cff.topDict.setByName("FontBBox", [65080, 65231, 2158, 989]);
203+
const fontDataRepaired = new CFFCompiler(cff).compile();
204+
205+
const properties = {};
206+
const reparsedCff = new CFFParser(
207+
new Stream(fontDataRepaired),
208+
properties,
209+
SEAC_ANALYSIS_ENABLED
210+
).parse();
211+
212+
expect(reparsedCff.topDict.getByName("FontBBox")).toEqual([
213+
-456, -305, 2158, 989,
214+
]);
215+
expect(properties.ascent).toEqual(989);
216+
expect(properties.descent).toEqual(-305);
217+
expect(properties.ascentScaled).toEqual(true);
218+
});
219+
220+
it("preserves large positive upper FontBBox coordinates", function () {
221+
cff.topDict.setByName("FontBBox", [0, -305, 40000, 989]);
222+
const fontDataRepaired = new CFFCompiler(cff).compile();
223+
224+
const properties = {
225+
bbox: [0, -305, 40000, 989],
226+
};
227+
const reparsedCff = new CFFParser(
228+
new Stream(fontDataRepaired),
229+
properties,
230+
SEAC_ANALYSIS_ENABLED
231+
).parse();
232+
233+
expect(reparsedCff.topDict.getByName("FontBBox")).toEqual([
234+
0, -305, 40000, 989,
235+
]);
236+
expect(properties.ascent).toEqual(989);
237+
expect(properties.descent).toEqual(-305);
238+
expect(properties.ascentScaled).toEqual(true);
239+
});
240+
157241
it("repairs likely Ghostscript-zeroed FDArray private defaults", function () {
158242
cff.isCIDFont = true;
159243
cff.topDict.setByName("ROS", [0, 0, 0]);

0 commit comments

Comments
 (0)