npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

repair-pdf

v0.2.0

Published

Repair damaged PDF files in the browser: rebuilds cross-reference tables, fixes stream lengths, recovers lost catalogs and page trees.

Readme

repair-pdf

Repair damaged PDF files in the browser. Zero-config, dependency-light (only fflate for compression), ~64 kB gzipped, built with tsdown.

The library does one thing: it takes bytes that are supposed to be a PDF and returns a normalized PDF using recovery strategies drawn from Chromium's PDFium, MuPDF, pdf.js and QPDF.

import { repairPdf } from 'repair-pdf';

const result = repairPdf(bytes);          // Uint8Array | ArrayBuffer
result.bytes;       // rewritten file, or original bytes if unchanged or repair is incomplete
result.repaired;    // true when a rewrite happened
result.issues;      // [{ code, severity, message, object? }, ...]
result.pageCount;   // pages reachable after repair
result.signed;      // true when the input carries digital signatures
result.signatureStatus; // 'present', 'absent', or 'unknown'

For upload pipelines, probe the container before doing a full repair:

import { quickCheckPdf, repairPdfIsolated } from 'repair-pdf';

const probe = quickCheckPdf(bytes);
if (probe.status === 'structurally-valid') {
  // Canonical envelope, xref offsets, catalog, and root Pages node: keep the
  // original bytes when container validity is all the application requires.
  return bytes;
}

// An indeterminate encrypted/unsupported container needs a full check; known
// damage needs repair. Use a Worker so timeout/abort is enforceable.
return (await repairPdfIsolated(bytes, { timeoutMs: 30_000 })).bytes;

The quick result is deliberately scoped to container validity. It does not decode content, fonts or images or traverse every semantic structure, so use analyzePdf/repairPdf when those checks are part of your acceptance policy.

API

repairPdf(input, options?) => RepairResult

Never throws on malformed input.

| option | default | description | | ------------------ | ------- | ----------- | | force | false | Rewrite even when no problem was detected. Also permits potentially lossy salvage of incomplete object streams; resource limits still preserve the original. | | password | '' | User or owner password for encrypted files. | | removeEncryption | false | Decrypt strings and streams and drop /Encrypt. Useful for libraries such as pdf-lib that refuse encrypted input. Requires the password to be accepted; an empty user password usually is. | | objectStreams | false | Repack eligible objects into compressed object streams. Produces PDF 1.5+ output and can substantially reduce rewritten file size. Encrypted input requires a valid password. | | maxSyntheticPages | 10000 | Maximum extra blank pages inferred from a damaged page-tree /Count. | | maxContentStreamsPerPage | 256 | Inspection ceiling for one page's /Contents array. Exceeding it returns the original with resource-limit; no streams are truncated. | | maxInputBytes | 268435456 | Refuse larger in-process inputs (hard maximum 2 GiB). | | maxObjects | 500000 | Maximum xref/object entries accepted from one document (hard maximum 2 million). | | contentStreams | 'validate' | 'preserve', 'validate', or conservatively 'sanitize' malformed page-description operators and state stacks. | | maxContentOperations | 1000000 | Maximum operators inspected in one content stream (hard maximum 10 million). | | maxDecodedStreamBytes | 33554432 | Per-stream decoded-byte budget, including xref/object streams and logical page-content sequences (capped at 512 MiB). | | maxTotalDecodedBytes | 134217728 | Shared decoding budget across structural recovery and semantic inspection (capped at 1 GiB). | | repairImages | true | Infer missing JPEG filters/dimensions/colourspaces and append a missing JPEG EOI marker. | | repairResources | true | Repair resource dictionaries and substitute Helvetica for fonts referenced by content but absent from /Resources. | | repairFormAppearances | true | Generate missing text/choice-field appearances and synchronize button /AS states. | | repairTags | true | Repair logical structure types, children, cycles, and parent links. | | flattenForms | false | Paint usable widget appearances into page content; remove the AcroForm only when every widget was flattened. This changes interactivity. | | flattenAnnotations | false | Paint usable visible annotation appearances into page content. Hidden/view-dependent annotations are retained. | | optimizeImages | false | Losslessly recompress image samples whose complete filter chain is understood. | | deduplicateObjects | false | Merge byte-identical immutable streams and fonts. Skipped for encrypted PDFs. | | garbageCollect | 'on-rewrite' | Remove unreachable objects on an existing rewrite; true also makes unused objects trigger a rewrite, false retains them. |

