From f9ecebe63c0afb820b49f1dc6d23357a43ee8e66 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 14 Apr 2026 15:32:25 +0200 Subject: [PATCH] Add a helper class for building TrueType font tables This helps reduce the amount of boilerplate code needed in multiple spots throughout the font code, and more importantly it'll help when building TrueType tables whose final size is non-trivial to compute upfront. --- src/core/fonts.js | 360 +++++++++++++++++++++++++++------------------- 1 file changed, 209 insertions(+), 151 deletions(-) diff --git a/src/core/fonts.js b/src/core/fonts.js index 27bcd9fe4..44dfb2f48 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -313,43 +313,110 @@ function string16(value) { return String.fromCharCode((value >> 8) & 0xff, value & 0xff); } -function setArray(data, pos, arr) { - data.set(arr, pos); - return pos + arr.length; -} +class TrueTypeTableBuilder { + #buf; -function setInt16(view, pos, val) { - if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { - assert( - typeof val === "number" && Math.abs(val) < 2 ** 16, - `setInt16: Unexpected input "${val}".` - ); - } - view.setInt16(pos, val); - return pos + 2; -} + #bufLength = 1024; -function setSafeInt16(view, pos, val) { - if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { - assert( - typeof val === "number" && !Number.isNaN(val), - `safeString16: Unexpected input "${val}".` - ); - } - // clamp value to the 16-bit int range - view.setInt16(pos, MathClamp(val, -0x8000, 0x7fff)); - return pos + 2; -} + #hasExactLength = false; -function setInt32(view, pos, val) { - if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { - assert( - typeof val === "number" && Math.abs(val) < 2 ** 32, - `setInt32: Unexpected input "${val}".` - ); + #pos = 0; + + #view; + + constructor({ exactLength, minLength }) { + this.#hasExactLength = !!exactLength; + this.#initBuf(exactLength || minLength); + } + + #initBuf(minLength) { + if (this.#hasExactLength) { + this.#bufLength = minLength; + } else { + // Compute the first power of two that is as big as the `minLength`. + while (this.#bufLength < minLength) { + this.#bufLength *= 2; + } + } + const newBuf = new Uint8Array(this.#bufLength); + + if (this.#buf) { + newBuf.set(this.#buf, 0); + } + this.#buf = newBuf; + this.#view = new DataView(newBuf.buffer); + } + + get data() { + return this.#buf.subarray(0, this.#pos); + } + + get length() { + return this.#pos; + } + + skip(n) { + this.#pos += n; + } + + setArray(arr) { + const newPos = this.#pos + arr.length; + + if (!this.#hasExactLength && newPos > this.#bufLength) { + this.#initBuf(newPos); + } + this.#buf.set(arr, this.#pos); + this.#pos = newPos; + } + + setInt16(val) { + if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { + assert( + typeof val === "number" && Math.abs(val) < 2 ** 16, + `setInt16: Unexpected input "${val}".` + ); + } + const newPos = this.#pos + 2; + + if (!this.#hasExactLength && newPos > this.#bufLength) { + this.#initBuf(newPos); + } + this.#view.setInt16(this.#pos, val); + this.#pos = newPos; + } + + setSafeInt16(val) { + if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { + assert( + typeof val === "number" && !Number.isNaN(val), + `safeString16: Unexpected input "${val}".` + ); + } + const newPos = this.#pos + 2; + + if (!this.#hasExactLength && newPos > this.#bufLength) { + this.#initBuf(newPos); + } + // clamp value to the 16-bit int range + this.#view.setInt16(this.#pos, MathClamp(val, -0x8000, 0x7fff)); + this.#pos = newPos; + } + + setInt32(val) { + if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { + assert( + typeof val === "number" && Math.abs(val) < 2 ** 32, + `setInt32: Unexpected input "${val}".` + ); + } + const newPos = this.#pos + 4; + + if (!this.#hasExactLength && newPos > this.#bufLength) { + this.#initBuf(newPos); + } + this.#view.setInt32(this.#pos, val); + this.#pos = newPos; } - view.setInt32(pos, val); - return pos + 4; } function isTrueTypeFile(file) { @@ -856,27 +923,24 @@ function createOS2Table(properties, charstrings, override) { const winAscent = override.yMax || typoAscent; const winDescent = -override.yMin || -typoDescent; - const data = new Uint8Array(96), - view = new DataView(data.buffer); - let pos = 0; - - pos = setArray(data, pos, [0x00, 0x03]); // version - pos = setArray(data, pos, [0x02, 0x24]); // xAvgCharWidth - pos = setArray(data, pos, [0x01, 0xf4]); // usWeightClass - pos = setArray(data, pos, [0x00, 0x05]); // usWidthClass - pos += 2; // fstype (0 to improve browser compatibility), skip redundant "\x00\x00" - pos = setArray(data, pos, [0x02, 0x8a]); // ySubscriptXSize - pos = setArray(data, pos, [0x02, 0xbb]); // ySubscriptYSize - pos += 2; // ySubscriptXOffset, skip redundant "\x00\x00" - pos = setArray(data, pos, [0x00, 0x8c]); // ySubscriptYOffset - pos = setArray(data, pos, [0x02, 0x8a]); // ySuperScriptXSize - pos = setArray(data, pos, [0x02, 0xbb]); // ySuperScriptYSize - pos += 2; // ySuperScriptXOffset, skip redundant "\x00\x00" - pos = setArray(data, pos, [0x01, 0xdf]); // ySuperScriptYOffset - pos = setArray(data, pos, [0x00, 0x31]); // yStrikeOutSize - pos = setArray(data, pos, [0x01, 0x02]); // yStrikeOutPosition - pos += 2; // sFamilyClass, skip redundant "\x00\x00" - pos = setArray(data, pos, [ + const os2 = new TrueTypeTableBuilder({ exactLength: 96 }); + os2.setArray([0x00, 0x03]); // version + os2.setArray([0x02, 0x24]); // xAvgCharWidth + os2.setArray([0x01, 0xf4]); // usWeightClass + os2.setArray([0x00, 0x05]); // usWidthClass + os2.skip(2); // fstype (0 to improve browser compatibility), skip redundant "\x00\x00" + os2.setArray([0x02, 0x8a]); // ySubscriptXSize + os2.setArray([0x02, 0xbb]); // ySubscriptYSize + os2.skip(2); // ySubscriptXOffset, skip redundant "\x00\x00" + os2.setArray([0x00, 0x8c]); // ySubscriptYOffset + os2.setArray([0x02, 0x8a]); // ySuperScriptXSize + os2.setArray([0x02, 0xbb]); // ySuperScriptYSize + os2.skip(2); // ySuperScriptXOffset, skip redundant "\x00\x00" + os2.setArray([0x01, 0xdf]); // ySuperScriptYOffset + os2.setArray([0x00, 0x31]); // yStrikeOutSize + os2.setArray([0x01, 0x02]); // yStrikeOutPosition + os2.skip(2); // sFamilyClass, skip redundant "\x00\x00" + os2.setArray([ 0x00, 0x00, 0x06, @@ -888,46 +952,47 @@ function createOS2Table(properties, charstrings, override) { 0x00, 0x00, ]); // Panose - pos = setInt32(view, pos, ulUnicodeRange1); // ulUnicodeRange1 (Bits 0-31) - pos = setInt32(view, pos, ulUnicodeRange2); // ulUnicodeRange2 (Bits 32-63) - pos = setInt32(view, pos, ulUnicodeRange3); // ulUnicodeRange3 (Bits 64-95) - pos = setInt32(view, pos, ulUnicodeRange4); // ulUnicodeRange4 (Bits 96-127) - pos = setArray(data, pos, [0x2a, 0x32, 0x31, 0x2a]); // achVendID - pos = setInt16(view, pos, properties.italicAngle ? 1 : 0); // fsSelection - pos = setInt16(view, pos, firstCharIndex || properties.firstChar); // usFirstCharIndex - pos = setInt16(view, pos, lastCharIndex || properties.lastChar); // usLastCharIndex - pos = setInt16(view, pos, typoAscent); // sTypoAscender - pos = setInt16(view, pos, typoDescent); // sTypoDescender - pos = setArray(data, pos, [0x00, 0x64]); // sTypoLineGap (7%-10% of the unitsPerEM value) - pos = setInt16(view, pos, winAscent); // usWinAscent - pos = setInt16(view, pos, winDescent); // usWinDescent - pos += 4; // ulCodePageRange1 (Bits 0-31), skip redundant "\x00\x00\x00\x00" - pos += 4; // ulCodePageRange2 (Bits 32-63), skip redundant "\x00\x00\x00\x00" - pos = setInt16(view, pos, properties.xHeight); // sxHeight - pos = setInt16(view, pos, properties.capHeight); // sCapHeight - pos += 2; // usDefaultChar, skip redundant "\x00\x00" - pos = setInt16(view, pos, firstCharIndex || properties.firstChar); // usBreakChar - setArray(data, pos, [0x00, 0x03]); // usMaxContext - - return data; + os2.setInt32(ulUnicodeRange1); // ulUnicodeRange1 (Bits 0-31) + os2.setInt32(ulUnicodeRange2); // ulUnicodeRange2 (Bits 32-63) + os2.setInt32(ulUnicodeRange3); // ulUnicodeRange3 (Bits 64-95) + os2.setInt32(ulUnicodeRange4); // ulUnicodeRange4 (Bits 96-127) + os2.setArray([0x2a, 0x32, 0x31, 0x2a]); // achVendID + os2.setInt16(properties.italicAngle ? 1 : 0); // fsSelection + os2.setInt16(firstCharIndex || properties.firstChar); // usFirstCharIndex + os2.setInt16(lastCharIndex || properties.lastChar); // usLastCharIndex + os2.setInt16(typoAscent); // sTypoAscender + os2.setInt16(typoDescent); // sTypoDescender + os2.setArray([0x00, 0x64]); // sTypoLineGap (7%-10% of the unitsPerEM value) + os2.setInt16(winAscent); // usWinAscent + os2.setInt16(winDescent); // usWinDescent + os2.skip( + 4 + // ulCodePageRange1 (Bits 0-31), skip redundant "\x00\x00\x00\x00" + 4 // ulCodePageRange2 (Bits 32-63), skip redundant "\x00\x00\x00\x00" + ); + os2.setInt16(properties.xHeight); // sxHeight + os2.setInt16(properties.capHeight); // sCapHeight + os2.skip(2); // usDefaultChar, skip redundant "\x00\x00" + os2.setInt16(firstCharIndex || properties.firstChar); // usBreakChar + os2.setArray([0x00, 0x03]); // usMaxContext + return os2.data; } function createPostTable(properties) { - const data = new Uint8Array(32), - view = new DataView(data.buffer); - let pos = 0; - - pos = setArray(data, pos, [0x00, 0x03, 0x00, 0x00]); // Version number - pos = setInt32(view, pos, Math.floor(properties.italicAngle * 2 ** 16)); // italicAngle - pos += 2; // underlinePosition, skip redundant "\x00\x00" - pos += 2; // underlineThickness, skip redundant "\x00\x00" - setInt32(view, pos, properties.fixedPitch ? 1 : 0); // isFixedPitch - // minMemType42, skip redundant "\x00\x00\x00\x00" - // maxMemType42, skip redundant "\x00\x00\x00\x00" - // minMemType1, skip redundant "\x00\x00\x00\x00" - // maxMemType1, skip redundant "\x00\x00\x00\x00" - - return data; + const post = new TrueTypeTableBuilder({ exactLength: 32 }); + post.setArray([0x00, 0x03, 0x00, 0x00]); // Version number + post.setInt32(Math.floor(properties.italicAngle * 2 ** 16)); // italicAngle + post.skip( + 2 + // underlinePosition, skip redundant "\x00\x00" + 2 // underlineThickness, skip redundant "\x00\x00" + ); + post.setInt32(properties.fixedPitch ? 1 : 0); // isFixedPitch + post.skip( + 4 + // minMemType42, skip redundant "\x00\x00\x00\x00" + 4 + // maxMemType42, skip redundant "\x00\x00\x00\x00" + 4 + // minMemType1, skip redundant "\x00\x00\x00\x00" + 4 // maxMemType1, skip redundant "\x00\x00\x00\x00" + ); + return post.data; } function createPostscriptName(name) { @@ -3299,29 +3364,28 @@ class Font { "head", (function fontTableHead() { const dateArr = [0x00, 0x00, 0x00, 0x00, 0x9e, 0x0b, 0x7e, 0x27]; - const data = new Uint8Array(54), - view = new DataView(data.buffer); - let pos = 0; - pos = setArray(data, pos, [0x00, 0x01, 0x00, 0x00]); // Version number - pos = setArray(data, pos, [0x00, 0x00, 0x10, 0x00]); // fontRevision - pos += 4; // checksumAdjustement, skip redundant "\x00\x00\x00\x00" - pos = setArray(data, pos, [0x5f, 0x0f, 0x3c, 0xf5]); // magicNumber - pos += 2; // Flags, skip redundant "\x00\x00" - pos = setSafeInt16(view, pos, unitsPerEm); // unitsPerEM - pos = setArray(data, pos, dateArr); // creation date - pos = setArray(data, pos, dateArr); // modifification date - pos += 2; // xMin, skip redundant "\x00\x00" - pos = setSafeInt16(view, pos, properties.descent); // yMin - pos = setArray(data, pos, [0x0f, 0xff]); // xMax - pos = setSafeInt16(view, pos, properties.ascent); // yMax - pos = setInt16(view, pos, properties.italicAngle ? 2 : 0); // macStyle - setArray(data, pos, [0x00, 0x11]); // lowestRecPPEM - // fontDirectionHint, skip redundant "\x00\x00" - // indexToLocFormat, skip redundant "\x00\x00" - // glyphDataFormat, skip redundant "\x00\x00" - - return data; + const head = new TrueTypeTableBuilder({ exactLength: 54 }); + head.setArray([0x00, 0x01, 0x00, 0x00]); // Version number + head.setArray([0x00, 0x00, 0x10, 0x00]); // fontRevision + head.skip(4); // checksumAdjustement, skip redundant "\x00\x00\x00\x00" + head.setArray([0x5f, 0x0f, 0x3c, 0xf5]); // magicNumber + head.skip(2); // Flags, skip redundant "\x00\x00" + head.setSafeInt16(unitsPerEm); // unitsPerEM + head.setArray(dateArr); // creation date + head.setArray(dateArr); // modifification date + head.skip(2); // xMin, skip redundant "\x00\x00" + head.setSafeInt16(properties.descent); // yMin + head.setArray([0x0f, 0xff]); // xMax + head.setSafeInt16(properties.ascent); // yMax + head.setInt16(properties.italicAngle ? 2 : 0); // macStyle + head.setArray([0x00, 0x11]); // lowestRecPPEM + head.skip( + 2 + // fontDirectionHint, skip redundant "\x00\x00" + 2 + // indexToLocFormat, skip redundant "\x00\x00" + 2 // glyphDataFormat, skip redundant "\x00\x00" + ); + return head.data; })() ); @@ -3329,33 +3393,31 @@ class Font { builder.addTable( "hhea", (function fontTableHhea() { - const data = new Uint8Array(36), - view = new DataView(data.buffer); - let pos = 0; - - pos = setArray(data, pos, [0x00, 0x01, 0x00, 0x00]); // Version number - pos = setSafeInt16(view, pos, properties.ascent); // Typographic Ascent - pos = setSafeInt16(view, pos, properties.descent); // Typographic Descent - pos += 2; // Line Gap, skip redundant "\x00\x00" - pos = setArray(data, pos, [0xff, 0xff]); // advanceWidthMax - pos += 2; // minLeftSidebearing, skip redundant "\x00\x00" - pos += 2; // minRightSidebearing, skip redundant "\x00\x00" - pos += 2; // xMaxExtent, skip redundant "\x00\x00" - pos = setSafeInt16(view, pos, properties.capHeight); // caretSlopeRise - pos = setSafeInt16( - view, - pos, + const hhea = new TrueTypeTableBuilder({ exactLength: 36 }); + hhea.setArray([0x00, 0x01, 0x00, 0x00]); // Version number + hhea.setSafeInt16(properties.ascent); // Typographic Ascent + hhea.setSafeInt16(properties.descent); // Typographic Descent + hhea.skip(2); // Line Gap, skip redundant "\x00\x00" + hhea.setArray([0xff, 0xff]); // advanceWidthMax + hhea.skip( + 2 + // minLeftSidebearing, skip redundant "\x00\x00" + 2 + // minRightSidebearing, skip redundant "\x00\x00" + 2 // xMaxExtent, skip redundant "\x00\x00" + ); + hhea.setSafeInt16(properties.capHeight); // caretSlopeRise + hhea.setSafeInt16( Math.tan(properties.italicAngle) * properties.xHeight ); // caretSlopeRun - pos += 2; // caretOffset, skip redundant "\x00\x00" - pos += 2; // -reserved-, skip redundant "\x00\x00" - pos += 2; // -reserved-, skip redundant "\x00\x00" - pos += 2; // -reserved-, skip redundant "\x00\x00" - pos += 2; // -reserved-, skip redundant "\x00\x00" - pos += 2; // metricDataFormat, skip redundant "\x00\x00" - setInt16(view, pos, numGlyphs); // Number of HMetrics - - return data; + hhea.skip( + 2 + // caretOffset, skip redundant "\x00\x00" + 2 + // -reserved-, skip redundant "\x00\x00" + 2 + // -reserved-, skip redundant "\x00\x00" + 2 + // -reserved-, skip redundant "\x00\x00" + 2 + // -reserved-, skip redundant "\x00\x00" + 2 // metricDataFormat, skip redundant "\x00\x00" + ); + hhea.setInt16(numGlyphs); // Number of HMetrics + return hhea.data; })() ); @@ -3366,10 +3428,9 @@ class Font { const charstrings = font.charstrings; const cffWidths = font.cff?.widths ?? null; - const data = new Uint8Array(numGlyphs * 4), - view = new DataView(data.buffer); + const hmtx = new TrueTypeTableBuilder({ exactLength: numGlyphs * 4 }); // Fake .notdef (width=0 and lsb=0) first, skip redundant assignment. - let pos = 4; + hmtx.skip(4); for (let i = 1, ii = numGlyphs; i < ii; i++) { let width = 0; @@ -3378,10 +3439,10 @@ class Font { } else if (cffWidths) { width = Math.ceil(cffWidths[i] || 0); } - pos = setInt16(view, pos, width); - pos += 2; // Use lsb=0, skip redundant assignment. + hmtx.setInt16(width); + hmtx.skip(2); // Use lsb=0, skip redundant assignment. } - return data; + return hmtx.data; })() ); @@ -3389,13 +3450,10 @@ class Font { builder.addTable( "maxp", (function fontTableMaxp() { - const data = new Uint8Array(6), - view = new DataView(data.buffer); - - setArray(data, 0, [0x00, 0x00, 0x50, 0x00]); // Version number - setInt16(view, 4, numGlyphs); // Num of glyphs - - return data; + const maxp = new TrueTypeTableBuilder({ exactLength: 6 }); + maxp.setArray([0x00, 0x00, 0x50, 0x00]); // Version number + maxp.setInt16(numGlyphs); // Num of glyphs + return maxp.data; })() );