Merge, split, extract text, compress, convert, and inspect PDFs entirely on your own machine. No uploads, no account, no telemetry — the shipped package contains no outbound network client at all.
LocalDocForge is a privacy-first document-processing workbench: a typed
Python core library, a scriptable ldf CLI, and a localhost API with a
status page. It exists for the documents you would never paste into a
cloud converter.
$ ldf compress outline-6page.pdf -o smaller.pdf
Operation : compress
Status : success
Engine : pikepdf 10.10.0
Input : outline-6page.pdf (5,777 B, 6 pages)
Output : smaller.pdf (2,953 B, 6 pages)
Elapsed : 0.36s
Validation: passed (7 checks)
Real output from this repository's synthetic test fixture — 48.9 % smaller, losslessly, with the result render-compared pixel-for-pixel against the source before anything was published.
- Your files never leave the machine. Every operation runs locally
through pikepdf/libqpdf, PDFium, and Pillow. The web UI binds to
127.0.0.1by default and authenticates every request. - It refuses to guess. Damaged PDFs are rejected rather than silently "repaired"; generated PDFs are structurally reopened/render-checked, images must decode, and extracted text must pass strict encoding/provenance/schema checks before publication. Originals are never modified in place.
- It refuses to lie. A feature is advertised only when its
implementation and a live engine probe both pass (
ldf doctoris the truth, not a brochure). Cropping is never called redaction. Known preservation losses are reported with stable warning codes instead of being dropped silently.ldf agent-briefturns the same registry and live probe state into compact Markdown or JSON for coding agents; planned capabilities cannot enter that output.
| Area | Capabilities |
|---|---|
| Organize | merge (whole files or per-input page ranges) · split (ranges / every-N / single pages) · remove pages · extract pages · reorder/duplicate/reverse |
| Edit | rotate · crop (with an explicit crop is not redaction warning) |
| Optimize | compress — lossless structural preset (stream recompression, object streams, unused-resource pruning). Image data is never re-encoded; sampled pages must render pixel-identical to the source or nothing is published; "didn't shrink" is reported, never hidden |
| Convert | PDF → UTF-8 Markdown/plain text/JSONL via PDFium (pdf-to-md, page anchors, per-page coverage, honest layout warnings, and opt-in conservative ruled tables through pdfplumber) · Markdown → validated PDF through Typst ≥0.15.1 (md-to-pdf, bounded CommonMark + GFM tables, local raster images, honest dropped-construct warnings) · images → PDF (HEIC/JPG/PNG/TIFF/BMP/WebP, multipage TIFF, EXIF orientation, A4/Letter/Legal/image/custom page sizes) · PDF → images (PNG/JPEG/WebP/TIFF, 18–1200 DPI; --preset llm makes per-page JPEG q85 renders with long edge ≤ 1568 px) · convert images (iPhone HEIC and the other formats → PNG/JPEG/WebP/TIFF; --preset llm produces AI-assistant-ready JPEGs with GPS/EXIF stripped) |
| Inspect | page count, encryption, page sizes, per-page extracted character counts, annotations, outlines, forms, attachments, JavaScript presence |
| Agent integration | deterministic ldf agent-brief Markdown/JSON generated from implemented CAPABILITY_SPECS plus one live capability probe, including usage, exit codes, gotchas, workflow, and feedback rules |
| Local web API | loopback FastAPI service + status page; every conversion runs in a fresh OS-contained worker process |
Everything above is covered by the repository's test suite and full release
gate; dated counts and platform-scoped evidence are recorded in
docs/STATUS.md. OCR, Office conversion, lossy compression presets,
redaction, signatures, and the rest of the roadmap are not implemented
yet and are honestly reported as unavailable by ldf doctor — see
docs/FEATURE_MATRIX.md.
Requires CPython 3.12–3.14. There is no PyPI package yet; install from the repository:
# CLI + core PDF/image tools (the default "lite" set)
pip install "git+https://github.com/pes1ug23am910/LocalDocForge"
# with the localhost web API and status page
pip install "localdocforge[standard] @ git+https://github.com/pes1ug23am910/LocalDocForge"
ldf doctorFor a supply-chain-audited install — hash-locked dependencies first, then the package with no re-resolution — use the lock profiles:
# Windows PowerShell (the release-hardened platform)
git clone https://github.com/pes1ug23am910/LocalDocForge && cd LocalDocForge
py -3.14 -m venv .venv
.venv\Scripts\python.exe -m pip install --require-hashes `
-r requirements\locks\lite.txt
.venv\Scripts\python.exe -m pip install --no-deps ".[lite]"
.venv\Scripts\ldf.exe --json doctor# Linux/macOS (CI-tested; see "Status and maturity")
git clone https://github.com/pes1ug23am910/LocalDocForge && cd LocalDocForge
python3 -m venv .venv
.venv/bin/python -m pip install --require-hashes -r requirements/locks/lite.txt
.venv/bin/python -m pip install --no-deps ".[lite]"
.venv/bin/ldf --json doctorProfiles: lite (CLI/core, the default), standard (adds the localhost
API), full (adds the pypdf diagnostic adapter), dev (adds test/lint/
build tooling). Exact recipes, locks, and uninstalls:
docs/PACKAGING.md.
md-to-pdf additionally needs a separately installed Typst executable at
version 0.15.1 or newer. The Python package does not bundle Typst; when the
probe is missing or too old, the capability remains visible but unavailable
and the CLI exits 3 with the install hint from ldf doctor.
ldf merge a.pdf b.pdf -o merged.pdf
ldf merge a.pdf --pages 1-5 b.pdf --pages 2-end -o merged.pdf
ldf split input.pdf -d parts/ --every 10
ldf remove-pages input.pdf --pages "2,5-7" -o out.pdf
ldf extract-pages input.pdf --pages "1-3,10" -o out.pdf
ldf organize input.pdf --order "3,1,2,4-end" -o out.pdf
ldf rotate input.pdf --degrees 90 --pages odd -o out.pdf
ldf crop input.pdf --box "50,50,400,500" -o out.pdf # warns: NOT redaction
ldf compress input.pdf -o smaller.pdf # lossless; images untouched
ldf images-to-pdf scans/*.jpg -o scans.pdf --page-size A4
ldf pdf-to-images input.pdf -d pages/ --format png --dpi 300
ldf pdf-to-images scanned.pdf -d vision/ --preset llm # per-page vision-ready JPEGs
ldf pdf-to-md input.pdf -o content.md # Markdown + source-page anchors
ldf pdf-to-md report.pdf -o content.md --tables # confident ruled grids → GFM tables
ldf pdf-to-md input.pdf -o content.jsonl --format jsonl # one UTF-8 JSON record per page
ldf md-to-pdf notes.md -o notes.pdf --paper A4 --margin 20 --toc
ldf convert-images photos/*.HEIC -d ready/ --preset llm # iPhone photos → AI-ready JPEGs
ldf inspect input.pdf
ldf agent-brief # registry-derived Markdown for coding agents
ldf --json agent-brief # the same ordered snapshot as structured JSON
ldf --json doctor
ldf --strict-offline web # localhost API + status page; prints the session tokenagent-brief must resolve the repository's writable
docs/AGENT_FEEDBACK.md. It works with a discoverable source checkout (including
the repository-local environment above); a detached wheel/direct VCS install
outside any checkout exits 1 rather than pointing agents at a packaged imitation.
Page ranges: 1-5,9,12-end, odd, even, reverse, last, last-5
(the last five pages). For encrypted PDFs, non-interactive callers use the
global --password-stdin option (one UTF-8 line) or LDF_PASSWORD; precedence
is flag → environment → hidden TTY prompt. Password values are never taken as
command-line arguments or written to output/reports/logs. One password applies
to all encrypted inputs in an invocation. Existing outputs are never
overwritten unless you say
--collision overwrite. Full grammar, exit codes, and the HTTP API
contract: docs/CLI.md.
pdf-to-md writes extracted content to the requested file using strict UTF-8
and normalized LF line endings; stdout remains the report/diagnostic channel,
not a text-fidelity channel. Markdown headings and reading order are explicitly
heuristic. Markdown-only --tables is off by default; when enabled, it emits
only bounded, rectangular tables found from explicit ruling lines, treats the
first physical row as the inferred GFM header, and keeps lower-confidence,
borderless, merged-cell, rotated, or overly dense candidates as flowed text.
The report summarizes selected-page coverage and attributes the stable
no-text-layer, headings-inferred, reading-order-uncertain,
table-fidelity-best-effort, and tables-flattened codes without copying
document text into the report. Absence of a table warning is not proof that a
page has no table; it means only that the bounded heuristics found no caveat.
For a page with no usable text layer, render it with
pdf-to-images --preset llm.
md-to-pdf accepts strict UTF-8 .md/.markdown files and renders a bounded
CommonMark subset plus GFM tables. Relative local raster images are validated,
counted as inputs, normalized to neutral PNG names in the private workspace,
and never exposed to Typst by their original paths. Raw HTML, math, footnotes,
and unknown constructs are dropped with markdown-construct-dropped; source
line numbers appear in report metadata, while source text and Typst diagnostics
do not. Link destinations are limited to http, https, mailto, and tel.
Typst packages/imports are not supported. Every result is reopened, syntax-
checked, page-count checked, and rendered through PDFium before publication.
Markdown preprocessing has a 16 MiB hard ceiling, reduced by the configured
input, memory/512, and temporary/64 byte limits (4 MiB with defaults), plus
100,000 source-line, 250,000 parser-token, and 256 image-reference ceilings.
Detailed dropped-construct entries stop at 256 and add one aggregate summary.
The ldf web server binds to 127.0.0.1, authenticates every API call
with a per-session token header, sends CSP/hardening headers, and keeps job
history in memory only. Every conversion runs in a fresh spawned worker
under OS containment (a Windows Job Object with kill-on-close, memory, CPU,
and process limits; a POSIX process group with resource limits), and a
terminal state is published only after the process tree is verified gone.
Early alpha, honestly scoped. Phase 0 (foundation), the core of Phase 1 (structural PDF tools + image conversion), the first Phase 2 slice (lossless compression, 2026-08-03), and the core Phase 3 PDF text-extraction path are implemented, tested, and gated.
- For everyday, non-sensitive documents: working and validated — the full release gate (locks, lint, types, two full test-suite runs including a network-blocked one, reproducible builds, clean install matrix) passes, and every capability has been exercised end-to-end on real files.
- For sensitive documents: not yet cleared, by the project's own
release decision. The open blockers (a bundled-dependency advisory,
no OS-enforced outbound-network denial proof, in-process CLI parsing,
platform scope) are recorded plainly in
docs/STATUS.md— nothing is hidden, and nothing is promised early. - Platforms: Windows 11 x64 is the release-hardened platform with
retained local evidence. Linux and macOS pass the full test suite and
clean-install matrix in CI (CPython 3.12–3.14, first executed
2026-08-03); they are CI-tested, not yet release-hardened. Deleting temp
files is best-effort, not forensic erasure, and
--strict-offlineis an application policy — not an OS firewall; seedocs/THREAT_MODEL.mdfor the boundaries stated without marketing.
Is it really offline? The shipped package contains no outbound network
client, telemetry, update check, or remote asset — verified by source
inspection and by running the complete test suite with Python DNS and
non-loopback sockets denied. --strict-offline additionally rejects
recognizable network filesystem paths and non-loopback serving. It is
application policy, not an OS firewall; a host firewall or offline VM
remains the stronger guarantee.
Can it OCR / convert Office files / shrink scanned PDFs? Not yet.
Those are roadmap phases, and ldf doctor will keep saying so until each
pipeline lands with tests. Lossless compression won't shrink scan-heavy
PDFs much (their bytes are already JPEG data) — and the report tells you
exactly that instead of pretending.
Why does remove-pages sometimes refuse? Your document has structures
(outlines, forms, internal links, tagged content) that this build cannot
yet rewrite safely. Refusing beats handing you a silently corrupted file.
Can I use it as a Python library? Yes —
docs/LIBRARY_API.md, with executed examples.
The full index is at docs/README.md. Highlights:
docs/TECHNICAL_REFERENCE.md (every
subsystem in one document) · docs/CLI.md (reference) ·
docs/GETTING_STARTED_WINDOWS.md
(task-oriented walkthrough) · docs/MACHINE_READINESS.md
(dated verification evidence) · docs/DEVELOPMENT.md
(contributing and the capability golden path).
pwsh -File scripts\bootstrap.ps1 # dev venv, locks, tests, lint, types
.venv\Scripts\python.exe -m pytest tests -q # current count: see docs/STATUS.md
.venv\Scripts\python.exe -m ruff check src tests scripts
.venv\Scripts\python.exe -m mypyTest fixtures are synthetic and generated by
tests/fixtures/make_fixtures.py — no third-party documents. The
capability rules are enforced by tests: a feature flips to "available" only
in the same change that lands its pipeline and tests, and the
documentation-consistency suite fails when these docs drift from shipped
reality. Start at docs/DEVELOPMENT.md.
Lossy compression presets, repair, OCR, Office↔PDF, PDF/A, and
advanced PDF→Markdown borderless/merged-cell table and semantic reconstruction,
editor, forms, encryption, redaction, signatures, compare, scanner
acquisition, full browser UI — phased plan in
docs/IMPLEMENTATION_PLAN.md, current truth
in docs/STATUS.md.
MIT. Dependency and external-engine licensing: docs/LICENSING.md.