Skip to content

Commit ac12699

Browse files
committed
✨ Introduce PdfMaker class
So far, font data had to be embedded directly within the document definition, despite being more logically associated with the renderer than the document itself. This also made it hard to inspect or debug document definitions due to the inclusion of large binary font data. This commit introduces a new `PdfMaker` class that replaces the `makePdf` function. This class allows font data to be registered separately and reused across multiple documents. With this approach, font data is no longer part of the document definition. Example: ```ts const pdfMaker = new PdfMaker(); pdfMaker.registerFont(await readFile('path/to/MyFont.ttf')); pdfMaker.registerFont(await readFile('path/to/MyFont-Bold.ttf')); const pdf1 = await pdfMaker.makePdf(doc1); const pdf2 = await pdfMaker.makePdf(doc2); ```
1 parent d0cbc2b commit ac12699

File tree

7 files changed

+102
-3
lines changed

7 files changed

+102
-3
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ Minimum requirements bumped to Node 20 and npm 10.
1515
- The functions `text()`, `image()`, `rows()`, and `columns()` to create
1616
blocks with less code and better tool support.
1717

18+
- The `PdfMaker` class to render multiple documents with the same
19+
font configuration.
20+
21+
```ts
22+
const pdfMaker = new PdfMaker(config);
23+
pdfMaker.registerFont(await readFile('path/to/MyFont.ttf'));
24+
pdfMaker.registerFont(await readFile('path/to/MyFont-Bold.ttf'));
25+
const pdf1 = await pdfMaker.makePdf(doc1);
26+
const pdf2 = await pdfMaker.makePdf(doc2);
27+
```
28+
1829
### Deprecated
1930

2031
- `TextAttrs` in favor of `TextProps`.
@@ -26,6 +37,9 @@ Minimum requirements bumped to Node 20 and npm 10.
2637
- `RectOpts` in favor of `RectProps`.
2738
- `CircleOpts` in favor of `CircleProps`.
2839
- `PathOpts` in favor of `PathProps`.
40+
- The `fonts` property in a document definition.
41+
- The `makePdf` function in favor of the `makePdf` method on the
42+
`PdfMaker` class.
2943

3044
## [0.5.4] - 2024-02-25
3145