RepairResult: bytes, repaired, issues, pageCount (-1 when structural inspection is incomplete), encrypted, signed, signatureStatus (unknown when unreadable objects could hide a signature), reconstructed (xref rebuilt by scanning), version.

repaired reports that a rewrite happened, not that all content was recovered. Incomplete object-stream recovery returns the original bytes with an objstm-unreadable error by default. Structural resource limits likewise return the original with resource-limit, even with force: true. Semantic streams that cannot be inspected within the remaining budget are preserved and reported in issues. Decoding limits bound retained output; the decoder also needs a fixed working window and temporary buffers, so these are not process-memory limits.

analyzePdf(input, options?) => AnalyzeResult

Same analysis without producing output: issues, needsRepair, pageCount, encrypted, signed, signatureStatus, reconstructed, version.

Issue severities: error and warning normally trigger a rewrite; preservation guards can prevent it. info is diagnostic only (for example duplicate-object, password-required).

quickCheckPdf(input, options?) => QuickCheckResult

A non-mutating fast path that validates the header/EOF envelope, xref chain, every uncompressed xref offset and object header, compressed-object containers, catalog, and complete page tree (kids, parents, cycles, duplicates, and subtree counts). It does not scan the file for recovery or decode page-content streams. Structural object streams are decoded as needed. The status is one of structurally-valid, needs-repair, or full-check-required (for example, for an encrypted compressed catalog). verifyOffsets: false skips the all-offset pass when latency matters more than assurance; maxObjects applies the same object ceiling as full repair.

The included 1,500-page benchmark currently checks canonical 3.2 MB and 14.8 MB files in 134 ms and 88 ms respectively on the development machine; the default full analysis/repair path takes roughly 0.8–1.0 seconds on the same files. Run node scripts/perf.mjs 1500 on the deployment hardware for a representative local number.

repairPdfIsolated(input, options?) => Promise<RepairResult>

Runs repairPdf in a fresh module Worker, transfers the result back, and always terminates the Worker. timeoutMs defaults to 30 seconds (maximum 10 minutes), signal supports cancellation, and repair contains ordinary repair options. transferInput: true avoids a large input copy when the caller can allow its input ArrayBuffer to be detached. The shipped Worker is self-contained (including compression code). Its default URL works when the published ESM files are served without rebundling. Bundled applications should pass an emitted workerUrl, or use framework handling such as Vite's import RepairWorker from 'repair-pdf/worker?worker' together with workerFactory: () => new RepairWorker(). This is the recommended boundary for hostile uploads because synchronous JavaScript can only be forcibly interrupted by terminating its Worker.

Renderer and Preflight repairs

repairPdfAdvanced adds the operations that intrinsically need a complete renderer, colour engine, or second-pass writer: page rasterization, true Fast Web View linearization, image transcoding, certificate decryption, and PDF/A/X/E conversion. The engine is injected, so the core stays synchronous, small, CSP-friendly, and MIT licensed.

import { repairPdfAdvanced } from 'repair-pdf';
import { createQpdfRepairEngine } from 'repair-pdf/qpdf';

const engine = await createQpdfRepairEngine({
  workerUrl: new URL('qpdf-run/worker', import.meta.url),
  qpdfJsUrl: new URL('qpdf-run/qpdf.js', import.meta.url),
  wasmUrl: new URL('qpdf-run/qpdf.wasm', import.meta.url),
});
try {
  const result = await repairPdfAdvanced(bytes, {
    engine,
    linearize: true,
    generateAppearances: true,
    optimizeImages: true,
  });
} finally {
  await engine.destroy();
}

