# tinypdf for Lean TypeScript PDF Generation

> tinypdf is a dependency-free TypeScript library for building PDFs from text, rectangles, lines, JPEG images, and links. It fits small invoice, report, ticket, and certificate generators, but excludes custom fonts and several common image formats.

**Lulzx/tinypdf** — Minimal PDF creation library. <400 LOC, zero dependencies, makes real PDFs.

- Repository: https://github.com/Lulzx/tinypdf
- Stars: 1,924 · Forks: 76
- Language: TypeScript
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/lulzx-tinypdf

## tinypdf's Target Use: Small, Programmatic Documents

tinypdf addresses developers who need to produce a valid PDF directly from TypeScript without pulling in a general document or browser-rendering stack. Its intended outputs include invoices, receipts, reports, shipping labels, tickets, certificates, contracts, and data exports. These use cases share a useful constraint: the application already knows the page geometry and content, and it needs to place text and a few visual elements at explicit coordinates.

The repository describes the implementation as fewer than 400 lines of code with zero runtime dependencies. That small surface is the central design choice. Instead of attempting to model every PDF feature, tinypdf keeps a focused set of primitives: Helvetica text using WinAnsi encoding, filled rectangles, lines, JPEG images, clickable links, multiple pages, and custom page sizes. Markdown is also available through a separate `markdown()` entry point for documents that can be expressed as headings, lists, paragraphs, and horizontal rules.

This is a different contract from an HTML-to-PDF library. A caller positions each item with coordinates and chooses its dimensions. The `examples/invoice.ts` file is described as a production-style invoice layout using only core text, rectangle, and line primitives, which gives developers a relevant reference without changing the dependency count. The images attached to several repository examples cover reports, tickets, certificates, and invoices, but the README does not present those files as production guarantees.

The library is most appropriate when application data maps cleanly to a page drawing model. It is a poor fit when the source document is arbitrary HTML or when visual fidelity depends on fonts, browser layout, vector assets, or embedded forms.

## Coordinate-Based Drawing and Byte Output

A tinypdf document begins with `pdf()`. Calling `doc.page(callback)` adds a default 612 by 792 page, while `doc.page(width, height, callback)` allows a custom page size. Inside the callback, a context exposes the drawing operations. Text uses `ctx.text(str, x, y, size, options?)`, and its documented options include color, alignment, and a width value used by the layout logic. Geometric marks are handled through `ctx.rect(x, y, w, h, fill)` and `ctx.line(x1, y1, x2, y2, stroke, width?)`.

Image input is narrower than the API naming might suggest. `ctx.image(jpegBytes, x, y, w, h)` accepts JPEG bytes, which the README identifies as suitable for photographs, logos, and signatures. PNG, GIF, and SVG are explicitly excluded. Links are implemented as clickable URL regions, and their underline can be controlled through `{ underline }`. This means a link is a separate positioned area associated with the text rather than a font decoration inferred by the text renderer.

After the drawing callbacks have registered their content, `doc.build()` returns a `Uint8Array`. Large-document workflows can instead call `doc.buildStream()`, which returns a `ReadableStream<Uint8Array>`. According to the README, the stream emits page and image content incrementally and avoids assembling the final PDF into one additional full-size buffer. The inputs for page content and images are retained until the stream reaches them, so streaming changes output delivery rather than eliminating all memory use.

The package metadata declares ESM as the primary format and also exports a CommonJS build. Type declarations are generated during the build, giving TypeScript consumers a typed public API. Based on the documented flow, output starts with application data and ends as either an in-memory byte array or a response stream suitable for HTTP delivery.

## Installing tinypdf and Creating a First PDF

The README gives npm as the installation route. It does not specify a global installation, so the package should be added to the project that consumes it.

```bash
npm install tinypdf
```

After installation, create a document, draw one page, and write the bytes returned by `build()` to a file. The following is the README's quick-start example.

```typescript
import { pdf } from 'tinypdf'
import { writeFileSync } from 'fs'

const doc = pdf()

doc.page((ctx) => {
  ctx.rect(50, 700, 200, 40, '#2563eb')
  ctx.text('Hello PDF!', 60, 712, 24, { color: '#ffffff' })
  ctx.line(50, 680, 250, 680, '#000000', 1)
})

writeFileSync('output.pdf', doc.build())
```

The rectangle, white text, and black line should appear on the generated page. `writeFileSync()` imports `fs`, so this example assumes a runtime that provides Node-compatible filesystem access. The repository explicitly demonstrates running TypeScript examples with Bun rather than documenting a compiled application workflow for every runtime.

```bash
bun examples/executive-report.ts
```

That documented command runs the included executive report example. The repository layout also includes invoice, event ticket, and certificate examples, giving developers working references before they write page-layout code. The README does not publish command-line output for that example.

## JPEG, Link Regions, and Markdown Conversions

Adding a photograph, logo, or signature requires reading the source into a `Uint8Array` and passing it to `ctx.image()` with explicit placement and dimensions. JPEG support is deliberate. The README does not describe decoding, resizing logic, or support for other raster formats.

```typescript
import { readFileSync } from 'fs'

doc.page((ctx) => {
  const logo = new Uint8Array(readFileSync('logo.jpg'))
  ctx.image(logo, 50, 700, 100, 50)
})
```

A clickable URL is also positioned as its own region. `measureText()` can supply a width derived from the string and font size, which helps align the link area with rendered text.

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

