This release includes 1 breaking change for platform teams planning a safe upgrade.
✓ No known CVEs patched in this version
Topics
Summary
AI summaryUpdates 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
- Embed before streaming. All `embedFont` / `embedJpg` calls must complete BEFORE `streamFlow`. Mid-stream embedding throws with a clear message.
- Lazy input. Pass a generator — don't materialize the whole Node[].
- Exclusive writable. streamFlow takes ownership; closes on success, aborts on failure.
- No `totalPages` in headers/footers. Accessing `ctx.totalPages` throws — switch to `renderFlow` if you need "Page X of Y".
- ~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
About earonesty/boxpdf](https:
All releases →Related context
Related tools
Beta — feedback welcome: [email protected]