Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# tspdf
# pdfkernel

Fast, dependency-light PDF forms and text for TypeScript. No WASM.

[![npm](https://img.shields.io/npm/v/@folsom/tspdf.svg)](https://www.npmjs.com/package/@folsom/tspdf)
[![bundle size](https://img.shields.io/bundlephobia/minzip/@folsom/tspdf)](https://bundlephobia.com/package/@folsom/tspdf)
[![license](https://img.shields.io/npm/l/@folsom/tspdf.svg)](./LICENSE)
[![npm](https://img.shields.io/npm/v/pdfkernel.svg)](https://www.npmjs.com/package/pdfkernel)
[![bundle size](https://img.shields.io/bundlephobia/minzip/pdfkernel)](https://bundlephobia.com/package/pdfkernel)
[![license](https://img.shields.io/npm/l/pdfkernel.svg)](./LICENSE)

- Read and fill AcroForm fields, including checkboxes, radios and dropdowns
- Generate field appearances (`/DA`, auto-size, alignment, comb, multiline)
Expand Down Expand Up @@ -43,15 +43,15 @@ Runs on `@noble/hashes`, `@noble/ciphers` and `fflate`. Nothing else.
## Installation

```bash
bun add @folsom/tspdf # or npm / pnpm / yarn
bun add pdfkernel # or npm / pnpm / yarn
```

## Usage

### Reading a document

```ts
import { PdfDocument } from "@folsom/tspdf";
import { PdfDocument } from "pdfkernel";

const doc = PdfDocument.open(bytes);

Expand Down Expand Up @@ -139,7 +139,7 @@ Appearances are drawn into the page content and the form is removed. Widgets tha
### Assembling documents

```ts
import { createDocument, addPage, copyPages, mergeDocuments, splitDocument } from "@folsom/tspdf";
import { createDocument, addPage, copyPages, mergeDocuments, splitDocument } from "pdfkernel";

const merged = mergeDocuments([a, b]);
const [chapter1, chapter2] = splitDocument(doc, [[0, 4], [5, 9]]); // inclusive ranges
Expand Down Expand Up @@ -175,7 +175,7 @@ const doc = await PdfDocument.load(bytes, { pages: [0] });

### Permissions

Documents can restrict filling, modification and copying via `/P`. tspdf enforces those flags, so
Documents can restrict filling, modification and copying via `/P`. pdfkernel enforces those flags, so
the restricted operation throws unless you opt out:

```ts
Expand Down Expand Up @@ -204,10 +204,10 @@ so a bomb degrades instead of taking the process down.

### Escaping to the object model

The facade covers the common cases. For anything else, `@folsom/tspdf/lowlevel` exposes the raw layer:
The facade covers the common cases. For anything else, `pdfkernel/lowlevel` exposes the raw layer:

```ts
import { N, isDict, isStream } from "@folsom/tspdf/lowlevel";
import { N, isDict, isStream } from "pdfkernel/lowlevel";

const cat = doc.dict(doc.trailer.get(N.Root));
const custom = doc.dict(cat?.get(N.AcroForm));
Expand All @@ -223,7 +223,7 @@ Every facade method delegates to a free function that is also exported. `doc.pag

Reproduce with `bun run bench`. Measured on f990.pdf, 778 KB with 1075 fields.

| | tspdf | pdfium | mupdf | pdf-lib |
| | pdfkernel | pdfium | mupdf | pdf-lib |
| --- | --- | --- | --- | --- |
| open + read all fields | **7.3 ms** | 34.4 ms | 20.8 ms | 221.2 ms |
| fill 20 fields and save | **7.0 ms** | n/a | 43.8 ms | 549.7 ms |
Expand All @@ -232,13 +232,13 @@ Reproduce with `bun run bench`. Measured on f990.pdf, 778 KB with 1075 fields.
| output size | **790 KB** | n/a | 1842 KB | 1492 KB |

Two things are not like-for-like: the mupdf and pdf-lib read columns fetch field names only, and
pdfium builds a richer text page than tspdf does, sorting into reading order and deriving boxes from
pdfium builds a richer text page than pdfkernel does, sorting into reading order and deriving boxes from
glyph outlines.

Holding a document open is where the difference compounds. mupdf's `fill()` costs the same whether
you set one value or two hundred, because each call reopens and re-serialises the file:

| values set | mupdf | tspdf | appended |
| values set | mupdf | pdfkernel | appended |
| --- | --- | --- | --- |
| 1 | 6.50 ms | **0.060 ms** | 0.5 KB |
| 10 | 6.20 ms | **0.116 ms** | 4.3 KB |
Expand All @@ -249,7 +249,7 @@ number that matters for a CLI that handles one file and exits.

## Limitations

tspdf does not render. There is no rasteriser and no plan for one; if you need pixels, use pdfium or
pdfkernel does not render. There is no rasteriser and no plan for one; if you need pixels, use pdfium or
mupdf. It also does not:

- Decode image codecs. DCT, JPX, JBIG2, CCITTFax and Brotli streams come back as raw bytes with an
Expand All @@ -271,15 +271,15 @@ mupdf. It also does not:
fixture files and no oracle. `test/fixtures.ts` builds them; add a case there rather than committing
a binary.

The deeper gates check tspdf against pdfium and mupdf as oracles rather than against its own
The deeper gates check pdfkernel against pdfium and mupdf as oracles rather than against its own
expectations, and need a corpus under `../data`. Field
reads are compared record by record (9418 widgets across 46 government and corporate forms, no
mismatches); written files are handed back to both engines to read; generated appearances are
rasterised by mupdf and compared as ink coverage. Text extraction agrees with pdfium on 99.80% of
characters.

Broader coverage comes from the pdf.js and pdfium regression corpora, 932 files that each exist
because they broke a parser. tspdf opens 908 of them; pdfium opens 896. Between them they exercise
because they broke a parser. pdfkernel opens 908 of them; pdfium opens 896. Between them they exercise
every security handler revision from R2 to R6, classic and stream xrefs, object streams and the
recovery path.

Expand Down
2 changes: 1 addition & 1 deletion bench/fill.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** Fill benchmark — the headline claim.
*
* 200 synthesized widgets on the bill-of-lading template, filled two ways by tspdf:
* 200 synthesized widgets on the bill-of-lading template, filled two ways by pdfkernel:
*
* "stateless" — open + set + save per call, i.e. apples-to-apples with a one-shot filler
* "live handle"— open once, then time only set + save, i.e. what the API shape unlocks
Expand Down
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"name": "@folsom/tspdf",
"name": "pdfkernel",
"version": "0.1.0",
"description": "A pure-TypeScript PDF core: lazy parsing, append-only writing, AcroForm read/fill/flatten/synthesize, and text extraction. No WASM.",
"type": "module",
"license": "MIT",
"author": "Folsom Intelligence",
"repository": {
"type": "git",
"url": "git+https://github.com/folsomintel/tspdf.git"
"url": "git+https://github.com/folsomintel/pdfkernel.git"
},
"homepage": "https://github.com/folsomintel/tspdf#readme",
"homepage": "https://github.com/folsomintel/pdfkernel#readme",
"bugs": {
"url": "https://github.com/folsomintel/tspdf/issues"
"url": "https://github.com/folsomintel/pdfkernel/issues"
},
"sideEffects": false,
"publishConfig": {
Expand Down
8 changes: 4 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** tspdf — public API.
/** pdfkernel — public API.
*
* The shape is an object model: `PdfDocument` → `Page`, and `Form` → `Field`. Nearly everything
* here is also available as a free function taking `(doc, …)`, because the facades are thin
Expand All @@ -7,9 +7,9 @@
* One build for every platform. Nothing in this graph imports `node:` anything, so a bundler has
* nothing to polyfill and there is no second target to keep in sync.
*
* The raw object model (`PdfDict`, the type guards, `N`) lives in `@folsom/tspdf/lowlevel`; its
* The raw object model (`PdfDict`, the type guards, `N`) lives in `pdfkernel/lowlevel`; its
* *types* are re-exported below so signatures stay nameable without that import. Anything reached
* by deep-importing `@folsom/tspdf/src/…` is internal and may change without notice.
* by deep-importing `pdfkernel/src/…` is internal and may change without notice.
*
* `PdfDocument.open(bytes)` — synchronous, every platform
* `PdfDocument.load(bytes)` — asynchronous, pre-warms streams up front
Expand All @@ -22,7 +22,7 @@ export { Form, type FieldEntry } from "./form/form.ts";
export { Field } from "./form/field.ts";

/** Value types, for naming what the facade hands back. The runtime constructors and type guards
* are in `@folsom/tspdf/lowlevel`. */
* are in `pdfkernel/lowlevel`. */
export type { PdfValue, PdfDict, PdfStream, PdfString, Ref } from "./core/objects.ts";
export type { Name } from "./core/names.ts";

Expand Down
2 changes: 1 addition & 1 deletion src/low-level.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** `@folsom/tspdf/lowlevel` — the raw object model and the helpers that speak it.
/** `pdfkernel/lowlevel` — the raw object model and the helpers that speak it.
*
* Nothing here is needed to read, fill, flatten, assemble or extract text; the facade on
* `PdfDocument`, `Page`, `Form` and `Field` covers all of that without a `PdfDict` in sight. This
Expand Down
6 changes: 3 additions & 3 deletions test/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
*
* Everything that writes is verified by pdfium and mupdf reading the result back. Our own reader
* agreeing with our own writer proves only that they share a bug — that lesson has been paid for
* twice already in this project, so nothing here is checked with tspdf alone.
* twice already in this project, so nothing here is checked with pdfkernel alone.
*/
import { readFileSync } from "node:fs";
import * as mupdf from "mupdf";
Expand Down Expand Up @@ -93,7 +93,7 @@ console.log("metadata");
const doc = PdfDocument.open(bytesOf(F1040));
setInfo(doc, {
title: "Round Trip",
author: "tspdf",
author: "pdfkernel",
subject: "テスト",
keywords: "a,b",
creationDate: now,
Expand All @@ -113,7 +113,7 @@ console.log("metadata");
const mupdfAuthor = md.getMetaData("info:Author");
md.destroy();
check(mupdfTitle === "Round Trip", `mupdf reads the title (${mupdfTitle})`);
check(mupdfAuthor === "tspdf", `mupdf reads the author (${mupdfAuthor})`);
check(mupdfAuthor === "pdfkernel", `mupdf reads the author (${mupdfAuthor})`);
}

// -------------------------------------------------------------------------- 2. page geometry
Expand Down
6 changes: 3 additions & 3 deletions test/appearance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
*
* Rendered with mupdf, not pdfium: FPDF_RenderPageBitmap does not draw widget annotations without
* an FPDF_FFLDraw pass. mupdf draws appearance streams directly, so it is both the right renderer
* here and an independent check — it is rasterising content tspdf wrote, using its own parser.
* here and an independent check — it is rasterising content pdfkernel wrote, using its own parser.
*
* Two variants per file, both rendered identically and compared to the untouched original:
* AP tspdf fill with appearance generation
* noAP tspdf fill with /NeedAppearances only, leaving mupdf to generate the appearances
* AP pdfkernel fill with appearance generation
* noAP pdfkernel fill with /NeedAppearances only, leaving mupdf to generate the appearances
*
* noAP is the reference: identical /V values, appearances drawn by mupdf itself. Comparing the two
* is a true like-for-like — our appearance stream against mupdf's, for the same text in the same
Expand Down
2 changes: 1 addition & 1 deletion test/pdfium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function pdfiumModule(): Promise<Module> {
return _module;
}

/** fpdf_formfill.h field types → the PDF `/FT` names tspdf reports. */
/** fpdf_formfill.h field types → the PDF `/FT` names pdfkernel reports. */
const FT_MAP: Record<number, string | null> = {
0: null,
1: "/Btn",
Expand Down
2 changes: 1 addition & 1 deletion test/spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** Behaviour checks against hand-built PDFs. No corpus, no pdfium, no mupdf.
*
* The differential gates prove tspdf *agrees* with the engines that own the format, which is the
* The differential gates prove pdfkernel *agrees* with the engines that own the format, which is the
* strongest evidence available but needs ~1 GB of fixtures and two WASM baselines. Without those a
* clone previously ran two tests. Everything here runs on `bun test/spec.ts` in a bare checkout.
*
Expand Down
Loading