Skip to content

Commit

Permalink
✨ Introduce PdfMaker class
Browse files Browse the repository at this point in the history
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);
```
  • Loading branch information
ralfstx committed Dec 14, 2024
1 parent ccb94ff commit f9e7be5
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 3 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ Minimum requirements bumped to Node 20 and npm 10.
- The functions `text()`, `image()`, `rows()`, and `columns()` to create
blocks with less code and better tool support.

- The `PdfMaker` class to render multiple documents with the same
font configuration.

```ts
const pdfMaker = new PdfMaker(config);
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);
```

### Deprecated

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

## [0.5.4] - 2024-02-25

Expand Down
56 changes: 56 additions & 0 deletions src/api/PdfMaker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { FontStore } from '../font-store.ts';
import { ImageStore } from '../image-store.ts';
import { layoutPages } from '../layout/layout.ts';
import type { MakerCtx } from '../maker-ctx.ts';
import { readDocumentDefinition } from '../read-document.ts';
import { renderDocument } from '../render/render-document.ts';
import { readAs } from '../types.ts';
import type { DocumentDefinition } from './document.ts';
import type { FontStyle, FontWeight } from './text.ts';

export type FontConfig = {
name?: string;
style?: FontStyle;
weight?: FontWeight;
};

/**
* Generates PDF documents.
*/
export class PdfMaker {
#ctx: MakerCtx;

constructor() {
const fontStore = new FontStore([]);
const imageStore = new ImageStore([]);
this.#ctx = { fontStore, imageStore };
}

/**
* Registers a font to be used in generated PDFs.
*
* @param data The font data. Must be in OpenType (OTF) or TrueType
* (TTF) format.
* @param config Additional configuration of the font, only needed if
* the meta data cannot be extracted from the font.
*/
registerFont(data: Uint8Array, config?: FontConfig): void {
this.#ctx.fontStore.registerFont(data, config);
}

/**
* Generates a PDF from the given document definition.
*
* @param definition The definition of the document to generate.
* @returns The generated PDF document.
*/
async makePdf(definition: DocumentDefinition): Promise<Uint8Array> {
const def = readAs(definition, 'definition', readDocumentDefinition);
const ctx = { ...this.#ctx };
if (def.fonts) ctx.fontStore = new FontStore(def.fonts);
if (def.images) ctx.imageStore = new ImageStore(def.images);
if (def.dev?.guides != null) ctx.guides = def.dev.guides;
const pages = await layoutPages(def, ctx);
return await renderDocument(def, pages);
}
}
6 changes: 6 additions & 0 deletions src/api/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export type DocumentDefinition = {
/**
* The fonts to use in the document. There is no default. Each font that is used in the document
* must be registered. Not needed for documents that contain only graphics.
*
* @deprecated Register fonts with `PdfMaker` instead.
*/
fonts?: FontsDefinition;

Expand Down Expand Up @@ -153,11 +155,15 @@ export type CustomInfoProps = {

/**
* An object that defines the fonts to use in the document.
*
* @deprecated Register fonts with `PdfMaker` instead.
*/
export type FontsDefinition = { [name: string]: FontDefinition[] };

/**
* The definition of a single font.
*
* @deprecated Register fonts with `PdfMaker` instead.
*/
export type FontDefinition = {
/**
Expand Down
3 changes: 3 additions & 0 deletions src/api/make-pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import type { DocumentDefinition } from './document.ts';
*
* @param definition The definition of the document to generate.
* @returns The generated PDF document.
*
* @deprecated Create an instance of `PdfMaker` and call `makePdf` on
* that instance.
*/
export async function makePdf(definition: DocumentDefinition): Promise<Uint8Array> {
const def = readAs(definition, 'definition', readDocumentDefinition);
Expand Down
26 changes: 23 additions & 3 deletions src/font-store.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import fontkit from '@pdf-lib/fontkit';
import { toUint8Array } from 'pdf-lib';

import type { FontWeight } from './api/text.ts';
import type { FontConfig } from './api/PdfMaker.ts';
import type { FontStyle, FontWeight } from './api/text.ts';
import type { Font, FontDef, FontSelector } from './fonts.ts';
import { weightToNumber } from './fonts.ts';
import { pickDefined } from './types.ts';

export class FontStore {
readonly #fontDefs: FontDef[];
readonly #fontCache: Record<string, Promise<Font>> = {};
#fontCache: Record<string, Promise<Font>> = {};

constructor(fontDefs: FontDef[]) {
this.#fontDefs = fontDefs;
}

registerFont(data: Uint8Array, config?: FontConfig): void {
const fkFont = fontkit.create(data, config?.name);
const family = config?.name ?? fkFont.familyName ?? 'Unknown';
const style = config?.style ?? extractStyle(fkFont);
const weight = weightToNumber(config?.weight ?? extractWeight(fkFont));
this.#fontDefs.push({ family, style, weight, data, fkFont });
this.#fontCache = {};
}

async selectFont(selector: FontSelector): Promise<Font> {
const cacheKey = [
selector.fontFamily ?? 'any',
Expand All @@ -32,7 +42,7 @@ export class FontStore {
_loadFont(selector: FontSelector): Promise<Font> {
const selectedFont = selectFont(this.#fontDefs, selector);
const data = toUint8Array(selectedFont.data);
const fkFont = fontkit.create(data);
const fkFont = selectedFont.fkFont ?? fontkit.create(data);
return Promise.resolve(
pickDefined({
name: fkFont.fullName ?? fkFont.postscriptName ?? selectedFont.family,
Expand Down Expand Up @@ -109,3 +119,13 @@ function selectFontForWeight(fonts: FontDef[], weight: FontWeight): FontDef | un
}
throw new Error(`Could not find font for weight ${weight}`);
}

function extractStyle(font: fontkit.Font): FontStyle {
if (font.italicAngle === 0) return 'normal';
if ((font.fullName ?? font.postscriptName)?.toLowerCase().includes('oblique')) return 'oblique';
return 'italic';
}

function extractWeight(font: fontkit.Font): number {
return (font['OS/2'] as any)?.usWeightClass ?? 400;
}
1 change: 1 addition & 0 deletions src/fonts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type FontDef = {
style: FontStyle;
weight: number;
data: string | Uint8Array | ArrayBuffer;
fkFont?: fontkit.Font;
};

export type Font = {
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as document from './api/document.ts';
import * as graphics from './api/graphics.ts';
import * as layout from './api/layout.ts';
import * as makePdf from './api/make-pdf.ts';
import * as pdfMaker from './api/PdfMaker.ts';
import * as sizes from './api/sizes.ts';
import * as text from './api/text.ts';

Expand All @@ -12,6 +13,7 @@ export const pdf = {
...graphics,
...layout,
...makePdf,
...pdfMaker,
...sizes,
...text,
};
Expand All @@ -21,5 +23,6 @@ export * from './api/document.ts';
export * from './api/graphics.ts';
export * from './api/layout.ts';
export * from './api/make-pdf.ts';
export * from './api/PdfMaker.ts';
export * from './api/sizes.ts';
export * from './api/text.ts';

0 comments on commit f9e7be5

Please sign in to comment.