For Markdown input, `markdown(str, options?)` accepts `width`, `height`, and `margin` options. The documented implementation handles headings through level three, bullet and numbered lists, horizontal rules, word wrapping, and pagination.

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

Markdown support reduces layout work for simple editorial content, but it is not presented as a complete CommonMark implementation. The README limits the stated syntax to the features shown above and does not document tables, embedded HTML, images, or arbitrary inline formatting.

## Streaming Large Documents Without Full Output Assembly

`buildStream()` is the main answer to large PDF output. The documented pattern registers 10,000 pages and writes the resulting stream with `Bun.write()`. It can also be returned directly as the body of a `Response` with the PDF media type.

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

await Bun.write('huge.pdf', doc.buildStream())
```

For a web response, the README shows this shape:

```typescript
return new Response(doc.buildStream(), {
  headers: { 'Content-Type': 'application/pdf' },
})
```

The distinction matters because `build()` returns one complete `Uint8Array`. With `buildStream()`, the consumer can receive chunks without the library first assembling an additional complete copy. That does not make the entire generation process constant-memory. Page content and image inputs remain retained until reached, and an application that reads many large JPEGs before streaming can still hold substantial input data.

The stream API is therefore most useful when downstream delivery, rather than page construction, is the memory constraint. The README does not document backpressure behavior, abort handling, error propagation details, or guarantees about chunk boundaries. Those points should not be assumed when designing a production HTTP integration.

## Feature Boundaries and the Case for jsPDF or pdf-lib

The clearest limitation is also the reason for tinypdf's small design: it omits Unicode and custom fonts, PNG, GIF, SVG, vector graphics, forms, encryption, compression, and HTML-to-PDF. Text is limited to Helvetica with WinAnsi encoding. A document containing customer names, legal copy, or labels in languages outside that encoding cannot be assumed to render correctly.

The README compares tinypdf with jsPDF using package size and dependency count, claiming 3.3 KB versus 229 KB and zero dependencies versus two. Those numbers are project-specific claims from the README, not an independent build measurement. The relevant architectural difference is that tinypdf removes the broader feature surface and retains direct placement of text and images. jsPDF or pdf-lib is the documented alternative when a project needs one of the excluded capabilities.

The README does not enumerate its API or capabilities, so it would be inaccurate to imply a specific feature match. The defensible distinction is scope: tinypdf is the compact, coordinate-based choice, while the named alternatives are presented for requirements outside that scope.

There is another operational constraint. The package scripts and examples use Bun for building, declaration generation, tests, and example execution. Consumers still need the compiled npm distribution, but maintainers changing source code are expected to work in a Bun-oriented toolchain. The README does not describe compatibility tests across Node.js versions or a supported-runtime matrix.

## Release State, Package Surface, and MIT Terms

The repository is not archived. Its last push was on 2026-08-13, and the latest listed release is `v0.4.1` from that date. That indicates recent project activity, although the repository does not publish a release cadence or long-term support policy.

Maintenance work is relatively exposed because the codebase is small and has no runtime dependencies. A release can be built with the `prepublishOnly` script, which invokes `bun run build`. The build creates a Node-targeted ESM output, a CommonJS output, and declaration files. Runtime dependency upgrades are not part of the stated package surface because `dependencies` is absent from `package.json`; however, TypeScript and Bun types remain development dependencies.

```bash
bun run build
```

The MIT license permits use, modification, distribution, and private reuse under its conditions, but this review does not provide legal advice. Developers distributing generated PDFs should still review the license text and determine whether their fonts, images, signatures, contracts, or other embedded content carry separate rights.

The published package contains `dist`, `LICENSE`, and `README.md`. Its package exports include types, an ESM import entry, and a CommonJS require entry. This is a useful compatibility surface for TypeScript consumers, although the README does not provide a complete Node.js example using the CommonJS entry.

## Conclusion

Adopt tinypdf when a small TypeScript generator only needs paginated text, basic geometry, JPEG images, links, or Markdown, and accepting Helvetica with WinAnsi encoding is acceptable. Do not adopt it as a replacement for a full PDF toolkit when Unicode, custom fonts, PNG assets, forms, encryption, compression, or HTML rendering is required. Before adoption, verify the character set and asset formats in the documents you intend to produce, then confirm that the chosen runtime can consume either `build()` or `buildStream()`. For the stated invoice and ticket use case, the boundary is clear: tinypdf is designed for direct drawing, not for reproducing full desktop publishing or HTML layout engines.

## FAQ

### Does tinypdf support Unicode and custom fonts?

No. The documented text support uses Helvetica with WinAnsi encoding, and custom fonts and Unicode are explicitly excluded.

### Which image formats can tinypdf embed?

tinypdf documents JPEG input through `ctx.image()`. PNG, GIF, and SVG are not included in the supported image formats.

### Can tinypdf stream a large PDF?

Yes. `doc.buildStream()` returns a `ReadableStream<Uint8Array>` and avoids assembling the final PDF into one additional full-size buffer, although retained page and image inputs can still consume memory.

## Sources

- [Issues](https://github.com/Lulzx/tinypdf/issues)
- [License: MIT](https://github.com/Lulzx/tinypdf/blob/main/LICENSE)
- [Lulzx/tinypdf on GitHub](https://github.com/Lulzx/tinypdf)
- [README](https://github.com/Lulzx/tinypdf/blob/main/README.md)
- [Releases](https://github.com/Lulzx/tinypdf/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/lulzx-tinypdf
