Skip to content

Commit 22a526b

Browse files
ralfstxclaude
andcommitted
💥 Replace pdf-lib with pdf-core, bump to 0.6.0
This change replaces `pdf-lib` and `fontkit` with `@ralfstx/pdf-core` as the underlying PDF generation library. This results in faster PDF generation, smaller bundle size, and opens up new possibilities such as font shaping. Loading font and image data from base64-encoded strings was a feature of `pdf-lib`. Reading encoded data should better be done outside of the library, so we drop this feature and require `Uint8Array`. With `pdf-core`, we can directly use the OS/2 typographic metrics for font metrics instead of relying on the hhea table values. This is the correct approach but it results in tighter line spacing for fonts whose hhea values include extra spacing that was effectively double-counted with the `lineHeight` multiplier. Links and anchors are supported by `pdf-core`, so the custom rendering code is removed. Parsing JPEG and PNG data is also handled by `pdf-core`, so the related code is removed. Rendering functions are simplified to use the `ContentStream` API directly. The version is bumped to 0.6.0 to account for the breaking changes in the API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8a61615 commit 22a526b

49 files changed

Lines changed: 935 additions & 1465 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
# Changelog
22

3-
## [0.5.7] - Unreleased
3+
## [0.6.0] - Unreleased
4+
5+
### Changed
6+
7+
- Replaced `pdf-lib` with `@ralfstx/pdf-core` as the underlying PDF
8+
generation library. This results in faster PDF generation and a
9+
smaller bundle size. It also opens up new possibilities for new
10+
features such as font shaping.
11+
12+
### Breaking
13+
14+
- Font and image data must now be provided as `Uint8Array`.
15+
Base64-encoded strings and `ArrayBuffer`s are no longer accepted.
16+
17+
- Text height is now based on the OS/2 typographic metrics
18+
(`sTypoAscender` / `sTypoDescender`) instead of the hhea table values.
19+
This results in tighter line spacing for fonts whose hhea values
20+
include extra spacing that was effectively double-counted with the
21+
`lineHeight` multiplier.
422

523
## [0.5.6] - 2025-01-19
624