Install qpdf-run to use that optional browser/WASM adapter. It is a peer dependency and its worker/WASM payload is not bundled into repair-pdf. For Chrome/PDFium-style visual salvage, provide an engine with rasterize(); renderFallback: 'on-error' or 'always' rebuilds a portable PDF from the returned JPEG pages. Rasterization deliberately reports that selectable text, tags, links, forms, and signatures were lost. A commercial/hosted Preflight engine can implement rewrite() to service conformance and certificate credentials through the same typed interface.

Engines can declare capabilities; unsupported requests are rejected before execution. Linearization is checked on the final bytes, certificate output must be decrypted, and PDF/A/X/E is only reported as verified when the engine also implements verify(). Renderer results default to limits of 10,000 pages and 512 MiB of JPEG payload (maxRasterPages, maxRasterBytes). JPEG grayscale, RGB and CMYK metadata is inferred from the SOF marker or can be supplied explicitly. A structurally invalid or failed-verification engine result is discarded transactionally and the last usable PDF is returned with an error.

What gets repaired

File envelope

  • Garbage before %PDF- (BOMs, HTML, MIME headers). Like PDFium, offsets are treated as relative to the header, so files that only suffer from a prefix still take the fast path.
  • Missing or truncated %%EOF, garbage after it, missing header.
  • stream followed by a lone CR, wrong or missing endobj / endstream.

Cross-reference data

  • Missing startxref, wrong startxref, /Prev loops, offsets shifted by inserted or removed bytes, free-entry off-by-one subsections, hybrid /XRefStm files, xref streams with bad /W or short data.
  • Every in-use entry is verified against the object actually found at that offset. Any mismatch, an unreachable catalog, or a page tree that yields no pages triggers a full reconstruction by scanning the file for N G obj.
  • During reconstruction, duplicates are resolved the way MuPDF and QPDF do: the definition latest in the file wins, unless it is truncated. Object-stream members only override top-level objects that appear earlier in the file. Compressed-object entries are salvaged from broken xref streams (their offsets are ignored, only the stream/index mapping is trusted).

Streams

  • Wrong, missing or unresolvable /Length: the real extent is located from endstream, endobj or the next object header in a single pass, and the dictionary is rewritten with the correct direct length.
  • Object streams are unpacked into plain objects by default so the output uses a classic xref table (readable by every library, including ones without xref-stream support). objectStreams: true repacks eligible objects in batches of 100 and emits an xref stream. Truncated Flate data is decoded as far as possible.
  • Page descriptions, Form XObjects, Type 3 glyph procedures and annotation appearances are checked against the PDF operator grammar. Sanitizing removes unknown/ill-formed operations, recovers truncated inline images by dropping only the broken operation, and balances graphics, text, compatibility and marked-content state. A page's content array is inspected as one logical program, preserving state across streams; sanitization writes a page-owned copy of shared content.
  • JPEG image dictionaries can be reconstructed from SOF markers and missing EOI markers appended. Fully decodable sample streams can be recompressed.

Document structure

  • Trailer selection prefers the newest trailer whose /Root is a real catalog; otherwise the newest /Type /Catalog object; otherwise a catalog is synthesised.
  • Page tree: dangling /Kids, cycles, wrong /Count, wrong /Parent, missing /Type, inline page dictionaries. When no page is reachable, a flat tree is rebuilt from every /Type /Page object in the file (something none of the reference engines do), carrying over inherited MediaBox, Resources, Rotate and CropBox.
  • /Info recovered from any dictionary with /Producer or /Creator.
  • Missing or unreadable pages become blank pages so numbering is preserved; a missing subtree gets as many blank slots as its parent's /Count implies; duplicated page objects and shared /Pages subtrees are cloned; pages get a MediaBox and Resources when none is inherited; non-stream /Contents entries are dropped; pathologically deep page trees are flattened without losing inherited page attributes.
  • Name and number trees are sorted and repaired (/Dests, /EmbeddedFiles, /PageLabels, structure /ParentTree); outline chains have dangling links, loops, /Parent, /Prev, /Last, and /Count corrected; invalid annotation entries are removed, wrong annotation /P links are relinked, and annotations shared by pages are separated. A corrupt secondary tree that aliases a live Catalog, Pages, or Page object is discarded without mutating page structure.
  • Digital signatures and certification permissions are detected, including unusual field layouts. Opaque objects yield signatureStatus: 'unknown'. Rewriting a known signed file reports signature-invalidated.
  • References to missing objects become null.
  • A document with no usable page at all gets one blank page so it can still be opened (the no-pages error remains in the report).
  • Stale linearization dictionaries and old xref streams are dropped.
  • Missing font resources used by Tf are substituted with a portable Standard-14 font; malformed resource categories are removed.
  • AcroForm fields and widgets are reconciled, missing text/choice appearances generated, button appearance states synchronized, and appearances can be flattened into page content.
  • Logical structure element graphs are cycle-checked and parent links repaired.
  • Unreachable objects are garbage-collected on rewrites; optional deduplication merges identical immutable streams and fonts.

