Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions lib/mixins/fonts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
59 changes: 59 additions & 0 deletions tests/unit/font.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Loading