@pimeo/podpdf
v1.2.9
Published
Ultra-fast, zero-dependency PDF generation for Node.js, Bun & Browser.
Maintainers
Readme
podpdf
A forked package of the amazing Herolab.ID podpdf project. Includes some features not (yet) present in the original library and a more human and easier-to-maintain syntax.
Ultra-fast, zero-dependency PDF generation for Node.js, Bun & Browser
Zero dependencies • TypeScript nativeWhat's differ from original podpdf library
- [x] Fix the
ERR_PACKAGE_PATH_NOT_EXPORTEDerror when compiling via Webpack. - [x] Support table multi-lines row cell. When a row has a fixed width, the text in the cells of the associated columns is automatically wrapped to the next line.
- [x] The project is structured into several files to facilitate maintenance, make it easier to integrate new features and avoid code repetition.
- [x] Get the boundaries (width/height/offsetX/offsetY) of each set of elements in the document.
Feature Comparison
Core Features
| Feature | podpdf | podpdf/plus | podpdf/browser | |---------|:------:|:-----------:|:--------------:| | Text & Styling | ✅ | ✅ | ✅ | | Text Wrap & Alignment | ✅ | ✅ | ✅ | | Shapes (rect, circle, line) | ✅ | ✅ | ✅ | | Tables (with support for multi-lines) | ✅ | ✅ | ✅ | | Images (JPEG) | ✅ | ✅ | ✅ | | Images (PNG) | ❌ | ✅ | ❌ | | Links/URLs | ✅ | ✅ | ✅ | | Multi-page | ✅ | ✅ | ✅ | | Document Metadata | ✅ | ✅ | ✅ | | Custom Fonts (TTF) | ❌ | ✅ | ❌ | | TypeScript Native | ✅ | ✅ | ✅ | | Bounding elements | ✅ | ❌ (incoming) | ✅ |
Environment Support
| Platform | podpdf | podpdf/plus | podpdf/browser | |----------|:------:|:-----------:|:--------------:| | Node.js / Bun | ✅ | ✅ | ❌ | | Browser | Manual | Manual | ✅ Native | | File System (.save) | ✅ | ✅ | ❌ | | Browser Download (.download) | ❌ | ❌ | ✅ |
Choose the right variant:
@pimeo/podpdf- Node.js/Bun for invoices, reports, tables@pimeo/podpdf/plus- Need PNG images or custom fonts@pimeo/podpdf/browser- Browser-native PDF generation
Installation
npm install @pimeo/podpdf
# or
yarn add @pimeo/podpdf
# or
pnpm add @pimeo/podpdf
# or
bun add @pimeo/podpdfPackage Variants
podpdf provides 3 specialized exports for different use cases:
| Import | Use Case | Key Features |
|--------|----------|--------------|
| @pimeo/podpdf | Node.js/Bun | Core features, .save() to file |
| @pimeo/podpdf/plus | Node.js/Bun | PNG images + custom TTF fonts |
| @pimeo/podpdf/browser | Browser | Browser-native, .download() method |
Quick Start
Node.js / Bun
import { pdf } from '@pimeo/podpdf'
await pdf('A4')
.text('Hello World!', 50, 50, { size: 24, weight: 'bold' })
.rect(50, 80, 200, 100, { fill: '#3498db', radius: 10 })
.save('hello.pdf')Browser
import { pdf } from '@pimeo/podpdf/browser'
pdf('A4')
.text('Hello Browser!', 50, 50, { size: 24, weight: 'bold' })
.rect(50, 80, 200, 100, { fill: '#3498db', radius: 10 })
.download('hello.pdf') // Triggers browser downloadFeatures
- Text - Multiple fonts, sizes, colors, alignment, text wrapping
- Shapes - Rectangle, rounded rectangle, circle, line (solid & dashed)
- Tables - Easy table creation with headers, styling, alignment
- Images - JPEG support (PNG via podpdf/plus)
- Links - Clickable URLs with optional underline
- Multi-page - Multiple pages with different sizes
- Metadata - Document title, author, subject, keywords
- Fluent API - Chainable methods for clean code
podpdf/plus (Extended)
For PNG images and custom fonts, use podpdf/plus:
import { pdfPlus } from '@pimeo/podpdf/plus'
// Load custom font
const fontData = await Bun.file('custom-font.ttf').bytes()
// Load PNG image
const pngData = await Bun.file('logo.png').bytes()
await pdfPlus('A4')
.registerFont('custom', fontData)
.text('Custom Font Text', 50, 50, { font: 'custom' })
.imagePng(pngData, 50, 100, { width: 200 }) // async method
.save('output.pdf')API Reference
Create Document
import { pdf, PDF, SIZES } from '@pimeo/podpdf'
// Using helper function
const doc = pdf('A4')
// Available sizes: A3, A4, A5, LETTER
// Or custom: pdf({ width: 600, height: 800 })Text
.text(content, x, y, options?)| Option | Type | Default | Description |
|--------|------|---------|-------------|
| size | number | 12 | Font size |
| color | string | '#000' | Color (hex) |
| weight | string | 'normal' | 'normal', 'bold', 'italic', 'bolditalic' |
| align | string | 'left' | 'left', 'center', 'right' |
| maxWidth | number | - | Auto wrap text |
.text('Title', 50, 50, { size: 24, weight: 'bold', color: '#333' })
.text('Centered', 297, 100, { align: 'center' })
.text('Long text...', 50, 150, { maxWidth: 400 })Shapes
// Rectangle
.rect(x, y, width, height, { fill?, stroke?, lineWidth?, radius? })
// Circle
.circle(cx, cy, radius, { fill?, stroke?, lineWidth? })
// Line
.line(x1, y1, x2, y2, { color?, width?, dash? }).rect(50, 50, 200, 100, { fill: '#e74c3c' })
.rect(50, 50, 200, 100, { fill: '#3498db', radius: 15 })
.circle(150, 200, 50, { fill: '#9b59b6' })
.line(50, 300, 250, 300, { color: '#2ecc71', width: 2 })
.line(50, 320, 250, 320, { dash: [5, 3] })Tables
.table(data, x, y, options)| Option | Type | Default | Description |
|--------|------|---------|-------------|
| columns | array | required | Column definitions |
| headerBg | string | '#F0F0F0' | Header background |
| headerColor | string | '#000' | Header text color |
| borderColor | string | '#CCC' | Border color |
| fontSize | number | 10 | Font size |
| padding | number | 8 | Cell padding |
.table(
[
['John', '25', 'Admin'],
['Jane', '30', 'User'],
],
50, 100,
{
columns: [
{ header: 'Name', width: 100 },
{ header: 'Age', width: 60, align: 'center' },
{ header: 'Role', width: 80 },
],
headerBg: '#2c3e50',
headerColor: '#fff'
}
)Images
const imageData = await Bun.file('photo.jpg').bytes()
// or: Buffer.from(fs.readFileSync('photo.jpg'))
.image(imageData, x, y, { width?, height? })Links
.link('Click here', 'https://example.com', x, y, { underline?, color? })Pages
.page() // Add page with default size
.page('A5') // Different size
.page({ width: 500, height: 700 }) // CustomMetadata
.metadata({ title?, author?, subject?, keywords?, creator? })pdf('A4')
.metadata({
title: 'Invoice #123',
author: 'Company Name',
subject: 'Monthly Invoice',
keywords: 'invoice, billing'
})
.text('...', 50, 50)
.save('invoice.pdf')Bounding Elements
.getBoundingElements()pdf('A4')
.text('...', 50, 50, {id: 'my-text-1'})
.getBoundingElements()Output
// Save to file
await doc.save('output.pdf')
// Get as Uint8Array
const bytes = doc.build()Examples
Complete Invoice
import { pdf } from '@pimeo/podpdf'
const invoice = {
number: 'INV-2024-001',
date: '2024-12-26',
dueDate: '2025-01-26',
company: {
name: 'Tech Solutions Inc.',
address: '123 Innovation Street',
city: 'San Francisco, CA 94102',
email: '[email protected]'
},
client: {
name: 'Acme Corporation',
address: '456 Business Avenue',
city: 'New York, NY 10001',
email: '[email protected]'
},
items: [
{ desc: 'Website Development', qty: 1, rate: 5000, amount: 5000 },
{ desc: 'Mobile App (iOS)', qty: 1, rate: 8000, amount: 8000 },
{ desc: 'UI/UX Design', qty: 40, rate: 75, amount: 3000 },
{ desc: 'API Integration', qty: 20, rate: 100, amount: 2000 },
{ desc: 'Quality Assurance', qty: 15, rate: 60, amount: 900 },
]
}
const subtotal = invoice.items.reduce((sum, i) => sum + i.amount, 0)
const tax = subtotal * 0.1
const total = subtotal + tax
const fmt = (n: number) => '$' + n.toLocaleString('en-US', { minimumFractionDigits: 2 })
await pdf('A4')
// Header
.rect(0, 0, 595, 100, { fill: '#1a1a2e' })
.text(invoice.company.name.toUpperCase(), 50, 40, { size: 20, color: '#fff', weight: 'bold' })
.text('INVOICE', 545, 35, { size: 28, color: '#4a9eff', weight: 'bold', align: 'right' })
.text(invoice.number, 545, 65, { size: 12, color: '#888', align: 'right' })
// From
.text('From:', 50, 130, { size: 10, color: '#888' })
.text(invoice.company.name, 50, 145, { size: 12, weight: 'bold' })
.text(invoice.company.address, 50, 160, { size: 10 })
.text(invoice.company.city, 50, 175, { size: 10 })
.text(invoice.company.email, 50, 190, { size: 10, color: '#4a9eff' })
// Bill To
.text('Bill To:', 320, 130, { size: 10, color: '#888' })
.text(invoice.client.name, 320, 145, { size: 12, weight: 'bold' })
.text(invoice.client.address, 320, 160, { size: 10 })
.text(invoice.client.city, 320, 175, { size: 10 })
.text(invoice.client.email, 320, 190, { size: 10, color: '#4a9eff' })
// Date boxes
.rect(50, 220, 160, 45, { fill: '#f8f9fa', radius: 5 })
.text('Invoice Date', 60, 235, { size: 9, color: '#888' })
.text(invoice.date, 60, 252, { size: 12, weight: 'bold' })
.rect(230, 220, 160, 45, { fill: '#f8f9fa', radius: 5 })
.text('Due Date', 240, 235, { size: 9, color: '#888' })
.text(invoice.dueDate, 240, 252, { size: 12, weight: 'bold' })
// Items Table
.table(
invoice.items.map(i => [i.desc, i.qty.toString(), fmt(i.rate), fmt(i.amount)]),
50, 290,
{
columns: [
{ header: 'Description', width: 220 },
{ header: 'Qty', width: 60, align: 'center' },
{ header: 'Rate', width: 90, align: 'right' },
{ header: 'Amount', width: 95, align: 'right' },
],
headerBg: '#1a1a2e',
headerColor: '#fff',
borderColor: '#e0e0e0',
fontSize: 10,
padding: 10
}
)
// Summary Box
.rect(330, 480, 185, 100, { stroke: '#e0e0e0', radius: 5 })
.text('Subtotal:', 345, 500, { size: 11 })
.text(fmt(subtotal), 500, 500, { size: 11, align: 'right' })
.text('Tax (10%):', 345, 520, { size: 11 })
.text(fmt(tax), 500, 520, { size: 11, align: 'right' })
.line(345, 540, 500, 540, { color: '#e0e0e0' })
.rect(330, 548, 185, 30, { fill: '#1a1a2e', radius: 5 })
.text('Total:', 345, 567, { size: 12, weight: 'bold', color: '#fff' })
.text(fmt(total), 500, 567, { size: 12, weight: 'bold', color: '#fff', align: 'right' })
// Payment Info
.rect(50, 610, 230, 90, { fill: '#f0f7ff', radius: 5 })
.text('Payment Information', 65, 630, { size: 11, weight: 'bold', color: '#1a1a2e' })
.text('Bank: First National Bank', 65, 650, { size: 9 })
.text('Account: 1234-5678-9012', 65, 665, { size: 9 })
.text('SWIFT: FNBKUS12', 65, 680, { size: 9 })
// Terms
.rect(300, 610, 215, 90, { fill: '#fff9e6', radius: 5 })
.text('Terms & Conditions', 315, 630, { size: 11, weight: 'bold', color: '#b8860b' })
.text('Payment due within 30 days', 315, 650, { size: 9 })
.text('Late fee: 1.5% per month', 315, 665, { size: 9 })
.text('Make checks payable to:', 315, 680, { size: 9 })
.text('Tech Solutions Inc.', 315, 695, { size: 9, weight: 'bold' })
// Footer
.line(50, 730, 545, 730, { color: '#eee' })
.text('Thank you for your business!', 297, 750, { size: 11, color: '#666', align: 'center' })
.text('Questions? Email [email protected]', 297, 768, { size: 9, color: '#999', align: 'center' })
.save('invoice.pdf')Report with Chart
import { pdf } from '@pimeo/podpdf'
const data = [120, 150, 180, 140, 200]
const max = Math.max(...data)
const doc = pdf('A4')
.text('Sales Report', 50, 50, { size: 24, weight: 'bold' })
// Simple bar chart
for (let i = 0; i < data.length; i++) {
const height = (data[i] / max) * 100
doc.rect(80 + i * 60, 200 - height, 40, height, { fill: '#3498db', radius: 3 })
}
await doc.save('report.pdf')Page Sizes
import { SIZES } from '@pimeo/podpdf'
SIZES.A3 // { width: 842, height: 1191 }
SIZES.A4 // { width: 595, height: 842 }
SIZES.A5 // { width: 420, height: 595 }
SIZES.LETTER // { width: 612, height: 792 }TypeScript
Full TypeScript support with exported types:
import type {
Color, // string | [r, g, b]
Align, // 'left' | 'center' | 'right'
Weight, // 'normal' | 'bold' | 'italic' | 'bolditalic'
Size, // { width, height }
PDFMetadata, // { title?, author?, subject?, keywords?, creator? }
TextOpts,
RectOpts,
LineOpts,
CircleOpts,
ImageOpts,
LinkOpts,
TableCol,
TableOpts
} from '@pimeo/podpdf'Benchmark
Tested with 1000 document generations:
| Test | podpdf | jsPDF | |------|--------|-------| | Simple text | 0.033ms | 0.271ms | | Styled text | 0.044ms | 0.260ms | | Shapes | 0.024ms | 0.254ms | | Multi-page | 0.083ms | 0.251ms | | Complex doc | 0.051ms | 0.260ms |
Result: podpdf is 5.5x faster on average
Browser Support
podpdf/browser (Recommended for Browser)
For browser environments, use the dedicated podpdf/browser module with built-in download support:
import { pdf } from '@pimeo/podpdf/browser'
// Create and download PDF directly in browser
pdf('A4')
.text('Hello from Browser!', 50, 50, { size: 24, weight: 'bold' })
.rect(50, 80, 200, 100, { fill: '#3498db', radius: 10 })
.download('document.pdf') // Triggers browser download
// Or get as Blob for custom handling
const blob = doc.toBlob()
// Or get data URL for iframe preview
const dataURL = doc.toDataURL()
const iframe = document.createElement('iframe')
iframe.src = dataURLKey differences in podpdf/browser:
- ✅ No Node.js dependencies (fully browser-compatible)
- ✅
download(filename)- Directly triggers browser download - ✅
toBlob()- Returns Blob object - ✅
toDataURL()- Returns object URL for preview - ❌ No
save()method (usedownload()instead) - ✅ Same API for text, shapes, tables, images, etc.
Using Core podpdf in Browser
The main podpdf module can also work in browsers using the build() method:
import { pdf } from '@pimeo/podpdf'
const bytes = doc.build()
const blob = new Blob([bytes], { type: 'application/pdf' })
const url = URL.createObjectURL(blob)
// Trigger download
const a = document.createElement('a')
a.href = url
a.download = 'document.pdf'
a.click()License
MIT
Contributing
Contributions are welcome! Please open an issue or submit a PR.
