Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,20 @@
smaller bundle size. It also opens up new possibilities for new
features such as font shaping.

### Breaking
### Removed

- Support for font and image data as base64-encoded strings and
`ArrayBuffer`. Data must now be provided as `Uint8Array`.

- Font and image data must now be provided as `Uint8Array`.
Base64-encoded strings and `ArrayBuffer`s are no longer accepted.
- The `images` property in the document definition. Images should be
referred to by URL instead. The `ImagesDefinition` and
`ImageDefinition` types have been removed.

- The `fonts` property in the document definition. Fonts must now be
registered with `PdfMaker.registerFont()`. The `FontsDefinition` and
`FontDefinition` types have been removed.

### Breaking

- Text height is now based on the OS/2 typographic metrics
(`sTypoAscender` / `sTypoDescender`) instead of the hhea table values.
Expand Down
30 changes: 10 additions & 20 deletions src/api/PdfMaker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FontStore } from '../font-store.ts';
import { ImageStore } from '../image-store.ts';
import { createImageLoader } from '../image-loader.ts';
import { layoutPages } from '../layout/layout.ts';
import type { MakerCtx } from '../maker-ctx.ts';
import { readDocumentDefinition } from '../read-document.ts';
Expand All @@ -18,12 +18,11 @@ export type FontConfig = {
* Generates PDF documents.
*/
export class PdfMaker {
#ctx: MakerCtx;
#fontStore: FontStore;
#resourceRoot?: string;

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

/**
Expand All @@ -35,7 +34,7 @@ export class PdfMaker {
* the meta data cannot be extracted from the font.
*/
registerFont(data: Uint8Array, config?: FontConfig): void {
this.#ctx.fontStore.registerFont(data, config);
this.#fontStore.registerFont(data, config);
}

/**
Expand All @@ -45,7 +44,7 @@ export class PdfMaker {
* @param root The root directory to read resources from.
*/
setResourceRoot(root: string): void {
this.#ctx.imageStore.setResourceRoot(root);
this.#resourceRoot = root;
}

/**
Expand All @@ -56,19 +55,10 @@ export class PdfMaker {
*/
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);
console.warn(
'Registering fonts via document definition is deprecated. Use PdfMaker.registerFont() instead.',
);
}
if (def.images) {
ctx.imageStore = new ImageStore(def.images);
console.warn(
'Registering images via document definition is deprecated. Use URLs to include images instead.',
);
}
const ctx: MakerCtx = {
fontStore: this.#fontStore,
imageLoader: createImageLoader(this.#resourceRoot),
};
if (def.dev?.guides != null) ctx.guides = def.dev.guides;
const pages = await layoutPages(def, ctx);
return await renderDocument(def, pages);
Expand Down
71 changes: 0 additions & 71 deletions src/api/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,23 +51,6 @@ export type DocumentDefinition = {
*/
margin?: Length | BoxLengths | ((info: PageInfo) => Length | BoxLengths);

/**
* 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;

/**
* Pre-defined image data. These images can be used by their name in
* the document. This is only needed if images cannot be loaded
* directly from the file system.
*
* @deprecated Use URLs to include images.
*/
images?: ImagesDefinition;

/**
* Metadata to include in the PDF's *document information dictionary*.
*/
Expand Down Expand Up @@ -224,60 +207,6 @@ export type CustomInfoAttrs = CustomInfoProps;
*/
export type CustomInfoProps = Record<`XX${string}`, string>;

/**
* 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 = {
/**
* The font data as a Uint8Array.
*
* Supports TrueType font files (`.ttf`) and OpenType (`.otf`) font
* files with TrueType outlines.
*/
data: Uint8Array;

/**
* Whether this is a bold font.
*/
bold?: boolean;

/**
* Whether this is an italic font.
*/
italic?: boolean;
};

/**
* Pre-defined image data. These images can be used by their name in the
* document. This is only needed if images cannot be loaded directly
* from the file system.
*
* @deprecated Use URLs to include images.
*/
export type ImagesDefinition = { [name: string]: ImageDefinition };

/**
* The definition of a single image.
*/
export type ImageDefinition = {
/**
* The image data as a Uint8Array.
* Supported image formats are PNG and JPEG.
*
* @deprecated Use URLs to include images.
*/
data: Uint8Array;
};

/**
* Information about the current page, provided to functions that create
* page-specific headers, footers, and margins.
Expand Down
101 changes: 50 additions & 51 deletions src/font-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type { PDFEmbeddedFont } from '@ralfstx/pdf-core';
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';

import { FontStore } from './font-store.ts';
import type { FontDef } from './fonts.ts';
import { mkData } from './test/test-utils.ts';

vi.mock('@ralfstx/pdf-core', async (importOriginal) => {
Expand Down Expand Up @@ -39,14 +38,6 @@ vi.mock('@ralfstx/pdf-core', async (importOriginal) => {
});

describe('FontStore', () => {
let normalFont: FontDef;
let italicFont: FontDef;
let obliqueFont: FontDef;
let boldFont: FontDef;
let italicBoldFont: FontDef;
let obliqueBoldFont: FontDef;
let otherFont: FontDef;

describe('registerFont', () => {
let robotoRegular: Uint8Array;
let robotoLightItalic: Uint8Array;
Expand All @@ -68,8 +59,8 @@ describe('FontStore', () => {
const selected1 = await store.selectFont({ fontFamily: 'Roboto' });
const selected2 = await store.selectFont({ fontFamily: 'Roboto Light', fontStyle: 'italic' });

expect(selected1.pdfFont.fontName).toBe('Roboto');
expect(selected2.pdfFont.fontName).toBe('Roboto Light Italic');
expect(selected1.fontName).toBe('Roboto');
expect(selected2.fontName).toBe('Roboto Light Italic');
});

it('registers font with custom config', async () => {
Expand All @@ -84,32 +75,16 @@ describe('FontStore', () => {
const selected1 = await store.selectFont({ fontFamily: 'Custom Name' });
const selected2 = await store.selectFont({ fontFamily: 'Custom Name', fontWeight: 'bold' });

expect(selected1.pdfFont.fontName).toBe('Roboto Light Italic');
expect(selected2.pdfFont.fontName).toBe('Roboto');
expect(selected1.fontName).toBe('Roboto Light Italic');
expect(selected2.fontName).toBe('Roboto');
});
});

describe('selectFont', () => {
let store: FontStore;

beforeEach(() => {
normalFont = fakeFontDef('Test');
italicFont = fakeFontDef('Test', { style: 'italic' });
obliqueFont = fakeFontDef('Test', { style: 'oblique' });
boldFont = fakeFontDef('Test', { weight: 700 });
italicBoldFont = fakeFontDef('Test', { style: 'italic', weight: 700 });
obliqueBoldFont = fakeFontDef('Test', { style: 'oblique', weight: 700 });
otherFont = fakeFontDef('Other');

store = new FontStore([
normalFont,
italicFont,
obliqueFont,
boldFont,
italicBoldFont,
obliqueBoldFont,
otherFont,
]);
store = createTestStore();
});

afterEach(() => {
Expand Down Expand Up @@ -137,7 +112,9 @@ describe('FontStore', () => {
});

it('rejects when no matching font style can be found', async () => {
store = new FontStore([normalFont, boldFont]);
const store = new FontStore();
registerFakeFont(store, 'Test');
registerFakeFont(store, 'Test', { weight: 700 });

await expect(store.selectFont({ fontFamily: 'Test', fontStyle: 'italic' })).rejects.toThrow(
new Error("Could not load font for 'Test', style=italic, weight=normal", {
Expand All @@ -154,56 +131,62 @@ describe('FontStore', () => {
const font3 = await store.selectFont({ fontFamily, fontStyle: 'italic' });
const font4 = await store.selectFont({ fontFamily, fontStyle: 'italic', fontWeight: 'bold' });

expect(font1.pdfFont.fontName).toBe('MockFont:Test:normal:400');
expect(font2.pdfFont.fontName).toBe('MockFont:Test:normal:700');
expect(font3.pdfFont.fontName).toBe('MockFont:Test:italic:400');
expect(font4.pdfFont.fontName).toBe('MockFont:Test:italic:700');
expect(font1.fontName).toBe('MockFont:Test:normal:400');
expect(font2.fontName).toBe('MockFont:Test:normal:700');
expect(font3.fontName).toBe('MockFont:Test:italic:400');
expect(font4.fontName).toBe('MockFont:Test:italic:700');
});

it('selects first matching font if no family specified', async () => {
const font1 = await store.selectFont({});
expect(font1.pdfFont.fontName).toBe('MockFont:Test:normal:400');
expect(font1.fontName).toBe('MockFont:Test:normal:400');

const font2 = await store.selectFont({ fontWeight: 'bold' });
expect(font2.pdfFont.fontName).toBe('MockFont:Test:normal:700');
expect(font2.fontName).toBe('MockFont:Test:normal:700');

const font3 = await store.selectFont({ fontStyle: 'italic' });
expect(font3.pdfFont.fontName).toBe('MockFont:Test:italic:400');
expect(font3.fontName).toBe('MockFont:Test:italic:400');

const font4 = await store.selectFont({ fontStyle: 'italic', fontWeight: 'bold' });
expect(font4.pdfFont.fontName).toBe('MockFont:Test:italic:700');
expect(font4.fontName).toBe('MockFont:Test:italic:700');
});

it('selects font with matching font family', async () => {
await expect(store.selectFont({ fontFamily: 'Other' })).resolves.toEqual(
expect.objectContaining({ name: 'MockFont:Other:normal:400' }),
expect.objectContaining({ fontName: 'MockFont:Other:normal:400' }),
);
});

it('falls back to oblique when no italic font can be found', async () => {
store = new FontStore([normalFont, obliqueFont, boldFont, obliqueBoldFont]);
const store = new FontStore();
registerFakeFont(store, 'Test');
registerFakeFont(store, 'Test', { style: 'oblique' });
registerFakeFont(store, 'Test', { weight: 700 });
registerFakeFont(store, 'Test', { style: 'oblique', weight: 700 });

await expect(store.selectFont({ fontFamily: 'Test', fontStyle: 'italic' })).resolves.toEqual(
expect.objectContaining({ name: 'MockFont:Test:oblique:400' }),
expect.objectContaining({ fontName: 'MockFont:Test:oblique:400' }),
);
});

it('falls back to italic when no oblique font can be found', async () => {
store = new FontStore([normalFont, italicFont, boldFont, italicBoldFont]);
const store = new FontStore();
registerFakeFont(store, 'Test');
registerFakeFont(store, 'Test', { style: 'italic' });
registerFakeFont(store, 'Test', { weight: 700 });
registerFakeFont(store, 'Test', { style: 'italic', weight: 700 });

const font = await store.selectFont({ fontFamily: 'Test', fontStyle: 'italic' });

expect(font.pdfFont).toEqual(
expect.objectContaining({ fontName: 'MockFont:Test:italic:400' }),
);
expect(font).toEqual(expect.objectContaining({ fontName: 'MockFont:Test:italic:400' }));
});

it('falls back when no matching font weight can be found', async () => {
await expect(store.selectFont({ fontFamily: 'Other', fontWeight: 'bold' })).resolves.toEqual(
expect.objectContaining({ name: 'MockFont:Other:normal:400' }),
expect.objectContaining({ fontName: 'MockFont:Other:normal:400' }),
);
await expect(store.selectFont({ fontFamily: 'Other', fontWeight: 200 })).resolves.toEqual(
expect.objectContaining({ name: 'MockFont:Other:normal:400' }),
expect.objectContaining({ fontName: 'MockFont:Other:normal:400' }),
);
});

Expand All @@ -228,9 +211,25 @@ describe('FontStore', () => {
});
});

function fakeFontDef(family: string, options?: Partial<FontDef>): FontDef {
function registerFakeFont(
store: FontStore,
family: string,
options?: { style?: string; weight?: number },
) {
const style = options?.style ?? 'normal';
const weight = options?.weight ?? 400;
const data = options?.data ?? mkData([family, style, weight].join(':'));
return { family, style, weight, data };
const data = mkData([family, style, weight].join(':'));
store.registerFont(data, { family, style: style as 'normal', weight });
}

function createTestStore(): FontStore {
const store = new FontStore();
registerFakeFont(store, 'Test');
registerFakeFont(store, 'Test', { style: 'italic' });
registerFakeFont(store, 'Test', { style: 'oblique' });
registerFakeFont(store, 'Test', { weight: 700 });
registerFakeFont(store, 'Test', { style: 'italic', weight: 700 });
registerFakeFont(store, 'Test', { style: 'oblique', weight: 700 });
registerFakeFont(store, 'Other');
return store;
}
Loading