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
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
# Changelog

## [0.5.7] - Unreleased
## [0.6.0] - Unreleased

### Changed

- Replaced `pdf-lib` with `@ralfstx/pdf-core` as the underlying PDF
generation library. This results in faster PDF generation and a
smaller bundle size. It also opens up new possibilities for new
features such as font shaping.

### Breaking

- Font and image data must now be provided as `Uint8Array`.
Base64-encoded strings and `ArrayBuffer`s are no longer accepted.

- Text height is now based on the OS/2 typographic metrics
(`sTypoAscender` / `sTypoDescender`) instead of the hhea table values.
This results in tighter line spacing for fonts whose hhea values
include extra spacing that was effectively double-counted with the
`lineHeight` multiplier.

## [0.5.6] - 2025-01-19

Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -668,10 +668,9 @@ const document = {

## Thanks

This project is inspired by [pdfmake] and builds on [pdf-lib] and
[fontkit]. It would not exist without the great work and the profound
knowledge contributed by the authors of those projects.
This project is inspired by [pdfmake] and [pdf-lib]. It would not exist
without the great work and the profound knowledge contributed by the
authors of those projects.

[pdfmake]: https://github.com/bpampuch/pdfmake
[pdf-lib]: https://github.com/Hopding/pdf-lib
[fontkit]: https://github.com/Hopding/fontkit
4 changes: 1 addition & 3 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,9 @@ export default tseslint.config(
'@typescript-eslint/parameter-properties': 'error',
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],

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

'simple-import-sort/imports': 'error',
'import/extensions': ['error', 'ignorePackages', { js: 'never', ts: 'always' }],
Expand Down
67 changes: 20 additions & 47 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pdfmkr",
"version": "0.5.7",
"version": "0.6.0",
"description": "Generate PDF documents from JavaScript objects",
"license": "MIT",
"repository": {
Expand All @@ -25,16 +25,15 @@
"npm": ">=10"
},
"scripts": {
"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/",
"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/",
"lint": "eslint . --max-warnings=0 && prettier --check .",
"test": "vitest run test",
"format": "prettier -w .",
"fix": "eslint . --fix && prettier -w .",
"examples": "./examples/run-all-examples.sh"
},
"dependencies": {
"@pdf-lib/fontkit": "^1.1.1",
"pdf-lib": "^1.17.1"
"@ralfstx/pdf-core": "^0.1.0"
},
"devDependencies": {
"@types/node": "^25.0.3",
Expand Down
24 changes: 1 addition & 23 deletions src/api/PdfMaker.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { beforeEach, describe, expect, it } from 'vitest';

import { image, text } from './layout.ts';
import { PdfMaker } from './PdfMaker.ts';

describe('makePdf', () => {
Expand Down Expand Up @@ -31,25 +30,4 @@ describe('makePdf', () => {
const string = Buffer.from(pdf.buffer).toString();
expect(string).toMatch(/[^\n]\n$/);
});

it('includes a trailer ID in the document', async () => {
const pdf = await pdfMaker.makePdf({ content: [{}] });

const string = Buffer.from(pdf.buffer).toString();
expect(string).toMatch(/\/ID \[ <[0-9A-F]{64}> <[0-9A-F]{64}> \]/);
});

it('creates consistent results across runs', async () => {
// ensure same timestamps in generated PDF
vi.useFakeTimers();
// include fonts and images to ensure they can be reused
const content = [text('Test'), image('file:/torus.png')];

const pdf1 = await pdfMaker.makePdf({ content });
const pdf2 = await pdfMaker.makePdf({ content });

const pdfStr1 = Buffer.from(pdf1.buffer).toString();
const pdfStr2 = Buffer.from(pdf2.buffer).toString();
expect(pdfStr1).toEqual(pdfStr2);
});
});
14 changes: 6 additions & 8 deletions src/api/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,14 +238,12 @@ export type FontsDefinition = { [name: string]: FontDefinition[] };
*/
export type FontDefinition = {
/**
* The font data, as a Uint8Array, ArrayBuffer, or a base64-encoded
* string.
* The font data as a Uint8Array.
*
* Supports TrueType (`.ttf`), OpenType (`.otf`), WOFF, WOFF2,
* TrueType Collection (`.ttc`), and Datafork TrueType (`.dfont`) font
* files (see https://github.com/Hopding/fontkit).
* Supports TrueType font files (`.ttf`) and OpenType (`.otf`) font
* files with TrueType outlines.
*/
data: string | Uint8Array | ArrayBuffer;
data: Uint8Array;

/**
* Whether this is a bold font.
Expand All @@ -272,12 +270,12 @@ export type ImagesDefinition = { [name: string]: ImageDefinition };
*/
export type ImageDefinition = {
/**
* The image data, as a Uint8Array, ArrayBuffer, or a base64-encoded string.
* The image data as a Uint8Array.
* Supported image formats are PNG and JPEG.
*
* @deprecated Use URLs to include images.
*/
data: string | Uint8Array | ArrayBuffer;
data: Uint8Array;
};

/**
Expand Down
36 changes: 13 additions & 23 deletions src/binary-data.test.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,28 @@
import { describe, expect, it } from 'vitest';

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

const data = Uint8Array.of(1, 183, 0);

describe('parseBinaryData', () => {
describe('readBinaryData', () => {
it('returns original Uint8Array', () => {
expect(parseBinaryData(data)).toBe(data);
});
const data = Uint8Array.of(1, 183, 0);

it('returns Uint8Array for ArrayBuffer', () => {
expect(parseBinaryData(data.buffer)).toEqual(data);
expect(readBinaryData(data)).toBe(data);
});

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

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

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

it('throws for other types', () => {
expect(() => parseBinaryData(23)).toThrow(
new TypeError('Expected Uint8Array, ArrayBuffer, or base64-encoded string, got: 23'),
);
expect(() => parseBinaryData(null)).toThrow(
new TypeError('Expected Uint8Array, ArrayBuffer, or base64-encoded string, got: null'),
);
expect(() => readBinaryData(23)).toThrow(new TypeError('Expected Uint8Array, got: 23'));
expect(() => readBinaryData(null)).toThrow(new TypeError('Expected Uint8Array, got: null'));
});
});
8 changes: 2 additions & 6 deletions src/binary-data.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { decodeFromBase64DataUri } from 'pdf-lib';

import { typeError } from './types.ts';

export function parseBinaryData(input: unknown): Uint8Array {
export function readBinaryData(input: unknown): Uint8Array {
if (input instanceof Uint8Array) return input;
if (input instanceof ArrayBuffer) return new Uint8Array(input);
if (typeof input === 'string') return decodeFromBase64DataUri(input);
throw typeError('Uint8Array, ArrayBuffer, or base64-encoded string', input);
throw typeError('Uint8Array', input);
}
33 changes: 33 additions & 0 deletions src/colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { ContentStream } from '@ralfstx/pdf-core';

export type Color = {
type: 'RGB';
red: number;
green: number;
blue: number;
};

export const rgb = (red: number, green: number, blue: number): Color => {
assertRange(red, 'red', 0, 1);
assertRange(green, 'green', 0, 1);
assertRange(blue, 'blue', 0, 1);
return { type: 'RGB', red, green, blue };
};

export function setFillingColor(cs: ContentStream, color: Color): void {
if (color.type === 'RGB') {
cs.setFillRGB(color.red, color.green, color.blue);
} else throw new Error(`Invalid color: ${JSON.stringify(color)}`);
}

export function setStrokingColor(cs: ContentStream, color: Color): void {
if (color.type === 'RGB') {
cs.setStrokeRGB(color.red, color.green, color.blue);
} else throw new Error(`Invalid color: ${JSON.stringify(color)}`);
}

function assertRange(value: number, valueName: string, min: number, max: number) {
if (typeof value !== 'number' || value < min || value > max) {
throw new Error(`${valueName} must be a number between ${min} and ${max}, got: ${value}`);
}
}
28 changes: 12 additions & 16 deletions src/font-metrics.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import type { Font } from '@pdf-lib/fontkit';
import type { PDFFont } from '@ralfstx/pdf-core';

export function getTextWidth(text: string, font: Font, fontSize: number): number {
const { glyphs } = font.layout(text);
const scale = 1000 / font.unitsPerEm;
let totalWidth = 0;
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
totalWidth += glyphs[idx].advanceWidth * scale;
}
return (totalWidth * fontSize) / 1000;
export function getTextWidth(text: string, font: PDFFont, fontSize: number): number {
const glyphs = font.shapeText(text, { defaultFeatures: false });
return glyphs.reduce(
(sum, glyph) => sum + (glyph.advance + (glyph.advanceAdjust ?? 0)) * (fontSize / 1000),
0,
);
}

export function getTextHeight(font: Font, fontSize: number): number {
const { ascent, descent, bbox } = font;
const scale = 1000 / font.unitsPerEm;
const yTop = (ascent || bbox.maxY) * scale;
const yBottom = (descent || bbox.minY) * scale;
const height = yTop - yBottom;
return (height / 1000) * fontSize;
export function getTextHeight(font: PDFFont, fontSize: number): number {
const ascent = font.ascent;
const descent = font.descent;
const height = ascent - descent;
return (height * fontSize) / 1000;
}
Loading