The Problem
Generating PDFs from JavaScript often pulls in heavyweight libraries (e.g., jsPDF) that add megabytes of code and multiple runtime dependencies. In low‑memory environments—edge functions, serverless workers, or embedded devices—those bundles are impractical, yet many applications only need to place text, simple shapes, and JPEG images on a page.
What This Does
tinypdf provides a < 400 LOC PDF generator with zero external dependencies. The core implementation lives in src/index.ts, exposing functions such as pdf(), measureText(), markdown(), and drawing helpers (text, rect, line, image). Example code in examples/invoice.ts and examples/showcase.ts shows how a few dozen lines produce a valid PDF (examples/invoice.pdf). The library’s API surface is defined in TypeScript, with the entry point src/index.ts exporting the public symbols.
How It Is Wired
Execution starts at the exported pdf() function in src/index.ts (line ~120). pdf() creates a PDFBuilder instance, then callers invoke doc.page(cb) or doc.page(width, height, cb). The page callback receives a context (ctx) that calls drawing primitives:
ctx.text→text()→measureText()andcolorOp()→addObject().ctx.rect/ctx.line→ shape helpers →colorOp()→addObject().ctx.image→parseJpeg()→addObject().
All drawing helpers funnel through addObject, the most‑connected internal function (called from 4 places). After page construction, doc.build() calls build() → emitChunks() → serialize() → pdfString(), which assembles the final byte stream. For streaming output, buildStream() follows the same path but yields a ReadableStream from emitChunks.
The test suite in src/index.test.ts exercises these paths, particularly collectStream, which reads the streamed output back into a buffer. The internal call graph contains 38 resolved edges; there are no circular dependencies, and src/index.ts is the sole hub (22 functions, 8 types/classes). The markdown converter (markdown() in src/index.ts) wraps text handling and reuses wrap(), measureText(), and colorOp().
How To Use It
# Clone and install
git clone https://github.com/moses-y/tinypdf
cd tinypdf
npm install # package.json defines runtime deps (none)
# Build the example PDF
npm run build # compiles TypeScript (tsconfig.json)
node examples/invoice.ts # writes examples/invoice.pdf
No additional configuration files or environment variables are required. The library is imported as shown in the README (import { pdf } from 'tinypdf'). For streaming large documents, call doc.buildStream() and pipe the resulting ReadableStream to a file or HTTP response.
Real‑World Use
A SaaS invoice service can embed tinypdf in a serverless function to generate a one‑page receipt on‑the‑fly:
import { pdf } from 'tinypdf';
export async function handler(event) {
const doc = pdf();
doc.page(ctx => {
ctx.text(`Invoice #${event.id}`, 50, 750, 18);
ctx.rect(45, 730, 200, 30, '#0044aa');
});
return new Response(doc.build(), { headers: { 'Content-Type': 'application/pdf' } });
}
The bundle stays under 5 KB, fitting comfortably within typical Lambda size limits.
Code Health & Issues
- MEDIUM – High branching density –
src/index.tshas 94 branch points across 324 lines. Refactor decision‑heavy logic into smaller, strategy‑style helpers. - MEDIUM – Oversized test file –
src/index.test.tsspans 1 093 lines, making local changes high‑risk. Split tests by feature (text, shapes, images, markdown). - MEDIUM – Enable Dependabot or Renovate – No update bot configured; a vulnerable advisory would remain unpatched. Add
.github/dependabot.ymlcovering the repository’s ecosystem. - Low – Missing CI/CD – No
.github/workflowsor other CI configuration; automated testing is not enforced. - Low – No lockfile –
package.jsonis present without a lockfile, risking non‑reproducible installs.
The Bottom Line
tinypdf delivers a functional, ultra‑light PDF generator that meets the core need of placing text, shapes, and JPEGs on a page without pulling in external code. The codebase is small but densely branched, and the test suite is monolithic, so maintainers should modularize both logic and tests. It is ideal for edge‑oriented services or any project where bundle size and dependency minimization are paramount.