npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pimeo/podpdf

v1.2.9

Published

Ultra-fast, zero-dependency PDF generation for Node.js, Bun & Browser.

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

npm version

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

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

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

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 })  // Custom

Metadata

.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 = 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:

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.