Encryption

  • Standard security handler R2 to R6 (RC4 40/128, AES-128, AES-256), user and owner passwords, Unicode SASLprep for R5/R6, /Identity, /StmF, /StrF and /EFF embedded-file crypt filters, unencrypted metadata.
  • Encrypted files are repaired without being decrypted: object numbers are preserved so existing ciphertext stays valid, and members unpacked from object streams are re-encrypted. When the password is unknown, members of encrypted object streams are written through untouched via an xref stream.
  • Unsupported public-key encrypted files are preserved byte-for-byte by the core unless force is set. Supply certificate material to a capable advanced engine instead; opaque rewriting is more likely to reduce compatibility.
  • Wrong or missing /Length, short /U, missing /ID: corrected in the /Encrypt dictionary once the password validates, so strict readers (pdf.js, MuPDF) accept the result too.

What it does not do

  • The small core does not contain JPEG 2000, JBIG2, CCITT, font-shaping, colour-management, CMS certificate, or full page-rendering engines. These jobs use repairPdfAdvanced with a compatible engine; qpdf WASM support is supplied as repair-pdf/qpdf.
  • Native sanitization intentionally preserves image operators and valid page content rather than attempting to recreate missing artwork. Use renderer fallback when source bytes are irrecoverable.
  • Rewriting a file invalidates digital signatures. Clean files are therefore returned untouched unless force or objectStreams is set; rewritten signed files report signature-invalidated.
  • Files whose /Encrypt dictionary itself is destroyed cannot be recovered by anyone without the key; a certificate-capable advanced engine may accept private-key material through the adapter interface.

Design notes

The implementation is a tolerant lexer and recursive-descent parser (never throws; unbalanced delimiters, junk tokens and keywords in the wrong place are skipped like pdf.js and PDFium do), a document loader with two strategies (trust-and-verify the xref, else scan), and a writer that re-serialises every object from the parsed model with stream data copied byte-for-byte.

Rules borrowed from the reference engines:

| rule | source | | ---- | ------ | | Header-relative offsets, %PDF searched in the first 1024 bytes | PDFium GetHeaderOffset, pdf.js checkHeader, QPDF, MuPDF | | Verify xref entries, rebuild on mismatch or unusable page tree | PDFium VerifyCrossRefTable / TryInit, pdf.js checkFirstPage | | Stream end = first of endstream / endobj, strip up to two EOL bytes | PDFium FindStreamEndPos | | Last object in file wins; truncated duplicate never replaces a good one | MuPDF pdf_repair_obj, pdf.js indexObjects | | Object-stream members lose to later top-level objects | MuPDF bug 708286 | | Recovered xref streams contribute only compressed entries | QPDF in_stream_recovery | | Trailer preference: newest with a valid /Root, then any /Type /Catalog | pdf.js, QPDF, MuPDF | | Set up decryption from harvested /Encrypt + /ID before opening object streams | MuPDF pdf_repair_xref | | Object number guard (8 388 607) | MuPDF PDF_MAX_OBJECT_NUMBER |

