diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eb79b9a..d2e2c034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - [BREAKING CHANGE] Remove the virtual file system (`pdfkit/virtual-fs`). Browser builds no longer depend on `fs`: use `registerFile` to register `Uint8Array` data under a path, pass a `Uint8Array` or `ArrayBuffer` directly to `registerFont`, `image` and `file`, or pass a data URL directly to `image` and `file` - Add `registerFile(path, data, options)` to globally register in-memory files in Node and browsers, with optional `birthtime` and `ctime` metadata. Passing `undefined` as data unregisters the path - Add experimental `toBlob(document)` and `toBytes(document)` output helpers under `pdfkit/output` +- Accept already-parsed fontkit `Font` instances in `doc.font()` and `registerFont` - Load the PDF/A ICC profile from disk only when needed in Node, while continuing to bundle it in browser builds - [BREAKING CHANGE] Restrict AcroForm options to documented mappings and explicit escape hatches. - [BREAKING CHANGE] Stop automatically uppercasing annotation option keys. diff --git a/docs/text.md b/docs/text.md index 4d90b7f6..e7a7a4fa 100644 --- a/docs/text.md +++ b/docs/text.md @@ -232,7 +232,9 @@ and Datafork TrueType (`.dfont`) fonts. To change the font used to render text, just call the `font` method. If you are using a standard PDF font, just pass the name to the `font` method. -Otherwise, pass the path to the font file, or a `Buffer` containing the font data. +Otherwise, pass the path to the font file, a `Buffer` containing the font data, +or an already-parsed [fontkit](https://github.com/foliojs/fontkit) `Font` +instance. If the font is a collection font (`.ttc` and `.dfont` files), meaning that it contains multiple styles in the same file, you should pass the name of the style to be extracted from the collection. diff --git a/lib/font_factory.js b/lib/font_factory.js index 7ba1ab3c..0d702497 100644 --- a/lib/font_factory.js +++ b/lib/font_factory.js @@ -18,6 +18,9 @@ class PDFFontFactory { font = create(src, family); } else if (src instanceof ArrayBuffer) { font = create(new Uint8Array(src), family); + } else if (typeof src?.layout === 'function') { + // already-parsed fontkit font instance + font = src; } if (font == null) { diff --git a/tests/unit/font.spec.js b/tests/unit/font.spec.js index faf28538..c4cc4c14 100644 --- a/tests/unit/font.spec.js +++ b/tests/unit/font.spec.js @@ -1,5 +1,6 @@ import { vi } from 'vitest'; import { readFileSync } from 'fs'; +import { create } from 'fontkit'; import PDFDocument from '../../lib/document'; import PDFFontFactory from '../../lib/font_factory'; import StandardFont from '../../lib/font/standard'; @@ -22,6 +23,60 @@ describe('PDFFontFactory', () => { }); }); +describe('fontkit font instance sources', () => { + const parseFont = (file = 'tests/fonts/Roboto-Regular.ttf') => + create(readFileSync(file)); + + test('doc.font() embeds a parsed font instance and renders text', () => { + const doc = new PDFDocument({ compress: false }); + doc.font(parseFont()).text('parsed roboto'); + + const pdf = collectPdf(doc); + + expect(pdf).toContain('Roboto-Regular'); + expect(pdf).toContain('startxref'); + expect(missingObjects(pdf)).toHaveLength(0); + }); + + test('the same instance passed twice reuses the embedded font', () => { + const doc = new PDFDocument({ font: null }); + const font = parseFont(); + + doc.font(font); + const first = doc._font; + const familyCount = Object.keys(doc._fontFamilies).length; + + doc.font(font); + + expect(doc._font).toBe(first); + expect(Object.keys(doc._fontFamilies)).toHaveLength(familyCount); + }); + + test('different instances do not collide', () => { + const doc = new PDFDocument({ font: null }); + + doc.font(parseFont('tests/fonts/Roboto-Regular.ttf')); + const regular = doc._font; + doc.font(parseFont('tests/fonts/Roboto-Italic.ttf')); + const italic = doc._font; + + expect(italic).not.toBe(regular); + expect(doc._fontFamilies['Roboto-Regular']).toBe(regular); + expect(doc._fontFamilies['Roboto-Italic']).toBe(italic); + }); + + test('registerFont accepts a parsed font instance', () => { + const doc = new PDFDocument({ compress: false }); + doc.registerFont('MyFont', parseFont()); + doc.font('MyFont').text('registered parsed font'); + + expect(doc._fontFamilies['MyFont']).toBe(doc._font); + + const pdf = collectPdf(doc); + expect(missingObjects(pdf)).toHaveLength(0); + }); +}); + describe('EmbeddedFont', () => { test('no fontLayoutCache option', () => { const document = new PDFDocument();