diff --git a/src/core/cff_parser.js b/src/core/cff_parser.js index 7c97124e0..50b42ea2d 100644 --- a/src/core/cff_parser.js +++ b/src/core/cff_parser.js @@ -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); } } } diff --git a/src/core/data_builder.js b/src/core/data_builder.js new file mode 100644 index 000000000..325ad7f53 --- /dev/null +++ b/src/core/data_builder.js @@ -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 }; diff --git a/src/core/fonts.js b/src/core/fonts.js index f5b0efb1f..4f5932d31 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -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;