‎README.md‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -668,10 +668,9 @@ const document = {
668668
669669
## Thanks
670670
671-
This project is inspired by [pdfmake] and builds on [pdf-lib] and
672-
[fontkit]. It would not exist without the great work and the profound
673-
knowledge contributed by the authors of those projects.
671+
This project is inspired by [pdfmake] and [pdf-lib]. It would not exist
672+
without the great work and the profound knowledge contributed by the
673+
authors of those projects.
674674
675675
[pdfmake]: https://github.com/bpampuch/pdfmake
676676
[pdf-lib]: https://github.com/Hopding/pdf-lib
677-
[fontkit]: https://github.com/Hopding/fontkit

‎eslint.config.js‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,9 @@ export default tseslint.config(
6666
'@typescript-eslint/parameter-properties': 'error',
6767
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
6868

69-
// TODO: revisit when we got rid of pdf-lib internals
69+
// TODO: remove when we got rid of any in the code base
7070
'@typescript-eslint/no-unsafe-assignment': ['off'],
71-
'@typescript-eslint/no-unsafe-call': ['off'],
7271
'@typescript-eslint/no-unsafe-member-access': ['off'],
73-
'@typescript-eslint/no-unsafe-return': ['off'],
7472

7573
'simple-import-sort/imports': 'error',
7674
'import/extensions': ['error', 'ignorePackages', { js: 'never', ts: 'always' }],

‎package-lock.json‎

Lines changed: 20 additions & 47 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pdfmkr",
3-
"version": "0.5.7",
3+
"version": "0.6.0",
44
"description": "Generate PDF documents from JavaScript objects",
55
"license": "MIT",
66
"repository": {
@@ -25,16 +25,15 @@
2525
"npm": ">=10"
2626
},
2727
"scripts": {
28-
"build": "rm -rf build/ dist/ && tsc && esbuild src/index.ts --bundle --sourcemap --platform=browser --target=es2022 --outdir=dist --format=esm --external:pdf-lib --external:@pdf-lib/fontkit && cp -a build/index.d.ts build/api/ dist/",
28+
"build": "rm -rf build/ dist/ && tsc && esbuild src/index.ts --bundle --sourcemap --platform=browser --target=es2022 --outdir=dist --format=esm --external:@ralfstx/pdf-core && cp -a build/index.d.ts build/api/ dist/",
2929
"lint": "eslint . --max-warnings=0 && prettier --check .",
3030
"test": "vitest run test",
3131
"format": "prettier -w .",
3232
"fix": "eslint . --fix && prettier -w .",
3333
"examples": "./examples/run-all-examples.sh"
3434
},
3535
"dependencies": {
36-
"@pdf-lib/fontkit": "^1.1.1",
37-
"pdf-lib": "^1.17.1"
36+
"@ralfstx/pdf-core": "^0.1.0"
3837
},
3938
"devDependencies": {
4039
"@types/node": "^25.0.3",

‎src/api/PdfMaker.test.ts‎

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { readFile } from 'node:fs/promises';
22
import { join } from 'node:path';
33

4-
import { beforeEach, describe, expect, it, vi } from 'vitest';
4+
import { beforeEach, describe, expect, it } from 'vitest';
55

6-
import { image, text } from './layout.ts';
76
import { PdfMaker } from './PdfMaker.ts';
87

98
describe('makePdf', () => {
@@ -31,25 +30,4 @@ describe('makePdf', () => {
3130
const string = Buffer.from(pdf.buffer).toString();
3231
expect(string).toMatch(/[^\n]\n$/);
3332
});
34-
35-
it('includes a trailer ID in the document', async () => {
36-
const pdf = await pdfMaker.makePdf({ content: [{}] });
37-
38-
const string = Buffer.from(pdf.buffer).toString();
39-
expect(string).toMatch(/\/ID \[ <[0-9A-F]{64}> <[0-9A-F]{64}> \]/);
40-
});
41-
42-
it('creates consistent results across runs', async () => {
43-
// ensure same timestamps in generated PDF
44-
vi.useFakeTimers();
45-
// include fonts and images to ensure they can be reused
46-
const content = [text('Test'), image('file:/torus.png')];
47-
48-
const pdf1 = await pdfMaker.makePdf({ content });
49-
const pdf2 = await pdfMaker.makePdf({ content });
50-
51-
const pdfStr1 = Buffer.from(pdf1.buffer).toString();
52-
const pdfStr2 = Buffer.from(pdf2.buffer).toString();
53-
expect(pdfStr1).toEqual(pdfStr2);
54-
});
5533
});

‎src/api/document.ts‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -238,14 +238,12 @@ export type FontsDefinition = { [name: string]: FontDefinition[] };
238238
*/
239239
export type FontDefinition = {
240240
/**
241-
* The font data, as a Uint8Array, ArrayBuffer, or a base64-encoded
242-
* string.
241+
* The font data as a Uint8Array.
243242
*
244-
* Supports TrueType (`.ttf`), OpenType (`.otf`), WOFF, WOFF2,
245-
* TrueType Collection (`.ttc`), and Datafork TrueType (`.dfont`) font
246-
* files (see https://github.com/Hopding/fontkit).
243+
* Supports TrueType font files (`.ttf`) and OpenType (`.otf`) font
244+
* files with TrueType outlines.
247245
*/
248-
data: string | Uint8Array | ArrayBuffer;
246+
data: Uint8Array;
249247

250248
/**
251249
* Whether this is a bold font.
@@ -272,12 +270,12 @@ export type ImagesDefinition = { [name: string]: ImageDefinition };
272270
*/
273271
export type ImageDefinition = {
274272
/**
275-
* The image data, as a Uint8Array, ArrayBuffer, or a base64-encoded string.
273+
* The image data as a Uint8Array.
276274
* Supported image formats are PNG and JPEG.
277275
*
278276
* @deprecated Use URLs to include images.
279277
*/
280-
data: string | Uint8Array | ArrayBuffer;
278+
data: Uint8Array;
281279
};
282280

283281
/**

‎src/binary-data.test.ts‎

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,28 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { parseBinaryData } from './binary-data.ts';
3+
import { readBinaryData } from './binary-data.ts';
44

5-
const data = Uint8Array.of(1, 183, 0);
6-
7-
describe('parseBinaryData', () => {
5+
describe('readBinaryData', () => {
86
it('returns original Uint8Array', () => {
9-
expect(parseBinaryData(data)).toBe(data);
10-
});
7+
const data = Uint8Array.of(1, 183, 0);
118

12-
it('returns Uint8Array for ArrayBuffer', () => {
13-
expect(parseBinaryData(data.buffer)).toEqual(data);
9+
expect(readBinaryData(data)).toBe(data);
1410
});
1511

16-
it('returns Uint8Array for base64-encoded string', () => {
17-
expect(parseBinaryData('Abc=`')).toEqual(data);
18-
});
12+
it('throws for ArrayBuffer', () => {
13+
const buffer = Uint8Array.of(1, 183, 0).buffer;
1914

20-
it('returns Uint8Array for data URL', () => {
21-
expect(parseBinaryData('data:image/jpeg;base64,Abc=`')).toEqual(data);
15+
expect(() => readBinaryData(buffer)).toThrow(
16+
new TypeError('Expected Uint8Array, got: ArrayBuffer [1, 183, 0]'),
17+
);
2218
});
2319

24-
it('throws for arrays', () => {
25-
expect(() => parseBinaryData([1, 2, 3])).toThrow(
26-
new TypeError('Expected Uint8Array, ArrayBuffer, or base64-encoded string, got: [1, 2, 3]'),
27-
);
20+
it('throws for strings', () => {
21+
expect(() => readBinaryData('AbcA')).toThrow(new TypeError("Expected Uint8Array, got: 'AbcA'"));
2822
});
2923

3024
it('throws for other types', () => {
31-
expect(() => parseBinaryData(23)).toThrow(
32-
new TypeError('Expected Uint8Array, ArrayBuffer, or base64-encoded string, got: 23'),
33-
);
34-
expect(() => parseBinaryData(null)).toThrow(
35-
new TypeError('Expected Uint8Array, ArrayBuffer, or base64-encoded string, got: null'),
36-
);
25+
expect(() => readBinaryData(23)).toThrow(new TypeError('Expected Uint8Array, got: 23'));
26+
expect(() => readBinaryData(null)).toThrow(new TypeError('Expected Uint8Array, got: null'));
3727
});
3828
});

‎src/binary-data.ts‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
import { decodeFromBase64DataUri } from 'pdf-lib';
2-
31
import { typeError } from './types.ts';
42

5-
export function parseBinaryData(input: unknown): Uint8Array {
3+
export function readBinaryData(input: unknown): Uint8Array {
64
if (input instanceof Uint8Array) return input;
7-
if (input instanceof ArrayBuffer) return new Uint8Array(input);
8-
if (typeof input === 'string') return decodeFromBase64DataUri(input);
9-
throw typeError('Uint8Array, ArrayBuffer, or base64-encoded string', input);
5+
throw typeError('Uint8Array', input);
106
}

‎src/colors.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { ContentStream } from '@ralfstx/pdf-core';
2+
3+
export type Color = {
4+
type: 'RGB';
5+
red: number;
6+
green: number;
7+
blue: number;
8+
};
9+
10+
export const rgb = (red: number, green: number, blue: number): Color => {
11+
assertRange(red, 'red', 0, 1);
12+
assertRange(green, 'green', 0, 1);
13+
assertRange(blue, 'blue', 0, 1);
14+
return { type: 'RGB', red, green, blue };
15+
};
16+
17+
export function setFillingColor(cs: ContentStream, color: Color): void {
18+
if (color.type === 'RGB') {
19+
cs.setFillRGB(color.red, color.green, color.blue);
20+
} else throw new Error(`Invalid color: ${JSON.stringify(color)}`);
21+
}
22+
23+
export function setStrokingColor(cs: ContentStream, color: Color): void {
24+
if (color.type === 'RGB') {
25+
cs.setStrokeRGB(color.red, color.green, color.blue);
26+
} else throw new Error(`Invalid color: ${JSON.stringify(color)}`);
27+
}
28+
29+
function assertRange(value: number, valueName: string, min: number, max: number) {
30+
if (typeof value !== 'number' || value < min || value > max) {
31+
throw new Error(`${valueName} must be a number between ${min} and ${max}, got: ${value}`);
32+
}
33+
}

0 commit comments

Comments
 (0)