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);
}
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;
})()
);