The complete list of tolerances, with spec sections and the engine each rule comes from, is in docs/tolerances.md.

Development

pnpm install
pnpm test          # vitest; uses qpdf and mutool as extra oracles when installed
pnpm build         # tsdown -> dist/
pnpm exec playwright install chromium  # install the browser for package checks
pnpm test:browser  # requires a current build; tests the packed npm artifact in Vite/Chromium
pnpm test:release  # requires qpdf/mutool; fetches/verifies corpus, tests, builds and checks browser
node scripts/make-fixtures.mjs   # regenerate test/fixtures/base (needs qpdf)
node scripts/try.mjs in.pdf out.pdf [--force] [--decrypt] [--object-streams] [--password=...]
node scripts/perf.mjs 1500       # timing on a generated 15 MB file
node scripts/fetch-corpus.mjs    # download the external corpus listed in test/corpus.json
node scripts/harvest-corpus.mjs  # clone ~30 000 PDFs of test suites / fuzz corpora (gitignored, ~30 GB)
node scripts/triage.mjs test/fixtures/harvest --jobs=4 --out=test/out/triage   # isolated repair + qpdf/mutool/pdf.js comparison

scripts/harvest-corpus.mjs sparse-clones the PDF test directories of pdf.js, qpdf (incl. fuzz corpora), pdfium and pdfium_tests, poppler, PDFBox, pypdf, pikepdf, iText, Tika, JHOVE, veraPDF, the SafeDocs and 0ca fuzz aggregates, Artifex's tests.git, corkami, PDF-Writer's decoder crashers and a dozen more, plus attachments of pdf.js bugs from Mozilla Bugzilla. scripts/triage.mjs repairs every file, checks originals and outputs with qpdf, mutool and pdf.js, each in memory-limited subprocesses, and writes report.txt grouped into crashes, regressions (output worse than input), still-broken outputs and files left untouched although an engine complained. That loop is how most of the rules in docs/tolerances.md were found. The last run over 28,814 files reported no repair-process crashes. Files whose structure is hidden behind unsupported certificate (PubSec) encryption or an experimental filter such as BrotliDecode are preserved byte-for-byte for a capable viewer or advanced engine; the core does not replace their inaccessible content with a synthetic document.

test/corpus.json lists real-world broken PDFs from the pdf.js, qpdf, pdfium and other open-source test suites. They are fetched into the gitignored test/fixtures/corpus/ directory and exercised by test/corpus.test.ts, which also runs over anything you drop into test/fixtures/private/ (never committed, useful for customer files).

The test suite applies 21 corruptions to 13 base fixtures (classic, object streams, linearized, incremental, RC4/AES encrypted) and checks the output with pdf.js (page count and text), qpdf --check and mutool.

The external corpus (test/corpus.json, 253 files) collects real-world broken PDFs from the qpdf, pdfium, pdf.js, pypdf, PDFBox, JHOVE, veraPDF, pikepdf, SafeDocs and OpenPreserve test suites. Each entry records the defect, the license of its source, a pinned SHA-256 and the expected page count. 22 are marked unrecoverable (no engine can open them, only "does not throw" is checked) and 8 contentDamaged (image or content-stream data is destroyed, so only the structure is verified). Readable source text is compared per page when page counts match, and across the document when recovery adds page slots. Validator diagnostics are compared with the original without suppressing font, decoding, authentication or metadata errors. Newly exposed source defects require narrow, count-limited exceptions with reasons in the manifest. Selected incomplete object streams must remain byte-for-byte unchanged by default; explicit forced salvage is tested separately for four malformed fixtures.

pnpm test:release, the prepublish hook and GitHub Actions require the full checksum-verified corpus and both native validators. The gate also includes MuPDF pixel comparisons for preservation regressions and a browser smoke test of the packed package: bundled/shipped workers, text preservation, input transfer, timeout, cancellation and password-protected qpdf WASM linearization. Set REPAIR_PDF_CHROME to a local Chromium executable to override Playwright's downloaded browser.