From 39d2be355b151a3e54269be6cfa398df0fe6d018 Mon Sep 17 00:00:00 2001 From: Sylvain Gougouzian Date: Thu, 15 Oct 2026 08:53:56 +0200 Subject: [PATCH] Share standard font metrics across documents StandardFont built a new AFMFont for every document, rebuilding the glyph width and kerning tables from the same font data each time. AFMFont only reads that data, so keep one per font data in a WeakMap. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + lib/font/standard.js | 10 +++++++++- tests/unit/font.spec.js | 10 ++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad6b9729..d9e97eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Unreleased +- Reuse the glyph width and kerning tables of a standard font across documents instead of building them again for every document that uses it, which made a short document using two standard fonts more than twice as slow to create - 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 diff --git a/lib/font/standard.js b/lib/font/standard.js index 57dd8b65..2a57483b 100644 --- a/lib/font/standard.js +++ b/lib/font/standard.js @@ -1,13 +1,21 @@ import AFMFont from './afm'; import PDFFont from '../font'; +// AFMFont only reads its data, so every document can share the one built for +// a given font data instead of rebuilding its glyph width and kerning tables. +const afmFonts = new WeakMap(); + class StandardFont extends PDFFont { constructor(document, fontData, id) { super(); this.document = document; this.name = fontData.name; this.id = id; - this.font = new AFMFont(fontData); + this.font = afmFonts.get(fontData); + if (!this.font) { + this.font = new AFMFont(fontData); + afmFonts.set(fontData, this.font); + } ({ ascender: this.ascender, descender: this.descender, diff --git a/tests/unit/font.spec.js b/tests/unit/font.spec.js index 64b7f0c0..83c26900 100644 --- a/tests/unit/font.spec.js +++ b/tests/unit/font.spec.js @@ -454,3 +454,13 @@ describe('binary font sources', () => { expect(doc._font.id).toBe(first.id); }); }); + +describe('standard font metrics', () => { + test('are shared across documents', () => { + const first = new PDFDocument().font('Helvetica-Bold')._font; + const second = new PDFDocument().font('Helvetica-Bold')._font; + + expect(second).not.toBe(first); + expect(second.font).toBe(first.font); + }); +});