Skip to content

earonesty/boxpdf](https:

v1.4.0 Breaking

This release includes 1 breaking change for platform teams planning a safe upgrade.

Published 20d Build & Package
✓ No known CVEs patched
Read the diff → Tool health → What is this tool? →

✓ No known CVEs patched in this version

Topics

cloudflare-workers flexbox layout pdf pdf-lib typescript

Summary

AI summary

Updates Contract, Tests + design, and Internals across a mixed release.

Full changelog

Stream PDFs page-by-page with bounded memory, regardless of total page count.

New: `streamFlow`

```ts
import { PDFDocument, StandardFonts } from "pdf-lib";
import { streamFlow, text } from "boxpdf";

const pdf = await PDFDocument.create();
const font = await pdf.embedFont(StandardFonts.Helvetica);

// Workers / edge — stream into the Response body.
const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>();
streamFlow(pdf, writable, generateRows(font)).catch(console.error);
return new Response(readable, { headers: { "content-type": "application/pdf" } });

async function* generateRows(font) {
for await (const order of fetchOrders()) {
yield buildRow(font, order); // GC-able after this yield is consumed
}
}
```

For Node, wrap a Writable:

```ts
import { createWriteStream } from "node:fs";
import { streamFlow, nodeAdapter } from "boxpdf";

const out = nodeAdapter(createWriteStream("./report.pdf"));
await streamFlow(pdf, out, nodes);
```

Memory bench (50 lines/page)

| pages | renderFlow Δheap | streamFlow Δheap | output |
| ---: | ---: | ---: | ---: |
| 50 | 6.1 MB | 0 MB | 70 KB |
| 250 | 35.2 MB | 0 MB | 347 KB |
| 1000 | 157.4 MB | 0 MB | 1.4 MB |

A 1000-page report peaks at 0 MB above baseline with streamFlow vs. 157 MB with renderFlow + pdf.save(). Same byte output within 0.2%.

Contract

  1. Embed before streaming. All `embedFont` / `embedJpg` calls must complete BEFORE `streamFlow`. Mid-stream embedding throws with a clear message.
  2. Lazy input. Pass a generator — don't materialize the whole Node[].
  3. Exclusive writable. streamFlow takes ownership; closes on success, aborts on failure.
  4. No `totalPages` in headers/footers. Accessing `ctx.totalPages` throws — switch to `renderFlow` if you need "Page X of Y".
  5. ~5% size overhead. Per-batch ObjStm packing is slightly less efficient than whole-doc compression.

Internals

  • Built on pdf-lib's public `PDFObjectStream` + `PDFCrossRefStream` exports. Zero deep imports, no fork.
  • Per-page snapshot/delta of `PDFContext.indirectObjects`. PDFStream objects (content streams, image XObjects) get written + `ctx.delete()`d immediately. PDFDicts (page dicts, annots) batch into compressed ObjStms (`objectsPerStream` default 50).
  • Cross-reference stream uses PDF 1.5+ format — keeps output competitive with renderFlow's default `save()`.

Tests + design

  • 12 new tests; 113 total passing
  • Full design rationale in `docs/design/streaming.md`
  • Comparison harness: `pnpm exec tsx scripts/compare-stream.ts`
  • Memory bench: `node --expose-gc --import tsx scripts/bench-memory.ts`

Breaking Changes

  • Mid‑stream embedding of fonts or images now throws; all embed calls must complete before streamFlow.

Weekly OSS security release digest.

The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.

No spam, unsubscribe anytime.

Share this release

Track earonesty/boxpdf](https:

Get notified when new releases ship.

Sign up free

About earonesty/boxpdf](https:

All releases →

Related context

Beta — feedback welcome: [email protected]