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.
This commit is contained in:
Jonas Jenwald 2026-04-14 15:32:25 +02:00
parent 7f7ac949ff
commit f9ecebe63c

View File

@ -313,43 +313,110 @@ function string16(value) {
return String.fromCharCode((value >> 8) & 0xff, value & 0xff); return String.fromCharCode((value >> 8) & 0xff, value & 0xff);
} }
function setArray(data, pos, arr) { class TrueTypeTableBuilder {
data.set(arr, pos); #buf;
return pos + arr.length;
}
function setInt16(view, pos, val) { #bufLength = 1024;
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;
}
function setSafeInt16(view, pos, val) { #hasExactLength = false;
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;
}
function setInt32(view, pos, val) { #pos = 0;
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
assert( #view;
typeof val === "number" && Math.abs(val) < 2 ** 32,
`setInt32: Unexpected input "${val}".` 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) { function isTrueTypeFile(file) {
@ -856,27 +923,24 @@ function createOS2Table(properties, charstrings, override) {
const winAscent = override.yMax || typoAscent; const winAscent = override.yMax || typoAscent;
const winDescent = -override.yMin || -typoDescent; const winDescent = -override.yMin || -typoDescent;
const data = new Uint8Array(96), const os2 = new TrueTypeTableBuilder({ exactLength: 96 });
view = new DataView(data.buffer); os2.setArray([0x00, 0x03]); // version
let pos = 0; os2.setArray([0x02, 0x24]); // xAvgCharWidth
os2.setArray([0x01, 0xf4]); // usWeightClass
pos = setArray(data, pos, [0x00, 0x03]); // version os2.setArray([0x00, 0x05]); // usWidthClass
pos = setArray(data, pos, [0x02, 0x24]); // xAvgCharWidth os2.skip(2); // fstype (0 to improve browser compatibility), skip redundant "\x00\x00"
pos = setArray(data, pos, [0x01, 0xf4]); // usWeightClass os2.setArray([0x02, 0x8a]); // ySubscriptXSize
pos = setArray(data, pos, [0x00, 0x05]); // usWidthClass os2.setArray([0x02, 0xbb]); // ySubscriptYSize
pos += 2; // fstype (0 to improve browser compatibility), skip redundant "\x00\x00" os2.skip(2); // ySubscriptXOffset, skip redundant "\x00\x00"
pos = setArray(data, pos, [0x02, 0x8a]); // ySubscriptXSize os2.setArray([0x00, 0x8c]); // ySubscriptYOffset
pos = setArray(data, pos, [0x02, 0xbb]); // ySubscriptYSize os2.setArray([0x02, 0x8a]); // ySuperScriptXSize
pos += 2; // ySubscriptXOffset, skip redundant "\x00\x00" os2.setArray([0x02, 0xbb]); // ySuperScriptYSize
pos = setArray(data, pos, [0x00, 0x8c]); // ySubscriptYOffset os2.skip(2); // ySuperScriptXOffset, skip redundant "\x00\x00"
pos = setArray(data, pos, [0x02, 0x8a]); // ySuperScriptXSize os2.setArray([0x01, 0xdf]); // ySuperScriptYOffset
pos = setArray(data, pos, [0x02, 0xbb]); // ySuperScriptYSize os2.setArray([0x00, 0x31]); // yStrikeOutSize
pos += 2; // ySuperScriptXOffset, skip redundant "\x00\x00" os2.setArray([0x01, 0x02]); // yStrikeOutPosition
pos = setArray(data, pos, [0x01, 0xdf]); // ySuperScriptYOffset os2.skip(2); // sFamilyClass, skip redundant "\x00\x00"
pos = setArray(data, pos, [0x00, 0x31]); // yStrikeOutSize os2.setArray([
pos = setArray(data, pos, [0x01, 0x02]); // yStrikeOutPosition
pos += 2; // sFamilyClass, skip redundant "\x00\x00"
pos = setArray(data, pos, [
0x00, 0x00,
0x00, 0x00,
0x06, 0x06,
@ -888,46 +952,47 @@ function createOS2Table(properties, charstrings, override) {
0x00, 0x00,
0x00, 0x00,
]); // Panose ]); // Panose
pos = setInt32(view, pos, ulUnicodeRange1); // ulUnicodeRange1 (Bits 0-31) os2.setInt32(ulUnicodeRange1); // ulUnicodeRange1 (Bits 0-31)
pos = setInt32(view, pos, ulUnicodeRange2); // ulUnicodeRange2 (Bits 32-63) os2.setInt32(ulUnicodeRange2); // ulUnicodeRange2 (Bits 32-63)
pos = setInt32(view, pos, ulUnicodeRange3); // ulUnicodeRange3 (Bits 64-95) os2.setInt32(ulUnicodeRange3); // ulUnicodeRange3 (Bits 64-95)
pos = setInt32(view, pos, ulUnicodeRange4); // ulUnicodeRange4 (Bits 96-127) os2.setInt32(ulUnicodeRange4); // ulUnicodeRange4 (Bits 96-127)
pos = setArray(data, pos, [0x2a, 0x32, 0x31, 0x2a]); // achVendID os2.setArray([0x2a, 0x32, 0x31, 0x2a]); // achVendID
pos = setInt16(view, pos, properties.italicAngle ? 1 : 0); // fsSelection os2.setInt16(properties.italicAngle ? 1 : 0); // fsSelection
pos = setInt16(view, pos, firstCharIndex || properties.firstChar); // usFirstCharIndex os2.setInt16(firstCharIndex || properties.firstChar); // usFirstCharIndex
pos = setInt16(view, pos, lastCharIndex || properties.lastChar); // usLastCharIndex os2.setInt16(lastCharIndex || properties.lastChar); // usLastCharIndex
pos = setInt16(view, pos, typoAscent); // sTypoAscender os2.setInt16(typoAscent); // sTypoAscender
pos = setInt16(view, pos, typoDescent); // sTypoDescender os2.setInt16(typoDescent); // sTypoDescender
pos = setArray(data, pos, [0x00, 0x64]); // sTypoLineGap (7%-10% of the unitsPerEM value) os2.setArray([0x00, 0x64]); // sTypoLineGap (7%-10% of the unitsPerEM value)
pos = setInt16(view, pos, winAscent); // usWinAscent os2.setInt16(winAscent); // usWinAscent
pos = setInt16(view, pos, winDescent); // usWinDescent os2.setInt16(winDescent); // usWinDescent
pos += 4; // ulCodePageRange1 (Bits 0-31), skip redundant "\x00\x00\x00\x00" os2.skip(
pos += 4; // ulCodePageRange2 (Bits 32-63), skip redundant "\x00\x00\x00\x00" 4 + // ulCodePageRange1 (Bits 0-31), skip redundant "\x00\x00\x00\x00"
pos = setInt16(view, pos, properties.xHeight); // sxHeight 4 // ulCodePageRange2 (Bits 32-63), skip redundant "\x00\x00\x00\x00"
pos = setInt16(view, pos, properties.capHeight); // sCapHeight );
pos += 2; // usDefaultChar, skip redundant "\x00\x00" os2.setInt16(properties.xHeight); // sxHeight
pos = setInt16(view, pos, firstCharIndex || properties.firstChar); // usBreakChar os2.setInt16(properties.capHeight); // sCapHeight
setArray(data, pos, [0x00, 0x03]); // usMaxContext os2.skip(2); // usDefaultChar, skip redundant "\x00\x00"
os2.setInt16(firstCharIndex || properties.firstChar); // usBreakChar
return data; os2.setArray([0x00, 0x03]); // usMaxContext
return os2.data;
} }
function createPostTable(properties) { function createPostTable(properties) {
const data = new Uint8Array(32), const post = new TrueTypeTableBuilder({ exactLength: 32 });
view = new DataView(data.buffer); post.setArray([0x00, 0x03, 0x00, 0x00]); // Version number
let pos = 0; post.setInt32(Math.floor(properties.italicAngle * 2 ** 16)); // italicAngle
post.skip(
pos = setArray(data, pos, [0x00, 0x03, 0x00, 0x00]); // Version number 2 + // underlinePosition, skip redundant "\x00\x00"
pos = setInt32(view, pos, Math.floor(properties.italicAngle * 2 ** 16)); // italicAngle 2 // underlineThickness, skip redundant "\x00\x00"
pos += 2; // underlinePosition, skip redundant "\x00\x00" );
pos += 2; // underlineThickness, skip redundant "\x00\x00" post.setInt32(properties.fixedPitch ? 1 : 0); // isFixedPitch
setInt32(view, pos, properties.fixedPitch ? 1 : 0); // isFixedPitch post.skip(
// minMemType42, skip redundant "\x00\x00\x00\x00" 4 + // minMemType42, skip redundant "\x00\x00\x00\x00"
// maxMemType42, skip redundant "\x00\x00\x00\x00" 4 + // maxMemType42, skip redundant "\x00\x00\x00\x00"
// minMemType1, skip redundant "\x00\x00\x00\x00" 4 + // minMemType1, skip redundant "\x00\x00\x00\x00"
// maxMemType1, skip redundant "\x00\x00\x00\x00" 4 // maxMemType1, skip redundant "\x00\x00\x00\x00"
);
return data; return post.data;
} }
function createPostscriptName(name) { function createPostscriptName(name) {
@ -3299,29 +3364,28 @@ class Font {
"head", "head",
(function fontTableHead() { (function fontTableHead() {
const dateArr = [0x00, 0x00, 0x00, 0x00, 0x9e, 0x0b, 0x7e, 0x27]; 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 const head = new TrueTypeTableBuilder({ exactLength: 54 });
pos = setArray(data, pos, [0x00, 0x00, 0x10, 0x00]); // fontRevision head.setArray([0x00, 0x01, 0x00, 0x00]); // Version number
pos += 4; // checksumAdjustement, skip redundant "\x00\x00\x00\x00" head.setArray([0x00, 0x00, 0x10, 0x00]); // fontRevision
pos = setArray(data, pos, [0x5f, 0x0f, 0x3c, 0xf5]); // magicNumber head.skip(4); // checksumAdjustement, skip redundant "\x00\x00\x00\x00"
pos += 2; // Flags, skip redundant "\x00\x00" head.setArray([0x5f, 0x0f, 0x3c, 0xf5]); // magicNumber
pos = setSafeInt16(view, pos, unitsPerEm); // unitsPerEM head.skip(2); // Flags, skip redundant "\x00\x00"
pos = setArray(data, pos, dateArr); // creation date head.setSafeInt16(unitsPerEm); // unitsPerEM
pos = setArray(data, pos, dateArr); // modifification date head.setArray(dateArr); // creation date
pos += 2; // xMin, skip redundant "\x00\x00" head.setArray(dateArr); // modifification date
pos = setSafeInt16(view, pos, properties.descent); // yMin head.skip(2); // xMin, skip redundant "\x00\x00"
pos = setArray(data, pos, [0x0f, 0xff]); // xMax head.setSafeInt16(properties.descent); // yMin
pos = setSafeInt16(view, pos, properties.ascent); // yMax head.setArray([0x0f, 0xff]); // xMax
pos = setInt16(view, pos, properties.italicAngle ? 2 : 0); // macStyle head.setSafeInt16(properties.ascent); // yMax
setArray(data, pos, [0x00, 0x11]); // lowestRecPPEM head.setInt16(properties.italicAngle ? 2 : 0); // macStyle
// fontDirectionHint, skip redundant "\x00\x00" head.setArray([0x00, 0x11]); // lowestRecPPEM
// indexToLocFormat, skip redundant "\x00\x00" head.skip(
// glyphDataFormat, skip redundant "\x00\x00" 2 + // fontDirectionHint, skip redundant "\x00\x00"
2 + // indexToLocFormat, skip redundant "\x00\x00"
return data; 2 // glyphDataFormat, skip redundant "\x00\x00"
);
return head.data;
})() })()
); );
@ -3329,33 +3393,31 @@ class Font {
builder.addTable( builder.addTable(
"hhea", "hhea",
(function fontTableHhea() { (function fontTableHhea() {
const data = new Uint8Array(36), const hhea = new TrueTypeTableBuilder({ exactLength: 36 });
view = new DataView(data.buffer); hhea.setArray([0x00, 0x01, 0x00, 0x00]); // Version number
let pos = 0; hhea.setSafeInt16(properties.ascent); // Typographic Ascent
hhea.setSafeInt16(properties.descent); // Typographic Descent
pos = setArray(data, pos, [0x00, 0x01, 0x00, 0x00]); // Version number hhea.skip(2); // Line Gap, skip redundant "\x00\x00"
pos = setSafeInt16(view, pos, properties.ascent); // Typographic Ascent hhea.setArray([0xff, 0xff]); // advanceWidthMax
pos = setSafeInt16(view, pos, properties.descent); // Typographic Descent hhea.skip(
pos += 2; // Line Gap, skip redundant "\x00\x00" 2 + // minLeftSidebearing, skip redundant "\x00\x00"
pos = setArray(data, pos, [0xff, 0xff]); // advanceWidthMax 2 + // minRightSidebearing, skip redundant "\x00\x00"
pos += 2; // minLeftSidebearing, skip redundant "\x00\x00" 2 // xMaxExtent, skip redundant "\x00\x00"
pos += 2; // minRightSidebearing, skip redundant "\x00\x00" );
pos += 2; // xMaxExtent, skip redundant "\x00\x00" hhea.setSafeInt16(properties.capHeight); // caretSlopeRise
pos = setSafeInt16(view, pos, properties.capHeight); // caretSlopeRise hhea.setSafeInt16(
pos = setSafeInt16(
view,
pos,
Math.tan(properties.italicAngle) * properties.xHeight Math.tan(properties.italicAngle) * properties.xHeight
); // caretSlopeRun ); // caretSlopeRun
pos += 2; // caretOffset, skip redundant "\x00\x00" hhea.skip(
pos += 2; // -reserved-, skip redundant "\x00\x00" 2 + // caretOffset, skip redundant "\x00\x00"
pos += 2; // -reserved-, skip redundant "\x00\x00" 2 + // -reserved-, skip redundant "\x00\x00"
pos += 2; // -reserved-, skip redundant "\x00\x00" 2 + // -reserved-, skip redundant "\x00\x00"
pos += 2; // -reserved-, skip redundant "\x00\x00" 2 + // -reserved-, skip redundant "\x00\x00"
pos += 2; // metricDataFormat, skip redundant "\x00\x00" 2 + // -reserved-, skip redundant "\x00\x00"
setInt16(view, pos, numGlyphs); // Number of HMetrics 2 // metricDataFormat, skip redundant "\x00\x00"
);
return data; hhea.setInt16(numGlyphs); // Number of HMetrics
return hhea.data;
})() })()
); );
@ -3366,10 +3428,9 @@ class Font {
const charstrings = font.charstrings; const charstrings = font.charstrings;
const cffWidths = font.cff?.widths ?? null; const cffWidths = font.cff?.widths ?? null;
const data = new Uint8Array(numGlyphs * 4), const hmtx = new TrueTypeTableBuilder({ exactLength: numGlyphs * 4 });
view = new DataView(data.buffer);
// Fake .notdef (width=0 and lsb=0) first, skip redundant assignment. // 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++) { for (let i = 1, ii = numGlyphs; i < ii; i++) {
let width = 0; let width = 0;
@ -3378,10 +3439,10 @@ class Font {
} else if (cffWidths) { } else if (cffWidths) {
width = Math.ceil(cffWidths[i] || 0); width = Math.ceil(cffWidths[i] || 0);
} }
pos = setInt16(view, pos, width); hmtx.setInt16(width);
pos += 2; // Use lsb=0, skip redundant assignment. hmtx.skip(2); // Use lsb=0, skip redundant assignment.
} }
return data; return hmtx.data;
})() })()
); );
@ -3389,13 +3450,10 @@ class Font {
builder.addTable( builder.addTable(
"maxp", "maxp",
(function fontTableMaxp() { (function fontTableMaxp() {
const data = new Uint8Array(6), const maxp = new TrueTypeTableBuilder({ exactLength: 6 });
view = new DataView(data.buffer); maxp.setArray([0x00, 0x00, 0x50, 0x00]); // Version number
maxp.setInt16(numGlyphs); // Num of glyphs
setArray(data, 0, [0x00, 0x00, 0x50, 0x00]); // Version number return maxp.data;
setInt16(view, 4, numGlyphs); // Num of glyphs
return data;
})() })()
); );