Minimal PDF creation library. <400 LOC, zero dependencies, makes real PDFs.
npm install tinypdf| Executive report | Event ticket |
|---|---|
![]() |
![]() |
| KPI dashboard and chart | Bold landscape admission ticket |
| Certificate | Invoice |
![]() |
![]() |
| Formal award certificate | Editorial studio invoice |
Run any example with Bun, for example: bun examples/executive-report.ts.
| tinypdf | jsPDF | |
|---|---|---|
| Size | 3.3 KB | 229 KB |
| Dependencies | 0 | 2 |
~70x smaller. We removed TTF fonts, PNG/SVG, HTML-to-PDF, forms, encryption, and compression. What's left is the 95% use case: put text and images on a page.
Invoices, receipts, reports, shipping labels, tickets, certificates, contracts, data exports
| Feature | Description |
|---|---|
| Text | Helvetica (WinAnsi), any size, hex colors, align left/center/right |
| Shapes | Rectangles and lines |
| Images | JPEG (photos, logos, signatures) |
| Links | Clickable URLs with optional underline |
| Pages | Multiple pages, custom sizes |
| Markdown | Convert markdown to PDF with headers, lists, rules |
Unicode/custom fonts, PNG/GIF/SVG, vector graphics, forms, encryption, compression, HTML-to-PDF
Need those? Use jsPDF or pdf-lib.
import { pdf } from 'tinypdf'
import { writeFileSync } from 'fs'
const doc = pdf()
doc.page((ctx) => {
ctx.rect(50, 700, 200, 40, '#2563eb') // blue rectangle
ctx.text('Hello PDF!', 60, 712, 24, { color: '#ffffff' })
ctx.line(50, 680, 250, 680, '#000000', 1) // black line
})
writeFileSync('output.pdf', doc.build())import { readFileSync } from 'fs'
doc.page((ctx) => {
const logo = new Uint8Array(readFileSync('logo.jpg'))
ctx.image(logo, 50, 700, 100, 50)
})import { pdf, measureText } from 'tinypdf'
doc.page((ctx) => {
const text = 'Visit Example.com'
const y = 700
ctx.text(text, 50, y, 14, { color: '#0066cc' })
ctx.link('https://example.com', 50, y - 4, measureText(text, 14), 18, { underline: '#0066cc' })
})import { measureText } from 'tinypdf'
measureText('Hello', 12) // => 27.34 (points)import { markdown } from 'tinypdf'
import { writeFileSync } from 'fs'
const pdf = markdown(`
# Hello World
A minimal PDF from markdown.
## Features
- Headers (h1, h2, h3)
- Bullet lists
- Numbered lists
- Horizontal rules
---
Automatic word wrapping and pagination included.
`)
writeFileSync('output.pdf', pdf)Use buildStream() to emit a ReadableStream<Uint8Array> incrementally. Page content and image inputs are retained until the stream reaches them, but stream bodies are emitted directly and the final PDF is not assembled into one additional full-size buffer.
import { pdf } from 'tinypdf'
const doc = pdf()
for (let i = 0; i < 10_000; i++) {
doc.page((ctx) => ctx.text(`Page ${i + 1}`, 50, 750, 12))
}
// Write to disk (Bun)
await Bun.write('huge.pdf', doc.buildStream())
// Or serve as an HTTP response
return new Response(doc.buildStream(), {
headers: { 'Content-Type': 'application/pdf' },
})pdf() // create document
doc.page(callback) // add page (612×792 default)
doc.page(width, height, callback) // add page with custom size
doc.build() // returns Uint8Array
doc.buildStream() // returns ReadableStream<Uint8Array>
ctx.text(str, x, y, size, options?) // options: { color, align, width }
ctx.rect(x, y, w, h, fill) // filled rectangle
ctx.line(x1, y1, x2, y2, stroke, width?) // line
ctx.image(jpegBytes, x, y, w, h) // JPEG image
ctx.link(url, x, y, w, h, options?) // options: { underline }
measureText(str, size) // text width in points
markdown(str, options?) // options: { width, height, margin }See the complete invoice generator for a production-style layout using only tinypdf's core text, rectangle, and line primitives.
MIT



