npm package: @pimeo/podpdf
Description: Ultra-fast, zero-dependency PDF generation for Node.js, Bun & Browser.
Latest version: 1.2.18

# podpdf

A forked package of the amazing [Herolab.ID podpdf](https://github.com/herolabid/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**

[![npm version](https://img.shields.io/npm/v/@pimeo/podpdf.svg)](https://www.npmjs.com/package/@pimeo/podpdf)

```
Zero dependencies  •  TypeScript native
```

## What's differ from original podpdf library
- [x] Fix the `ERR_PACKAGE_PATH_NOT_EXPORTED` error 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

```bash
npm install @pimeo/podpdf
# or
yarn add @pimeo/podpdf
# or
pnpm add @pimeo/podpdf
# or
bun add @pimeo/podpdf
```

## Package 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

```typescript
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

```typescript
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 download
```

## Features

- **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`:

```typescript
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

```typescript
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

```typescript
.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 |

```typescript
.text('Title', 50, 50, { size: 24, weight: 'bold', color: '#333' })
.text('Centered', 297, 100, { align: 'center' })
.text('Long text...', 50, 150, { maxWidth: 400 })
```

### Shapes

```typescript
// 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? })
```

```typescript
.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

```typescript
.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 |

```typescript
.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

```typescript
const imageData = await Bun.file('photo.jpg').bytes()
// or: Buffer.from(fs.readFileSync('photo.jpg'))

.image(imageData, x, y, { width?, height? })
```

### Links

```typescript
.link('Click here', 'https://example.com', x, y, { underline?, color? })
```

### Pages

```typescript
.page()           // Add page with default size
.page('A5')       // Different size
.page({ width: 500, height: 700 })  // Custom
```

### Metadata

```typescript
.metadata({ title?, author?, subject?, keywords?, creator? })
```

```typescript
pdf('A4')
  .metadata({
    title: 'Invoice #123',
    author: 'Company Name',
    subject: 'Monthly Invoice',
    keywords: 'invoice, billing'
  })
  .text('...', 50, 50)
  .save('invoice.pdf')
```

### Bounding Elements

```typescript
.getBoundingElements()
```

```typescript
pdf('A4')
  .text('...', 50, 50, {id: 'my-text-1'})
  .getBoundingElements()
```

### Output

```typescript
// Save to file
await doc.save('output.pdf')

// Get as Uint8Array
const bytes = doc.build()
```

## Examples

### Complete Invoice

```typescript
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: 'billing@techsolutions.com'
  },
  client: {
    name: 'Acme Corporation',
    address: '456 Business Avenue',
    city: 'New York, NY 10001',
    email: 'accounts@acme.com'
  },
  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 billing@techsolutions.com', 297, 768, { size: 9, color: '#999', align: 'center' })

  .save('invoice.pdf')
```

### Report with Chart

```typescript
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

```typescript
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:

```typescript
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:

```typescript
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 = dataURL
```

**Key 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 (use `download()` 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:

```typescript
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.