Take a PDF apart and see everything inside it β the object model, page contents, text down to individual characters, images, vector paths, coordinates, fonts, annotations, forms, attachments and metadata. Every extracted element can be viewed, copied and downloaded. Several documents can be open at once.
Runs entirely on your machine. No accounts, no database, no cloud, no network calls.
- Why
- What it shows
- Install and run
- The interface
- Documentation
- Architecture
- Extraction output
- What is exposed
- What PyMuPDF cannot reach
- Multi-document behaviour
- HTTP API
- Use the core without the UI
- Configuration
- Development
- Known limitations
- Licence
PDF viewers show you the rendered result. Extraction libraries give you the text. Neither answers "what is actually in this file, and where?"
This tool does: the catalog and page tree, every indirect object, the operators that draw each page, every span of text with its font and colour and bounding box, every image with its filters and placements, every path with its coordinates β presented so each element can be inspected in place on the rendered page and taken out of the app for use elsewhere.
- Structure β catalog, page tree, structure tree (tagging), name trees,
named destinations, outline, object-type histogram, trailer and document
/ID, object-stream and cross-reference-stream detection. - Objects β load any object by xref number: dictionary entries, raw source, stream sizes, decoded stream, and every outgoing reference as a link.
- Content streams β the decompiled view: each operator with its operands, byte offset and meaning, including inline images.
- Text β page, block, line, span and character level, each with bounding box, font, size, colour and style flags; reading order preserved.
- Images β bytes in the original format, dimensions, DPI, colourspace, bit depth, filters, SMask; deduplicated per xref while every placement keeps its own bbox and matrix.
- Vector graphics β paths with full coordinates, stroke and fill colours, width, dashes, opacity, layer.
- Everything else β annotations, links, AcroForm fields and values, embedded files, optional content groups, document JavaScript, XMP and Info metadata, fonts with embedding and subset information, encryption method and decoded permissions.
- The gaps β a dedicated tab lists what exists in PDFs but cannot be reached through PyMuPDF, so nothing is silently missing.
Python 3.10 or newer. PyMuPDF ships wheels, so there is nothing to compile.
python3 -m venv .venv.venv/bin/pip install -r requirements.txt.venv/bin/python -m pdf_scopeOpen http://127.0.0.1:8000. Options: --host, --port, --reload.
make install and make run do the same.
Or in Docker, with no Python on the host:
docker build --load -t pdf-scope .
docker run --rm -p 127.0.0.1:8000:8000 pdf-scopemake docker-build and make docker-run wrap those. Keep the published port
on 127.0.0.1: the app has no authentication, so binding every interface with
-p 8000:8000 would let anyone who can reach the machine upload files and read
every open document. Details, volumes and environment variables:
docs/configuration.md.
Full walkthrough: docs/getting-started.md.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PDF Scope [Open PDFs] [Download everything] pool status β
βββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Documents β file.pdf Β· PDF 1.7 Β· 4 pages Β· 11 KB Β· sha256 β¦ Β· id β
β βββββββββββββ β [Document JSON] [.txt] [.md] [Images] [β¦] β
β β a.pdf β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β ready 4 β β Page β Structure β Objects β Metadata β Fonts β β¦ β
β βββββββββββββ€ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β b.pdf β β βΉ βΊ page 2 of 4 zoom βββ βblocks βlines βimages β
β β analyzing β β βββββββββββββββββββββββββββββ ββββββββββββββββββββββ β
β βββββββββββββ β β rendered page + overlays β β element details β β
β β βββββββββββββββββββββββββββββ ββββββββββββββββββββββ β
βββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Bounding boxes for text blocks, lines, spans, characters, images, drawings, annotations, links and form fields are drawn on the rendered page, each type toggleable, aligned at any zoom and on rotated pages. Click one for its details, the underlying object, and copy/download actions.
Every panel is documented in docs/ui-guide.md.
| Document | Contents |
|---|---|
| Getting started | Install, run, first document, tour |
| How a PDF is built | Objects, xrefs, page tree, resources, content streams, coordinate systems |
| Architecture | Process model, module map, request flows, design decisions |
| User interface guide | Every panel, overlay and action |
| HTTP API reference | All endpoints, parameters, status codes, examples |
| Extraction schema | Every output field, field by field |
| Core Python API | Scripting without the web layer |
| Coverage | Feature-by-feature: exposed, partial, or unreachable |
| Multi-document behaviour | Identity, isolation, concurrency, lifecycle |
| Configuration and operations | Variables, limits, performance, deployment |
| Troubleshooting | Symptoms, causes, fixes |
| Development | Code map, tests, common tasks, releases |
| Repository configuration | GitHub settings, CI and automation, going public |
Also: CHANGELOG Β· CONTRIBUTING Β· SECURITY Β· NOTICE Β· CODE_OF_CONDUCT
flowchart LR
U["Browser UI<br/>static HTML + vanilla JS"]
A["FastAPI app<br/>pdf_scope.web.app"]
R["Document registry<br/>ids, status, lifecycle"]
P["ExtractionPool<br/>ProcessPoolExecutor"]
W["Worker process<br/>pdf_scope.web.tasks"]
C["Extraction core<br/>pdf_scope.core"]
M["PyMuPDF / MuPDF"]
F[("Workspace on disk<br/>source.pdf, images/,<br/>cache/, exports/")]
U -- "upload PDFs" --> A
A --> R
R -- "store source" --> F
A -- "submit job" --> P
P --> W
W --> C
C --> M
W -- "images, page cache, bundles" --> F
W -- "JSON report" --> A
A -- "JSON, PNG renders, downloads" --> U
F -- "artifacts" --> A
Three choices worth knowing up front:
- The extraction core has no web dependency.
pdf_scope.coretakes a file path and returns JSON-serialisable data; it is importable, testable and scriptable on its own. - All PDF work runs in worker processes. PyMuPDF's documentation states it
"does not support running on multiple threads" and recommends
multiprocessing; extraction is CPU-bound anyway, so aProcessPoolExecutorkeeps the event loop free and caps concurrency. - No build step for the UI. One HTML file, one CSS file, one JS file, no npm, no CDN, works offline.
Details and trade-offs: docs/architecture.md.
Two JSON reports, both schema_version: "1.0":
- the document report β identity, file structure, object-model profile, encryption, metadata, structure trees, fonts, attachments, forms, optional content, JavaScript, page summaries, known limitations;
- the page report β page dictionary and boxes, matrices, resources, content streams with the operator listing, text at every granularity, images, drawings, annotations, links, widgets, xobjects, fonts.
Everything is JSON-native; binary payloads are written to the artifact directory and referenced by name. Field-by-field reference: docs/schema.md.
All coordinates are PDF points in PyMuPDF space: origin top-left of
page.rect, y growing downwards, page rotation already applied. Each page
report carries the matrices to convert to the PDF's own bottom-left space β
see coordinate systems.
| Area | Highlights |
|---|---|
| Document | PDF version, page/chapter/version counts, trailer, /ID, catalog xref, page mode and layout, /MarkInfo, language, page labels, repaired-on-open, fast web view |
| Object model | xref slots, per-/Type histogram, stream counts, object streams, cross-reference streams; any object by number with entries, source, stream sizes, decoded stream and references |
| Pages | All five boxes plus page.rect, /Rotate, four matrices, page dictionary, /Resources per category (direct and indirect) |
| Content streams | Per-stream xrefs, filters, raw/decoded sizes, decoded text, and an ordered operator listing with operands, offsets and descriptions |
| Tables | Detected from ruling lines and text alignment: bounding box, row and column counts, header names, cell rectangles, cell text and Markdown. Labelled as a detection, because PDF has no table object |
| Document overview | Producer, creator, dates, page sizes, and counts of text, images, vector paths, tables, annotations, links and form fields across the document |
| Text | Page, block, line, span, character; bbox, font, size, colour, alpha, ascender/descender, font and style flags, writing mode, direction |
| Fonts | Base name, subtype, embedded flag and extension, subset prefix, encoding, resource name, /FontDescriptor and /ToUnicode xrefs, usage per page |
| Images | Original bytes, pixel size, DPI, colourspace, bit depth, filters, SMask, per-placement bbox and matrix, inline images |
| Vector graphics | Full path items, stroke and fill, width, dashes, caps/joins, opacity, layer, clip rect |
| Tagging | /StructTreeRoot walk with tags, /Alt, /ActualText, language, page links |
| Navigation | Outline with destinations, named destinations, name trees |
| Other | Annotations of every type, links, AcroForm fields and values, embedded files, optional content, document JavaScript, XMP and Info metadata, encryption method and decoded permissions |
Complete matrix, including partial and unreachable features: docs/coverage.md.
Reported in every document report, in the UI's Not extractable tab, and in docs/coverage.md:
- Digital signatures β widgets and
/SigFlagsare shown; signatures are not validated and the PKCS#7 chain is not decoded. - Encryption internals β method and permissions are shown; the
/Encryptdictionary's parameters and keys are not exposed. - Font programs β downloadable as raw streams, but glyph outlines, CMaps
and
/ToUnicodemappings are not decoded. - Scanned pages β no OCR, so image-only pages have no text; the tool says so instead of returning an empty result.
- Original byte offsets β MuPDF re-serialises object source, so file offsets, the classic xref layout and incremental update sections are not reported; object-stream and xref-stream usage is detected by object type.
- Content-stream provenance β streams are concatenated and decoded by MuPDF; unsupported or damaged filter chains are reported per stream.
- Identity β every upload gets a fresh UUID. The same file twice, or two files with the same name, never collide; identical bytes are reported as duplicates, not merged.
- Isolation β artifacts live in
<workspace>/<document_id>/(source.pdf,images/,cache/,exports/), and each extraction opens its own PyMuPDF document. - Concurrency β analyses run in parallel in the process pool
(
PDF_SCOPE_WORKERS, defaultmin(4, CPU)), with a semaphore queueing the rest. The event loop never blocks. - Failure containment β a corrupt or locked file is marked in the list; every other document keeps working.
- Lifecycle β closing a document deletes its directory. The workspace is emptied at startup, so restart is a clean slate and nothing is restored.
- Limits β 25 open documents, 512 MB per upload, both configurable.
Details: docs/multi-document.md.
| Method | Path | Purpose |
|---|---|---|
POST |
/api/documents |
Upload one or more PDFs |
GET |
/api/documents |
List open documents, limits, pool state |
GET |
/api/documents/{id} |
Summary plus the document report |
GET |
/api/documents/{id}/summary?offset=&limit= |
Per-page and total content counts, including detected tables |
POST |
/api/documents/{id}/unlock |
Retry an encrypted document with a password |
DELETE |
/api/documents/{id} |
Close and delete artifacts |
GET |
/api/documents/{id}/report.json |
Download the document report |
GET |
/api/documents/{id}/pages/{n} |
Page report |
GET |
/api/documents/{id}/pages/{n}/report.json |
Download the page report |
GET |
/api/documents/{id}/pages/{n}/render.png?dpi=&clip= |
Rendered page, or one rectangle of it (X-Render-Info carries the scale) |
GET |
/api/documents/{id}/pages/{n}/drawings?offset=&limit= |
Window of the page's vector paths, with the real total |
GET |
/api/documents/{id}/pages/{n}/operators?offset=&limit= |
Window of the operator listing, with the exact total |
GET |
/api/documents/{id}/pages/{n}/text?fmt=txt|md |
Page text |
GET |
/api/documents/{id}/pages/{n}/content-stream?raw= |
Decoded or raw content stream |
GET |
/api/documents/{id}/objects/{xref} |
Object dictionary, stream info, references |
GET |
/api/documents/{id}/objects/{xref}/stream?raw= |
Object stream bytes |
GET |
/api/documents/{id}/images/{file} |
One extracted image, in the PDF's own format |
GET |
/api/documents/{id}/images/{xref}/preview.png?max_side= |
Same pixels as PNG, for formats browsers cannot display |
GET |
/api/documents/{id}/images.zip |
Every image of the document |
GET |
/api/documents/{id}/attachments/{index} |
One embedded file |
GET |
/api/documents/{id}/text?fmt=txt|md |
Whole-document text |
GET |
/api/documents/{id}/export.zip |
Complete extraction bundle |
GET |
/api/export/all.zip |
Complete extraction of every open document |
GET |
/api/status |
Schema version, open documents, pool state |
Interactive docs at /docs. Full reference with examples:
docs/api.md.
from pdf_scope.core import analyze_document, analyze_page, dumps
report = analyze_document("contract.pdf")
print(report["file"]["pdf_version"], report["file"]["page_count"])
print([f["base_font"] for f in report["fonts"]["items"]])
page = analyze_page("contract.pdf", 0, image_dir="out/images")
for block in page["text"]["structure"]["blocks"]:
if block["type"] == "text":
print(block["bbox"], block["lines"][0]["spans"][0]["text"])
open("page1.json", "w").write(dumps(page))Also available: build_document_bundle, collect_document_json,
document_text, describe_object, render_page_png,
page_content_stream_bytes. See docs/core-api.md.
| Variable | Default | Effect |
|---|---|---|
PDF_SCOPE_WORKSPACE |
./.workspace |
Where artifacts live; emptied on every start |
PDF_SCOPE_WORKERS |
min(4, CPU count) |
Worker processes, i.e. simultaneous extractions |
PDF_SCOPE_MAX_UPLOAD_MB |
512 |
Per-file upload ceiling |
More, including built-in limits, performance figures and deployment notes: docs/configuration.md.
.venv/bin/pip install -r requirements-dev.txt.venv/bin/python -m pytest.venv/bin/ruff check . && .venv/bin/ruff format --check .Tests build their fixture PDFs programmatically β no binaries in the repository β and cover text, bounding boxes, fonts, images, drawings, annotations, forms, metadata, the object model, the content-stream parser, rotated pages, scanned pages, encrypted and corrupt files, export bundles, and two documents processed concurrently without cross-contamination.
make check runs lint and tests together, make audit checks the pinned
dependencies for known vulnerabilities, and make hooks installs the optional
pre-commit hooks. CI additionally builds the container image and runs the smoke
test inside it. See CONTRIBUTING.md,
docs/development.md and
docs/repository.md.
- No OCR: scanned pages have no text layer, and that is reported rather than hidden.
- Object-model scans stop at 200 000 objects; structure and name-tree walks at 5 000 nodes per tree. Truncation is always flagged.
- Decoded content streams are inlined up to 200 000 characters; the full stream is always downloadable.
- Long lists are read in windows, not all at once: a page report inlines the first
5 000 vector paths and 5 000 operators, and the rest is reached page by page
through the
drawingsandoperatorsendpoints. Testing on CAD sheets found 265 507 paths and 5 071 999 operators on a single page, so nothing here is hypothetical. - Font aggregation scans up to 2 000 pages.
- Whole-document exports are built before the download starts, so a very large document takes a while.
- The page view scrolls through the whole document, but pages are loaded a few at a time; scrolling very fast shows placeholders until the renders arrive.
- State is in memory: restarting the server closes every document.
- Everything under What PyMuPDF cannot reach.
This repository's own source is MIT β see LICENSE.
It depends on PyMuPDF, which is dual licensed under the GNU AGPL v3 or an Artifex commercial licence. Running it locally is unaffected, but distributing a combined work or hosting it for other users brings the AGPL's obligations into play unless you hold a commercial licence. Details and the full dependency licence list: NOTICE.md.