diff --git a/CHANGELOG.md b/CHANGELOG.md index 32bb18f..9c788a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. @@ -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 diff --git a/src/api/PdfMaker.ts b/src/api/PdfMaker.ts new file mode 100644 index 0000000..af7f7d7 --- /dev/null +++ b/src/api/PdfMaker.ts @@ -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 { + 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); + } +} diff --git a/src/api/document.ts b/src/api/document.ts index 4d88943..d23cd66 100644 --- a/src/api/document.ts +++ b/src/api/document.ts @@ -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; @@ -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 = { /** diff --git a/src/api/make-pdf.ts b/src/api/make-pdf.ts index 4e25203..88a7433 100644 --- a/src/api/make-pdf.ts +++ b/src/api/make-pdf.ts @@ -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 { const def = readAs(definition, 'definition', readDocumentDefinition); diff --git a/src/font-store.ts b/src/font-store.ts index 66493f7..b669d90 100644 --- a/src/font-store.ts +++ b/src/font-store.ts @@ -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> = {}; + #fontCache: Record> = {}; 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 { const cacheKey = [ selector.fontFamily ?? 'any', @@ -32,7 +42,7 @@ export class FontStore { _loadFont(selector: FontSelector): Promise { 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, @@ -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; +} diff --git a/src/fonts.ts b/src/fonts.ts index 618102b..0c64543 100644 --- a/src/fonts.ts +++ b/src/fonts.ts @@ -15,6 +15,7 @@ export type FontDef = { style: FontStyle; weight: number; data: string | Uint8Array | ArrayBuffer; + fkFont?: fontkit.Font; }; export type Font = { diff --git a/src/index.ts b/src/index.ts index 41d52d8..ff26f6c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; @@ -12,6 +13,7 @@ export const pdf = { ...graphics, ...layout, ...makePdf, + ...pdfMaker, ...sizes, ...text, }; @@ -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';