diff --git a/CHANGELOG.md b/CHANGELOG.md index 72c60d08..ad6b9729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Unreleased +- Fix `doc.table()` parsing a font passed as binary data (e.g. registered with `registerFont(name, uint8Array)`) again for every cell, when it restores the font after measuring and drawing the cell, which made tables with such fonts slower. A font loaded from the same `Uint8Array` is now reused without parsing it again - Add `lineBreak: 'whitespace'` to `doc.text()`, which breaks a line only at whitespace, a soft hyphen or a zero-width space. The Unicode line breaking rules the wrapper follows also allow a break after a hyphen or a slash, so `design-system` or `https://github.com/foliojs/pdfkit` could be cut at a line end, and a text extractor that joins the lines, such as a résumé parser, reads two words - Add an `objectStreams` option, which packs the objects that are not streams into compressed object streams and writes a cross-reference stream instead of the xref table, making documents with many such objects, e.g. tagged ones, much smaller. It requires a `pdfVersion` of `1.5` or higher, and cannot be used with encryption or PDF/A-1. Fixes #1810 - Fix wrapped text with `align: 'center'` sitting left of the middle of the line when `characterSpacing` or `wordSpacing` is set, and with `align: 'right'` running past the right edge by the `wordSpacing` of each gap between words diff --git a/lib/mixins/fonts.js b/lib/mixins/fonts.js index bb2da590..8bf690d2 100644 --- a/lib/mixins/fonts.js +++ b/lib/mixins/fonts.js @@ -29,6 +29,8 @@ export default { ) { // Lookup table for embedded fonts this._fontFamilies = {}; + // Embedded fonts by binary source (e.g. Uint8Array), so passing the same object again does not parse the font again + this._fontsBySource = new WeakMap(); this._fontCount = 0; // Font state @@ -80,6 +82,14 @@ export default { return this; } + // fast path for binary data that was already loaded, so passing the same + // object again does not parse the font again. Not used with a family + const bySource = !family && src !== null && typeof src === 'object'; + if (bySource && (font = this._fontsBySource.get(src))) { + this._font = font; + return this; + } + // load the font const id = `F${++this._fontCount}`; this._font = PDFFontFactory.open(this, src, family, id); @@ -91,9 +101,12 @@ export default { isEqualFont(this._font, font) ) { this._font = font; + if (bySource) this._fontsBySource.set(src, font); return this; } + if (bySource) this._fontsBySource.set(src, this._font); + // save the font for reuse later if (cacheKey) { this._fontFamilies[cacheKey] = this._font; diff --git a/tests/unit/font.spec.js b/tests/unit/font.spec.js index c4cc4c14..64b7f0c0 100644 --- a/tests/unit/font.spec.js +++ b/tests/unit/font.spec.js @@ -395,3 +395,62 @@ describe('font name collision', () => { }); }); }); + +describe('binary font sources', () => { + const data = () => + new Uint8Array(readFileSync('tests/fonts/Roboto-Regular.ttf')); + + test('the same data passed twice is parsed once', () => { + const doc = new PDFDocument({ font: null }); + const open = vi.spyOn(PDFFontFactory, 'open'); + const buf = data(); + + doc.font(buf); + const first = doc._font; + doc.font(buf); + + expect(doc._font.id).toBe(first.id); + expect(open).toHaveBeenCalledTimes(1); + open.mockRestore(); + }); + + test('a registered binary font is parsed once when tables restore it', () => { + const doc = new PDFDocument({ font: null }); + doc.registerFont('Body', data()); + doc.font('Body'); + const open = vi.spyOn(PDFFontFactory, 'open'); + + doc.table({ + data: [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ], + }); + + expect(open).not.toHaveBeenCalled(); + expect(doc._font.name).toBe('Roboto-Regular'); + open.mockRestore(); + }); + + test('different data of the same font still embeds one font', () => { + const doc = new PDFDocument({ font: null }); + + doc.font(data()); + const first = doc._font; + doc.font(data()); + + expect(doc._font.id).toBe(first.id); + }); + + test('a duplicate buffer passed again reuses the embedded font', () => { + const doc = new PDFDocument({ font: null }); + const buf = data(); + + doc.font(data()); + const first = doc._font; + doc.font(buf); + doc.font(buf); + + expect(doc._font.id).toBe(first.id); + }); +});