src/api/PdfMaker.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { FontStore } from '../font-store.ts';
2+
import { ImageStore } from '../image-store.ts';
3+
import { layoutPages } from '../layout/layout.ts';
4+
import type { MakerCtx } from '../maker-ctx.ts';
5+
import { readDocumentDefinition } from '../read-document.ts';
6+
import { renderDocument } from '../render/render-document.ts';
7+
import { readAs } from '../types.ts';
8+
import type { DocumentDefinition } from './document.ts';
9+
import type { FontStyle, FontWeight } from './text.ts';
10+
11+
export type FontConfig = {
12+
name?: string;
13+
style?: FontStyle;
14+
weight?: FontWeight;
15+
};
16+
17+
/**
18+
* Generates PDF documents.
19+
*/
20+
export class PdfMaker {
21+
#ctx: MakerCtx;
22+
23+
constructor() {
24+
const fontStore = new FontStore([]);
25+
const imageStore = new ImageStore([]);
26+
this.#ctx = { fontStore, imageStore };
27+
}
28+
29+
/**
30+
* Registers a font to be used in generated PDFs.
31+
*
32+
* @param data The font data. Must be in OpenType (OTF) or TrueType
33+
* (TTF) format.
34+
* @param config Additional configuration of the font, only needed if
35+
* the meta data cannot be extracted from the font.
36+
*/
37+
registerFont(data: Uint8Array, config?: FontConfig): void {
38+
this.#ctx.fontStore.registerFont(data, config);
39+
}
40+
41+
/**
42+
* Generates a PDF from the given document definition.
43+
*
44+
* @param definition The definition of the document to generate.
45+
* @returns The generated PDF document.
46+
*/
47+
async makePdf(definition: DocumentDefinition): Promise<Uint8Array> {
48+
const def = readAs(definition, 'definition', readDocumentDefinition);
49+
const ctx = { ...this.#ctx };
50+
if (def.fonts) ctx.fontStore = new FontStore(def.fonts);
51+
if (def.images) ctx.imageStore = new ImageStore(def.images);
52+
if (def.dev?.guides != null) ctx.guides = def.dev.guides;
53+
const pages = await layoutPages(def, ctx);
54+
return await renderDocument(def, pages);
55+
}
56+
}

src/api/document.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ export type DocumentDefinition = {
5454
/**
5555
* The fonts to use in the document. There is no default. Each font that is used in the document
5656
* must be registered. Not needed for documents that contain only graphics.
57+
*
58+
* @deprecated Register fonts with `PdfMaker` instead.
5759
*/
5860
fonts?: FontsDefinition;
5961

src/api/make-pdf.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import type { DocumentDefinition } from './document.ts';
1111
*
1212
* @param definition The definition of the document to generate.
1313
* @returns The generated PDF document.
14+
*
15+
* @deprecated Create an instance of `PdfMaker` and call `makePdf` on
16+
* that instance.
1417
*/
1518
export async function makePdf(definition: DocumentDefinition): Promise<Uint8Array> {
1619
const def = readAs(definition, 'definition', readDocumentDefinition);

src/font-store.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
11
import fontkit from '@pdf-lib/fontkit';
22
import { toUint8Array } from 'pdf-lib';
33

4-
import type { FontWeight } from './api/text.ts';
4+
import type { FontConfig } from './api/PdfMaker.ts';
5+
import type { FontStyle, FontWeight } from './api/text.ts';
56
import type { Font, FontDef, FontSelector } from './fonts.ts';
67
import { weightToNumber } from './fonts.ts';
78
import { pickDefined } from './types.ts';
89

910
export class FontStore {
1011
readonly #fontDefs: FontDef[];
11-
readonly #fontCache: Record<string, Promise<Font>> = {};
12+
#fontCache: Record<string, Promise<Font>> = {};
1213

1314
constructor(fontDefs: FontDef[]) {
1415
this.#fontDefs = fontDefs;
1516
}
1617

18+
registerFont(data: Uint8Array, config?: FontConfig): void {
19+
const fkFont = fontkit.create(data, config?.name);
20+
const family = config?.name ?? fkFont.familyName ?? 'Unknown';
21+
const style = config?.style ?? extractStyle(fkFont);
22+
const weight = weightToNumber(config?.weight ?? extractWeight(fkFont));
23+
this.#fontDefs.push({ family, style, weight, data, fkFont });
24+
this.#fontCache = {};
25+
}
26+
1727
async selectFont(selector: FontSelector): Promise<Font> {
1828
const cacheKey = [
1929
selector.fontFamily ?? 'any',
@@ -32,7 +42,7 @@ export class FontStore {
3242
_loadFont(selector: FontSelector): Promise<Font> {
3343
const selectedFont = selectFont(this.#fontDefs, selector);
3444
const data = toUint8Array(selectedFont.data);
35-
const fkFont = fontkit.create(data);
45+
const fkFont = selectedFont.fkFont ?? fontkit.create(data);
3646
return Promise.resolve(
3747
pickDefined({
3848
name: fkFont.fullName ?? fkFont.postscriptName ?? selectedFont.family,
@@ -109,3 +119,13 @@ function selectFontForWeight(fonts: FontDef[], weight: FontWeight): FontDef | un
109119
}
110120
throw new Error(`Could not find font for weight ${weight}`);
111121
}
122+
123+
function extractStyle(font: fontkit.Font): FontStyle {
124+
if (font.italicAngle === 0) return 'normal';
125+
if ((font.fullName ?? font.postscriptName)?.toLowerCase().includes('oblique')) return 'oblique';
126+
return 'italic';
127+
}
128+
129+
function extractWeight(font: fontkit.Font): number {
130+
return (font['OS/2'] as any)?.usWeightClass ?? 400;
131+
}

src/fonts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type FontDef = {
1515
style: FontStyle;
1616
weight: number;
1717
data: string | Uint8Array | ArrayBuffer;
18+
fkFont?: fontkit.Font;
1819
};
1920

2021
export type Font = {

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as document from './api/document.ts';
33
import * as graphics from './api/graphics.ts';
44
import * as layout from './api/layout.ts';
55
import * as makePdf from './api/make-pdf.ts';
6+
import * as pdfMaker from './api/PdfMaker.ts';
67
import * as sizes from './api/sizes.ts';
78
import * as text from './api/text.ts';
89

@@ -12,6 +13,7 @@ export const pdf = {
1213
...graphics,
1314
...layout,
1415
...makePdf,
16+
...pdfMaker,
1517
...sizes,
1618
...text,
1719
};
@@ -21,5 +23,6 @@ export * from './api/document.ts';
2123
export * from './api/graphics.ts';
2224
export * from './api/layout.ts';
2325
export * from './api/make-pdf.ts';
26+
export * from './api/PdfMaker.ts';
2427
export * from './api/sizes.ts';
2528
export * from './api/text.ts';

0 commit comments

Comments
 (0)