Merge pull request #21220 from Snuffleupagus/DataBuilder

Replace `TrueTypeTableBuilder` and `CompilerOutput` with a single class
This commit is contained in:
Jonas Jenwald 2026-05-05 13:07:31 +02:00 committed by GitHub
commit e8d3d19f67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 161 additions and 186 deletions

View File

@ -28,6 +28,7 @@ import {
ISOAdobeCharset,
} from "./charsets.js";
import { ExpertEncoding, StandardEncoding } from "./encodings.js";
import { DataBuilder } from "./data_builder.js";
// Maximum subroutine call depth of type 2 charstrings. Matches OTS.
const MAX_SUBR_NESTING = 10;
@ -1365,51 +1366,6 @@ class CFFOffsetTracker {
}
}
class CompilerOutput {
#buf;
#bufLength = 1024;
#pos = 0;
constructor(minLength) {
// Note: Usually the compiled size is smaller than the initial data,
// however in some cases it may increase slightly.
this.#initBuf(minLength);
}
#initBuf(minLength) {
// 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;
}
get data() {
return this.#buf.subarray(0, this.#pos);
}
get length() {
return this.#pos;
}
add(data) {
const newPos = this.#pos + data.length;
if (newPos > this.#bufLength) {
// It should be very rare that the buffer needs to grow.
this.#initBuf(newPos);
}
this.#buf.set(data, this.#pos);
this.#pos = newPos;
}
}
// Takes a CFF and converts it to the binary representation.
class CFFCompiler {
constructor(cff) {
@ -1418,14 +1374,14 @@ class CFFCompiler {
compile() {
const cff = this.cff;
const output = new CompilerOutput(cff.rawFileLength);
const output = new DataBuilder({ minLength: cff.rawFileLength });
// Compile the five entries that must be in order.
const header = this.compileHeader(cff.header);
output.add(header);
output.setArray(header);
const nameIndex = this.compileNameIndex(cff.names);
output.add(nameIndex);
output.setArray(nameIndex);
if (cff.isCIDFont) {
// The spec is unclear on how font matrices should relate to each other
@ -1465,14 +1421,14 @@ class CFFCompiler {
output.length,
cff.isCIDFont
);
output.add(compiled.output);
output.setArray(compiled.output);
const topDictTracker = compiled.trackers[0];
const stringIndex = this.compileStringIndex(cff.strings.strings);
output.add(stringIndex);
output.setArray(stringIndex);
const globalSubrIndex = this.compileIndex(cff.globalSubrIndex);
output.add(globalSubrIndex);
output.setArray(globalSubrIndex);
// Now start on the other entries that have no specific order.
if (cff.encoding && cff.topDict.hasName("Encoding")) {
@ -1485,7 +1441,7 @@ class CFFCompiler {
} else {
const encoding = this.compileEncoding(cff.encoding);
topDictTracker.setEntryLocation("Encoding", [output.length], output);
output.add(encoding);
output.setArray(encoding);
}
}
const charset = this.compileCharset(
@ -1495,23 +1451,23 @@ class CFFCompiler {
cff.isCIDFont
);
topDictTracker.setEntryLocation("charset", [output.length], output);
output.add(charset);
output.setArray(charset);
const charStrings = this.compileCharStrings(cff.charStrings);
topDictTracker.setEntryLocation("CharStrings", [output.length], output);
output.add(charStrings);
output.setArray(charStrings);
if (cff.isCIDFont) {
// For some reason FDSelect must be in front of FDArray on windows. OSX
// and linux don't seem to care.
topDictTracker.setEntryLocation("FDSelect", [output.length], output);
const fdSelect = this.compileFDSelect(cff.fdSelect);
output.add(fdSelect);
output.setArray(fdSelect);
// It is unclear if the sub font dictionary can have CID related
// dictionary keys, but the sanitizer doesn't like them so remove them.
compiled = this.compileTopDicts(cff.fdArray, output.length, true);
topDictTracker.setEntryLocation("FDArray", [output.length], output);
output.add(compiled.output);
output.setArray(compiled.output);
const fontDictTrackers = compiled.trackers;
this.compilePrivateDicts(cff.fdArray, fontDictTrackers, output);
@ -1521,7 +1477,7 @@ class CFFCompiler {
// If the font data ends with INDEX whose object data is zero-length,
// the sanitizer will bail out. Add a dummy byte to avoid that.
output.add([0]);
output.setArray([0]);
return output.data;
}
@ -1689,7 +1645,7 @@ class CFFCompiler {
[privateDictData.length, outputLength],
output
);
output.add(privateDictData);
output.setArray(privateDictData);
if (privateDict.subrsIndex && privateDict.hasName("Subrs")) {
const subrs = this.compileIndex(privateDict.subrsIndex);
@ -1698,7 +1654,7 @@ class CFFCompiler {
[privateDictData.length],
output
);
output.add(subrs);
output.setArray(subrs);
}
}
}

125
src/core/data_builder.js Normal file
View File

@ -0,0 +1,125 @@
/* Copyright 2026 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert } from "../shared/util.js";
import { MathClamp } from "../shared/math_clamp.js";
class DataBuilder {
#buf;
#bufLength = 1024;
#hasExactLength = false;
#pos = 0;
#view;
constructor({ exactLength = 0, minLength = 0 }) {
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;
}
}
export { DataBuilder };

View File

@ -58,10 +58,10 @@ import {
import { IdentityToUnicodeMap, ToUnicodeMap } from "./to_unicode_map.js";
import { CFFFont } from "./cff_font.js";
import { compileFontInfo } from "./obj_bin_transform_core.js";
import { DataBuilder } from "./data_builder.js";
import { FontRendererFactory } from "./font_renderer.js";
import { getFontBasicMetrics } from "./metrics.js";
import { GlyfTable } from "./glyf.js";
import { MathClamp } from "../shared/math_clamp.js";
import { OpenTypeFileBuilder } from "./opentype_file_builder.js";
import { Stream } from "./stream.js";
import { Type1Font } from "./type1_font.js";
@ -302,112 +302,6 @@ function writeUint32(bytes, index, value) {
bytes[index] = value >>> 24;
}
class TrueTypeTableBuilder {
#buf;
#bufLength = 1024;
#hasExactLength = false;
#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;
}
}
function isTrueTypeFile(file) {
const header = file.peekBytes(4),
str = bytesToString(header);
@ -679,7 +573,7 @@ function createCmapTable(glyphs, toUnicodeExtraMap, numGlyphs) {
const ranges = getRanges(glyphs, toUnicodeExtraMap, numGlyphs);
const numTables = ranges.at(-1)[1] > 0xffff ? 2 : 1;
const cmap = new TrueTypeTableBuilder({ exactLength: 12 });
const cmap = new DataBuilder({ exactLength: 12 });
cmap.skip(2); // version, skip redundant "\x00\x00"
cmap.setInt16(numTables); // numTables
cmap.setArray([0x00, 0x03]); // platformID
@ -703,11 +597,11 @@ function createCmapTable(glyphs, toUnicodeExtraMap, numGlyphs) {
// Fill up the 4 parallel arrays describing the segments.
const segmentsLength = bmpLength * 2 + trailingRangesCount * 2;
const startCount = new TrueTypeTableBuilder({ exactLength: segmentsLength }),
endCount = new TrueTypeTableBuilder({ exactLength: segmentsLength }),
idDeltas = new TrueTypeTableBuilder({ exactLength: segmentsLength }),
idRangeOffsets = new TrueTypeTableBuilder({ exactLength: segmentsLength }),
glyphsIds = new TrueTypeTableBuilder({});
const startCount = new DataBuilder({ exactLength: segmentsLength }),
endCount = new DataBuilder({ exactLength: segmentsLength }),
idDeltas = new DataBuilder({ exactLength: segmentsLength }),
idRangeOffsets = new DataBuilder({ exactLength: segmentsLength }),
glyphsIds = new DataBuilder({});
let bias = 0;
for (i = 0, ii = bmpLength; i < ii; i++) {
@ -746,7 +640,7 @@ function createCmapTable(glyphs, toUnicodeExtraMap, numGlyphs) {
idRangeOffsets.skip(2); // Skip redundant "\x00\x00"
}
const format314 = new TrueTypeTableBuilder({
const format314 = new DataBuilder({
exactLength:
12 +
startCount.length +
@ -771,12 +665,12 @@ function createCmapTable(glyphs, toUnicodeExtraMap, numGlyphs) {
format31012 = null,
header31012 = null;
if (numTables > 1) {
cmap31012 = new TrueTypeTableBuilder({ exactLength: 8 });
cmap31012 = new DataBuilder({ exactLength: 8 });
cmap31012.setArray([0x00, 0x03]); // platformID
cmap31012.setArray([0x00, 0x0a]); // encodingID
cmap31012.setInt32(4 + numTables * 8 + 4 + format314.length); // start of the table record
format31012 = new TrueTypeTableBuilder({});
format31012 = new DataBuilder({});
for (const range of ranges) {
let start = range[0];
const codes = range[2];
@ -796,7 +690,7 @@ function createCmapTable(glyphs, toUnicodeExtraMap, numGlyphs) {
format31012.setInt32(code); // startGlyphID
}
header31012 = new TrueTypeTableBuilder({ exactLength: 16 });
header31012 = new DataBuilder({ exactLength: 16 });
header31012.setArray([0x00, 0x0c]); // format
header31012.skip(2); // reserved, skip redundant "\x00\x00"
header31012.setInt32(format31012.length + 16); // length
@ -804,7 +698,7 @@ function createCmapTable(glyphs, toUnicodeExtraMap, numGlyphs) {
header31012.setInt32(format31012.length / 12); // nGroups
}
const table = new TrueTypeTableBuilder({
const table = new DataBuilder({
exactLength:
4 +
cmap.length +
@ -927,7 +821,7 @@ function createOS2Table(properties, charstrings, override) {
const winAscent = override.yMax || typoAscent;
const winDescent = -override.yMin || -typoDescent;
const os2 = new TrueTypeTableBuilder({ exactLength: 96 });
const os2 = new DataBuilder({ exactLength: 96 });
os2.setArray([0x00, 0x03]); // version
os2.setArray([0x02, 0x24]); // xAvgCharWidth
os2.setArray([0x01, 0xf4]); // usWeightClass
@ -982,7 +876,7 @@ function createOS2Table(properties, charstrings, override) {
}
function createPostTable(properties) {
const post = new TrueTypeTableBuilder({ exactLength: 32 });
const post = new DataBuilder({ exactLength: 32 });
post.setArray([0x00, 0x03, 0x00, 0x00]); // Version number
post.setInt32(Math.floor(properties.italicAngle * 2 ** 16)); // italicAngle
post.skip(
@ -1028,7 +922,7 @@ function createNameTable(name, proto) {
for (i = 0, ii = strings.length; i < ii; i++) {
str = proto[1][i] || strings[i];
const strUnicode = new TrueTypeTableBuilder({
const strUnicode = new DataBuilder({
exactLength: str.length * 2,
});
for (j = 0, jj = str.length; j < jj; j++) {
@ -1058,7 +952,7 @@ function createNameTable(name, proto) {
const strs = namesBytes[i];
for (j = 0, jj = strs.length; j < jj; j++) {
str = strs[j];
const nameRecord = new TrueTypeTableBuilder({
const nameRecord = new DataBuilder({
exactLength:
6 +
platformsBytes[i].length +
@ -1078,7 +972,7 @@ function createNameTable(name, proto) {
}
const namesRecordCount = stringsBytes.length * platformsBytes.length;
const nameTable = new TrueTypeTableBuilder({
const nameTable = new DataBuilder({
exactLength:
6 +
Math.sumPrecise(nameRecords.map(arr => arr.length)) +
@ -3407,7 +3301,7 @@ class Font {
(function fontTableHead() {
const dateArr = [0x00, 0x00, 0x00, 0x00, 0x9e, 0x0b, 0x7e, 0x27];
const head = new TrueTypeTableBuilder({ exactLength: 54 });
const head = new DataBuilder({ 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"
@ -3435,7 +3329,7 @@ class Font {
builder.addTable(
"hhea",
(function fontTableHhea() {
const hhea = new TrueTypeTableBuilder({ exactLength: 36 });
const hhea = new DataBuilder({ exactLength: 36 });
hhea.setArray([0x00, 0x01, 0x00, 0x00]); // Version number
hhea.setSafeInt16(properties.ascent); // Typographic Ascent
hhea.setSafeInt16(properties.descent); // Typographic Descent
@ -3470,7 +3364,7 @@ class Font {
const charstrings = font.charstrings;
const cffWidths = font.cff?.widths ?? null;
const hmtx = new TrueTypeTableBuilder({ exactLength: numGlyphs * 4 });
const hmtx = new DataBuilder({ exactLength: numGlyphs * 4 });
// Fake .notdef (width=0 and lsb=0) first, skip redundant assignment.
hmtx.skip(4);
@ -3492,7 +3386,7 @@ class Font {
builder.addTable(
"maxp",
(function fontTableMaxp() {
const maxp = new TrueTypeTableBuilder({ exactLength: 6 });
const maxp = new DataBuilder({ exactLength: 6 });
maxp.setArray([0x00, 0x00, 0x50, 0x00]); // Version number
maxp.setInt16(numGlyphs); // Num of glyphs
return maxp.data;