Skip to content

fix(writer): renumber objects densely on full save#71

Closed
Mythie wants to merge 1 commit into
mainfrom
fix/renumber-on-save
Closed

fix(writer): renumber objects densely on full save#71
Mythie wants to merge 1 commit into
mainfrom
fix/renumber-on-save

Conversation

@Mythie
Copy link
Copy Markdown
Contributor

@Mythie Mythie commented May 17, 2026

writeComplete previously preserved original object numbers from the
registry, which left gaps whenever orphans were dropped during garbage
collection. The resulting xref was sparse, /Size reported the max
original number while the actual entry count was smaller, producing
files where the trailer's stated size didn't match reality.

Renumber reachable objects to a contiguous 1..N range in a single pass:
walk the graph, build an old-ref → new-ref map, then remap refs inside
each object as it's written. Dangling refs (targets not in the
registry) become PdfNull per PDF 1.7 §7.3.10, since the old number no
longer exists in the new file.

Move the "objNum gen" key formatter onto PdfRef as an instance method
(ref.key()) so it's reusable and self-describing, replacing the local
helper in pdf-writer.

Also bumps dev/runtime deps (oxlint 1.39→1.65, vitest 4.0→4.1,
lint-staged 16.2→16.4, pkijs/asn1js/@noble/* patches, etc.) and drops
@typescript-eslint/prefer-const from .oxlintrc.json since newer oxlint
removed that alias — the core prefer-const rule still applies.

writeComplete previously preserved original object numbers from the
registry, which left gaps whenever orphans were dropped during garbage
collection. The resulting xref was sparse — /Size reported the max
original number while the actual entry count was smaller, producing
files where the trailer's stated size didn't match reality.

Renumber reachable objects to a contiguous 1..N range in a single pass:
walk the graph, build an old-ref → new-ref map, then remap refs inside
each object as it's written. Dangling refs (targets not in the
registry) become PdfNull per PDF 1.7 §7.3.10, since the old number no
longer exists in the new file.

Move the "objNum gen" key formatter onto PdfRef as an instance method
(ref.key()) so it's reusable and self-describing, replacing the local
helper in pdf-writer.

Also bumps dev/runtime deps (oxlint 1.39→1.65, vitest 4.0→4.1,
lint-staged 16.2→16.4, pkijs/asn1js/@noble/* patches, etc.) and drops
@typescript-eslint/prefer-const from .oxlintrc.json since newer oxlint
removed that alias — the core prefer-const rule still applies.
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
core Ready Ready Preview, Comment May 17, 2026 11:49pm

@github-actions
Copy link
Copy Markdown
Contributor

Benchmark Results

Comparison

Load PDF

Benchmark Mean p99 RME Samples
libpdf 2.27ms 3.49ms ±1.7% 220
pdf-lib 39.75ms 44.83ms ±3.7% 13
@cantoo/pdf-lib 39.70ms 44.45ms ±3.1% 13

Create blank PDF

Benchmark Mean p99 RME Samples
libpdf 73μs 160μs ±1.6% 6820
pdf-lib 420μs 1.42ms ±2.3% 1191
@cantoo/pdf-lib 442μs 1.64ms ±2.6% 1132

Add 10 pages

Benchmark Mean p99 RME Samples
libpdf 132μs 204μs ±1.2% 3790
pdf-lib 518μs 1.90ms ±2.8% 966
@cantoo/pdf-lib 466μs 2.20ms ±3.1% 1073

Draw 50 rectangles

Benchmark Mean p99 RME Samples
libpdf 370μs 930μs ±1.4% 1354
pdf-lib 1.60ms 5.16ms ±6.8% 312
@cantoo/pdf-lib 1.95ms 4.86ms ±5.1% 257

Load and save PDF

Benchmark Mean p99 RME Samples
libpdf 2.28ms 3.05ms ±1.2% 220
pdf-lib 87.10ms 93.73ms ±3.3% 10
@cantoo/pdf-lib 159.30ms 172.70ms ±3.3% 10

Load, modify, and save PDF

Benchmark Mean p99 RME Samples
libpdf 53.49ms 62.69ms ±8.6% 10
pdf-lib 88.85ms 100.45ms ±4.9% 10
@cantoo/pdf-lib 156.10ms 161.13ms ±1.5% 10

Extract single page from 100-page PDF

Benchmark Mean p99 RME Samples
libpdf 3.69ms 4.26ms ±0.8% 136
pdf-lib 9.24ms 11.98ms ±2.1% 55
@cantoo/pdf-lib 9.73ms 11.53ms ±2.1% 52

Split 100-page PDF into single-page PDFs

Benchmark Mean p99 RME Samples
libpdf 42.35ms 44.24ms ±2.0% 12
pdf-lib 87.16ms 93.37ms ±4.0% 6
@cantoo/pdf-lib 95.45ms 102.16ms ±5.5% 6

Split 2000-page PDF into single-page PDFs (0.9MB)

Benchmark Mean p99 RME Samples
libpdf 765.69ms 765.69ms ±0.0% 1
pdf-lib 1.62s 1.62s ±0.0% 1
@cantoo/pdf-lib 1.70s 1.70s ±0.0% 1

Copy 10 pages between documents

Benchmark Mean p99 RME Samples
libpdf 4.68ms 5.38ms ±1.0% 107
pdf-lib 12.00ms 14.48ms ±2.0% 42
@cantoo/pdf-lib 13.72ms 16.70ms ±2.1% 37

Merge 2 x 100-page PDFs

Benchmark Mean p99 RME Samples
libpdf 15.66ms 37.55ms ±9.2% 32
pdf-lib 53.09ms 54.16ms ±1.0% 10
@cantoo/pdf-lib 63.48ms 68.79ms ±3.4% 8

Fill FINTRAC form fields

Benchmark Mean p99 RME Samples
libpdf 21.54ms 25.66ms ±4.2% 24
pdf-lib 33.96ms 47.74ms ±6.6% 15
@cantoo/pdf-lib 34.64ms 41.56ms ±5.0% 15

Fill and flatten FINTRAC form

Benchmark Mean p99 RME Samples
libpdf 19.29ms 22.85ms ±4.0% 26
pdf-lib FAILED - - 0
@cantoo/pdf-lib 38.54ms 48.54ms ±5.6% 13
Copying

Copy pages between documents

Benchmark Mean p99 RME Samples
copy 1 page 1.16ms 2.64ms ±3.2% 433
copy 10 pages from 100-page PDF 4.82ms 8.51ms ±3.1% 104
copy all 100 pages 7.77ms 8.43ms ±0.9% 65

Duplicate pages within same document

Benchmark Mean p99 RME Samples
duplicate page 0 1.03ms 1.93ms ±1.4% 484
duplicate all pages (double the document) 1.02ms 2.05ms ±1.3% 491

Merge PDFs

Benchmark Mean p99 RME Samples
merge 2 small PDFs 1.57ms 2.35ms ±1.2% 320
merge 10 small PDFs 7.89ms 8.53ms ±0.8% 64
merge 2 x 100-page PDFs 14.47ms 21.12ms ±2.8% 35
Drawing

benchmarks/drawing.bench.ts

Benchmark Mean p99 RME Samples
draw 100 rectangles 639μs 1.70ms ±3.0% 782
draw 100 circles 1.35ms 2.79ms ±2.8% 370
draw 100 lines 577μs 1.23ms ±1.5% 868
draw 100 text lines (standard font) 1.62ms 2.33ms ±1.4% 309
create 10 pages with mixed content 1.46ms 2.35ms ±1.7% 343
Forms

benchmarks/forms.bench.ts

Benchmark Mean p99 RME Samples
get form fields 3.53ms 8.53ms ±4.9% 142
fill text fields 12.58ms 17.31ms ±4.7% 40
read field values 3.01ms 3.52ms ±0.7% 166
flatten form 8.59ms 12.26ms ±2.2% 59
Loading

benchmarks/loading.bench.ts

Benchmark Mean p99 RME Samples
load small PDF (888B) 57μs 141μs ±0.6% 8802
load medium PDF (19KB) 92μs 175μs ±0.5% 5449
load form PDF (116KB) 1.39ms 2.50ms ±1.8% 359
load heavy PDF (9.9MB) 2.28ms 2.87ms ±0.7% 220
Saving

benchmarks/saving.bench.ts

Benchmark Mean p99 RME Samples
save unmodified (19KB) 112μs 258μs ±2.9% 4451
save with modifications (19KB) 875μs 2.69ms ±3.0% 572
incremental save (19KB) 176μs 352μs ±3.0% 2842
save heavy PDF (9.9MB) 2.40ms 5.89ms ±4.5% 209
incremental save heavy PDF (9.9MB) 7.86ms 9.22ms ±3.2% 64
Splitting

Extract single page

Benchmark Mean p99 RME Samples
extractPages (1 page from small PDF) 1.12ms 2.57ms ±3.2% 447
extractPages (1 page from 100-page PDF) 3.64ms 4.14ms ±0.7% 138
extractPages (1 page from 2000-page PDF) 56.72ms 57.80ms ±0.7% 10

Split into single-page PDFs

Benchmark Mean p99 RME Samples
split 100-page PDF (0.1MB) 41.11ms 45.68ms ±3.6% 13
split 2000-page PDF (0.9MB) 746.02ms 746.02ms ±0.0% 1

Batch page extraction

Benchmark Mean p99 RME Samples
extract first 10 pages from 2000-page PDF 58.17ms 60.05ms ±1.1% 9
extract first 100 pages from 2000-page PDF 62.19ms 63.45ms ±1.2% 9
extract every 10th page from 2000-page PDF (200 pages) 66.23ms 67.69ms ±1.2% 8
Environment
  • Runner: Linux (X64)
  • Runtime: Bun 1.3.14

Results are machine-dependent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant