Merge pull request #21053 from Snuffleupagus/CFFCompiler-TypedArray

Reduce allocations when compiling CFF fonts
This commit is contained in:
calixteman 2026-04-07 16:55:02 +02:00 committed by GitHub
commit 1bf1ef2939
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 121 additions and 77 deletions

View File

@ -228,7 +228,7 @@ class CFFParser {
parse() {
const properties = this.properties;
const cff = new CFF();
const cff = new CFF(this.bytes.length);
this.cff = cff;
// The first five sections must be in order, all the others are reached
@ -1017,6 +1017,10 @@ class CFF {
charStringCount = 0;
constructor(rawFileLength = 0) {
this.rawFileLength = rawFileLength;
}
duplicateFirstGlyph() {
// Browsers will not display a glyph at position 0. Typically glyph 0 is
// notdef, but a number of fonts put a valid glyph there so it must be
@ -1372,6 +1376,57 @@ 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 finalData() {
const data = this.#buf.slice(0, this.#pos);
this.#buf = null;
return data;
}
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) {
@ -1380,21 +1435,7 @@ class CFFCompiler {
compile() {
const cff = this.cff;
const output = {
data: [],
length: 0,
add(data) {
try {
// It's possible to exceed the call stack maximum size when trying
// to push too much elements.
// In case of failure, we fallback to the `concat` method.
this.data.push(...data);
} catch {
this.data = this.data.concat(data);
}
this.length = this.data.length;
},
};
const output = new CompilerOutput(cff.rawFileLength);
// Compile the five entries that must be in order.
const header = this.compileHeader(cff.header);
@ -1499,7 +1540,7 @@ class CFFCompiler {
// the sanitizer will bail out. Add a dummy byte to avoid that.
output.add([0]);
return output.data;
return output.finalData;
}
encodeNumber(value) {
@ -1781,7 +1822,7 @@ class CFFCompiler {
} else {
const length = 1 + numGlyphsLessNotDef * 2;
out = new Uint8Array(length);
out[0] = 0; // format 0
// format 0, skip redundant `out[0] = 0;` assignment.
let charsetIndex = 0;
const numCharsets = charset.charset.length;
let warned = false;
@ -1802,11 +1843,11 @@ class CFFCompiler {
out[i + 1] = sid & 0xff;
}
}
return this.compileTypedArray(out);
return out;
}
compileEncoding(encoding) {
return this.compileTypedArray(encoding.raw);
return encoding.raw;
}
compileFDSelect(fdSelect) {
@ -1847,11 +1888,7 @@ class CFFCompiler {
out = new Uint8Array(ranges);
break;
}
return this.compileTypedArray(out);
}
compileTypedArray(data) {
return Array.from(data);
return out;
}
compileIndex(index, trackers = []) {
@ -1915,9 +1952,8 @@ class CFFCompiler {
for (i = 0; i < count; i++) {
// Notify the tracker where the object will be offset in the data.
if (trackers[i]) {
trackers[i].offset(data.length);
}
trackers[i]?.offset(data.length);
data.push(...objects[i]);
}
return data;

View File

@ -35,11 +35,6 @@ function writeData(dest, offset, data) {
for (let i = 0, ii = data.length; i < ii; i++) {
dest[offset++] = data.charCodeAt(i) & 0xff;
}
} else {
// treating everything else as array
for (const num of data) {
dest[offset++] = num & 0xff;
}
}
}

View File

@ -153,6 +153,8 @@ function getEexecBlock(stream, suggestedLength) {
* Type1Font is also a CIDFontType0.
*/
class Type1Font {
#rawFileLength;
constructor(name, file, properties) {
// Some bad generators embed pfb file as is, we have to strip 6-byte header.
// Also, length1 and length2 might be off by 6 bytes as well.
@ -200,6 +202,7 @@ class Type1Font {
for (const key in data.properties) {
properties[key] = data.properties[key];
}
this.#rawFileLength = headerBlock.length + eexecBlock.length;
const charstrings = data.charstrings;
const type2Charstrings = this.getType2Charstrings(charstrings);
@ -323,7 +326,7 @@ class Type1Font {
}
wrap(name, glyphs, charstrings, subrs, properties) {
const cff = new CFF();
const cff = new CFF(this.#rawFileLength);
cff.header = new CFFHeader(1, 0, 4, 4);
cff.names = [name];

View File

@ -430,47 +430,53 @@ describe("CFFCompiler", function () {
const fdSelect = new CFFFDSelect(0, [3, 2, 1]);
const c = new CFFCompiler();
const out = c.compileFDSelect(fdSelect);
expect(out).toEqual([
0, // format
3, // gid: 0 fd 3
2, // gid: 1 fd 3
1, // gid: 2 fd 3
]);
expect(out).toEqual(
new Uint8Array([
0, // format
3, // gid: 0 fd 3
2, // gid: 1 fd 3
1, // gid: 2 fd 3
])
);
});
it("compiles fdselect format 3", function () {
const fdSelect = new CFFFDSelect(3, [0, 0, 1, 1]);
const c = new CFFCompiler();
const out = c.compileFDSelect(fdSelect);
expect(out).toEqual([
3, // format
0, // nRanges (high)
2, // nRanges (low)
0, // range struct 0 - first (high)
0, // range struct 0 - first (low)
0, // range struct 0 - fd
0, // range struct 0 - first (high)
2, // range struct 0 - first (low)
1, // range struct 0 - fd
0, // sentinel (high)
4, // sentinel (low)
]);
expect(out).toEqual(
new Uint8Array([
3, // format
0, // nRanges (high)
2, // nRanges (low)
0, // range struct 0 - first (high)
0, // range struct 0 - first (low)
0, // range struct 0 - fd
0, // range struct 0 - first (high)
2, // range struct 0 - first (low)
1, // range struct 0 - fd
0, // sentinel (high)
4, // sentinel (low)
])
);
});
it("compiles fdselect format 3, single range", function () {
const fdSelect = new CFFFDSelect(3, [0, 0]);
const c = new CFFCompiler();
const out = c.compileFDSelect(fdSelect);
expect(out).toEqual([
3, // format
0, // nRanges (high)
1, // nRanges (low)
0, // range struct 0 - first (high)
0, // range struct 0 - first (low)
0, // range struct 0 - fd
0, // sentinel (high)
2, // sentinel (low)
]);
expect(out).toEqual(
new Uint8Array([
3, // format
0, // nRanges (high)
1, // nRanges (low)
0, // range struct 0 - first (high)
0, // range struct 0 - first (low)
0, // range struct 0 - fd
0, // sentinel (high)
2, // sentinel (low)
])
);
});
it("compiles charset of CID font", function () {
@ -479,13 +485,15 @@ describe("CFFCompiler", function () {
const numGlyphs = 7;
const out = c.compileCharset(charset, numGlyphs, new CFFStrings(), true);
// All CID charsets get turned into a simple format 2.
expect(out).toEqual([
2, // format
0, // cid (high)
1, // cid (low)
0, // nLeft (high)
numGlyphs - 2, // nLeft (low)
]);
expect(out).toEqual(
new Uint8Array([
2, // format
0, // cid (high)
1, // cid (low)
0, // nLeft (high)
numGlyphs - 2, // nLeft (low)
])
);
});
it("compiles charset of non CID font", function () {
@ -494,13 +502,15 @@ describe("CFFCompiler", function () {
const numGlyphs = 3;
const out = c.compileCharset(charset, numGlyphs, new CFFStrings(), false);
// All non-CID fonts use a format 0 charset.
expect(out).toEqual([
0, // format
0, // sid of 'space' (high)
1, // sid of 'space' (low)
0, // sid of 'exclam' (high)
2, // sid of 'exclam' (low)
]);
expect(out).toEqual(
new Uint8Array([
0, // format
0, // sid of 'space' (high)
1, // sid of 'space' (low)
0, // sid of 'exclam' (high)
2, // sid of 'exclam' (low)
])
);
});
// TODO a lot more compiler tests