Lulzx/tinypdf

Minimal PDF creation library. <400 LOC, zero dependencies, makes real PDFs.

TypeScript

1,923

25 commits

updated Aug 13, 2026

See the code

README

tinypdf

npm size license

Minimal PDF creation library. <400 LOC, zero dependencies, makes real PDFs.

npm install tinypdf
Executive reportEvent ticket
Executive reportEvent ticket
KPI dashboard and chartBold landscape admission ticket
CertificateInvoice
CertificateInvoice
Formal award certificateEditorial studio invoice

Run any example with Bun, for example: bun examples/executive-report.ts.


Why tinypdf?

tinypdfjsPDF
Size3.3 KB229 KB
Dependencies02

~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.

Build with it

Invoices, receipts, reports, shipping labels, tickets, certificates, contracts, data exports

Features

FeatureDescription
TextHelvetica (WinAnsi), any size, hex colors, align left/center/right
ShapesRectangles and lines
ImagesJPEG (photos, logos, signatures)
LinksClickable URLs with optional underline
PagesMultiple pages, custom sizes
MarkdownConvert markdown to PDF with headers, lists, rules

Not included

Unicode/custom fonts, PNG/GIF/SVG, vector graphics, forms, encryption, compression, HTML-to-PDF

Need those? Use jsPDF or pdf-lib.


Quick start

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())

Add images

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' })
})

Measure text width

import { measureText } from 'tinypdf'

measureText('Hello', 12) // => 27.34 (points)

Markdown to PDF

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)

Stream large PDFs

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' },
})

API

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 }

Full example

See the complete invoice generator for a production-style layout using only tinypdf's core text, rectangle, and line primitives.


License

MIT

invoice-generator
pdf
pdf-generation

Lulzx/tinypdf

Minimal PDF creation library. <400 LOC, zero dependencies, makes real PDFs.

TypeScript

1,923

25 commits

updated Aug 13, 2026

See the code

README

tinypdf

npm size license

Minimal PDF creation library. <400 LOC, zero dependencies, makes real PDFs.

npm install tinypdf
Executive reportEvent ticket
Executive reportEvent ticket
KPI dashboard and chartBold landscape admission ticket
CertificateInvoice
CertificateInvoice
Formal award certificateEditorial studio invoice

Run any example with Bun, for example: bun examples/executive-report.ts.


Why tinypdf?

tinypdfjsPDF
Size3.3 KB229 KB
Dependencies02

~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.

Build with it

Invoices, receipts, reports, shipping labels, tickets, certificates, contracts, data exports

Features

FeatureDescription
TextHelvetica (WinAnsi), any size, hex colors, align left/center/right
ShapesRectangles and lines
ImagesJPEG (photos, logos, signatures)
LinksClickable URLs with optional underline
PagesMultiple pages, custom sizes
MarkdownConvert markdown to PDF with headers, lists, rules

Not included

Unicode/custom fonts, PNG/GIF/SVG, vector graphics, forms, encryption, compression, HTML-to-PDF

Need those? Use jsPDF or pdf-lib.


Quick start

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())

Add images

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' })
})

Measure text width

import { measureText } from 'tinypdf'

measureText('Hello', 12) // => 27.34 (points)

Markdown to PDF

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)

Stream large PDFs

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' },
})

API

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 }

Full example

See the complete invoice generator for a production-style layout using only tinypdf's core text, rectangle, and line primitives.


License

MIT

invoice-generator
pdf
pdf-generation

Project health

Latest release

0.4.1 · Aug 13, 2026

Releases this year

5

Significant stargazers

(top 24 of 37)

silverwind

670 followers · starred Dec 2025

Miguel Grinberg

12,947 followers · starred Dec 2025

Mitar

448 followers · starred Dec 2025

Kainoa Kanter

593 followers · starred Dec 2025