From 9d84fe9561c655fcd79e52f54cf3207fe114a6b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Gediz=20Ayd=C4=B1ndo=C4=9Fmu=C5=9F?= Date: Wed, 10 Jun 2026 18:33:20 +0300 Subject: [PATCH] fix #353: guard empty glyph in TTFGlyph._getCBox Fixes #353. _getCBox always decodes a 10-byte GlyfHeader. A glyph with a zero-length loca entry (space, U+200D ZWJ, and similar) has no glyf data, so the decode reads the following glyph's header. When that empty glyph is the last glyph, the read runs past the end of the glyf table and throws "RangeError: Offset is outside the bounds of the DataView" (older builds: "Trying to access beyond buffer length"). This surfaces when embedding or laying out a subsetted font, where a trailing empty glyph is common, and is reported downstream through pdf-lib and pdfmake. Add the same glyfPos === nextPos guard that _decode already uses and return an empty bounding box. The default new BBox() would set the Infinity sentinel, which _getMetrics turns into a non-finite topBearing, so the guard returns an explicit zero-area box. Add a regression test in test/issues.js that reads the cbox of the OpenSans space glyph and asserts it has no positive extent. --- src/glyph/TTFGlyph.js | 8 +++++++- test/issues.js | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/glyph/TTFGlyph.js b/src/glyph/TTFGlyph.js index 8dc1c75b..43329e66 100644 --- a/src/glyph/TTFGlyph.js +++ b/src/glyph/TTFGlyph.js @@ -74,8 +74,14 @@ export default class TTFGlyph extends Glyph { return this.path.cbox; } + let glyfPos = this._font.loca.offsets[this.id]; + let nextPos = this._font.loca.offsets[this.id + 1]; + + // Return an empty bounding box if there is no data for this glyph + if (glyfPos === nextPos) { return Object.freeze(new BBox(0, 0, 0, 0)); } + let stream = this._font._getTableStream('glyf'); - stream.pos += this._font.loca.offsets[this.id]; + stream.pos += glyfPos; let glyph = GlyfHeader.decode(stream); let cbox = new BBox(glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax); diff --git a/test/issues.js b/test/issues.js index ed3d5d5f..66e40e8e 100644 --- a/test/issues.js +++ b/test/issues.js @@ -1,4 +1,5 @@ import * as fontkit from 'fontkit'; +import assert from 'assert'; describe('issues', function () { describe('#282 - ReferenceError: Cannot access \'c3x\' before initialization', function () { @@ -10,4 +11,20 @@ describe('issues', function () { glyph.path; }); }); + + describe('#353 - reading the cbox of an empty glyph', function () { + it('returns an empty box instead of reading past the glyph data', function () { + let font = fontkit.openSync(new URL('data/OpenSans/OpenSans-Regular.ttf', import.meta.url)); + + let glyph = font.glyphForCodePoint(0x20); // space: no outline, empty glyf entry + + // Before the fix this read the following glyph's header (a positive-area + // box), or ran past the buffer for a trailing empty glyph. An empty + // glyph has no extent. Asserting "no positive extent" rather than exact + // zeros so the test holds whether the empty box is (0,0,0,0) or the + // BBox sentinel. + let cbox = glyph.cbox; + assert.ok(cbox.width <= 0 && cbox.height <= 0); + }); + }); });