Skip to main content

English | 日本語

pdf2context

Turn multiple PDFs into one corpus. What you hand to an AI is the PDF and the Markdown.

Pass -o an output path without an extension. For /hoge/context/merged, the same directory gets these three files:

  • /hoge/context/merged.pdf — the merged PDF. It is the authoritative copy of how the pages look.
  • /hoge/context/merged.md — text with a source citation on every page.
  • /hoge/context/merged.json — a record for reusing sources from the previous run. It stores the source, the SHA-256, and the page text.

Hand merged.pdf and merged.md to the AI. merged.json is for this tool. Do not hand it to the AI.

Each page heading carries both the page number in the merged file and the page number in the original file. The body is wrapped in a code fence.

### contract.pdf — source p.4 — merged p.16

Requirements

Merging PDFs and extracting text requires these commands:

brew install qpdf poppler

OCR is performed by OCRmyPDF. uv run installs that package. Pages are rasterized with pypdfium2, which is installed alongside it, so Ghostscript is not required.

The Tesseract binary is required only when you use OCR. brew install tesseract includes the English trained data. The full language pack tesseract-lang is not required. To read Japanese, add only jpn.traineddata.

brew install tesseract
curl -L "https://github.com/tesseract-ocr/tessdata/raw/main/jpn.traineddata" \
  -o "$(brew --prefix)/share/tessdata/jpn.traineddata"

Usage

Run this from the repository root. uv run installs the package into a virtual environment and starts it as the pdf2context command. qpdf and pdftotext still have to be on PATH, as described in Requirements. Without a clone, uvx pdf2context runs the package via PyPI.

uv run pdf2context '/hoge/pdf_a/*.pdf' -o /hoge/context/merged
uv run pdf2context '/hoge/pdf_a/*.pdf' -o /hoge/context/pdf_a
uv run pdf2context docs/ -o output/merged
uv run pdf2context --ocr auto --ocr-lang jpn+eng shots/*.pdf -o output/merged
uv run pdf2context --ocr force --update replace shots/*.pdf -o output/merged
uv run pdf2context --dry-run --prune shots/*.pdf -o output/merged
uv run pdf2context --lang ja docs/ -o output/merged

For /hoge/context/pdf_a, the file names are pdf_a.pdf, pdf_a.md, and pdf_a.json. If the path ends in .pdf, .md, or .json, that extension is stripped and the same rule applies.

A directory argument merges the PDFs directly inside it, in code-point order of the file names. Globs keep the order of the arguments; matched files are ordered by path name. Recursive globs such as **/*.pdf work. 2.pdf sorts after 10.pdf. To fix the order, zero-pad names such as 01_, or pass the files explicitly as arguments.

If the same source name appears more than once with the same bytes, later copies are dropped. If several inputs share a source name but differ in bytes, the run errors. The .pdf, .md, and .json about to be written are excluded from the input.

Reruns follow --update. The default, changed, reuses a source whose source name, bytes, and OCR settings match the previous run, and rebuilds only sources whose bytes changed. replace rebuilds the sources passed this time. keep leaves an existing source name on its previous pages even when the bytes changed. Source names absent from the input stay. --prune drops them. When the result matches the previous run, files are not rewritten, except under replace.

--lang is en or ja. It selects the language of errors, help, and progress logs. The first run defaults to en. A later run, when --lang is omitted, uses options.language from the previous JSON. Passing --lang with a different value rewrites only that JSON field. The PDF and the Markdown stay as they are. --dry-run prints language: ja -> en when that field would change. Dry-run status names stay in English: added, replaced, reused, kept, retained, pruned, and same-content.

The Markdown contains an English ## Notice section. It does not follow --lang.

A previous JSON without options.language, or with a value other than en or ja, is an error. The message names the line where "language": "en" or "language": "ja" goes, inside options. --update replace and --dry-run stop as well.

--dry-run writes no files. It prints the outcome for each source name, then the pairs that have different names and the same bytes. That same-content list is the set before --prune is applied. The command still exits successfully when such pairs exist. A real run does not stop because of them. changed and keep with no previous JSON, and an empty input, fail the same way as a real run.

Symbolic links and directories are not replaced. During a run, .{name}.pdf2context.lock is placed at the output location, and a concurrent run is refused. If replacement fails, the previous output is restored.

Each external command is limited to --timeout seconds. The default is 600.

OCR

--ocr is off, auto, or force. The default is off. The default language is jpn+eng.

OCRmyPDF writes the recognized characters back into the PDF as an invisible text layer. The page looks the same, while search, copy, and a later pdftotext can read those characters. Skew is not corrected. The output is not converted to PDF/A. If the page count changes after OCR, the run stops.

  • auto leaves any page from which at least one character can be extracted as it is, and adds a text layer only to pages with no characters. A page that is only a page number, or an image with a single heading line, is left out entirely.
  • force discards existing text and OCRs every page again. Use it for garbled text, or for an image whose only text is a caption. Page images are regenerated.

Text extraction

Each page is extracted with pdftotext -f N -l N -nopgbrk -layout. The whole file is not split on form-feed characters. Pass --no-layout to drop -layout.

The JSON is the record of the corpus. When the previous JSON is missing and only the PDF or the Markdown remains, changed and keep stop. To rebuild, pass --update replace.

What this tool does not produce

This tool does not restore tables or headings into Markdown structure. It also does not restore author, timestamp, reply, or quote relationships from a Slack screenshot. OCR recovers characters.

Encrypted PDFs are rejected. Remove the password before passing the file.

Development

uv run python -m unittest discover -s tests -v

License

Apache License 2.0

Metadata

Release files for pdf2context 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pdf2context 0.1.0
File Size Uploaded
pdf2context-0.1.0.tar.gz 30.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pdf2context 0.1.0
File Interpreter ABI Platform
pdf2context-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 55.4 kB

Release files / pdf2context-0.1.0.tar.gz

Download URL pdf2context-0.1.0.tar.gz
Size 30.9 kB
Tags Source
SHA-256 checksum
How to use checksums
f3243e054aa07163f875f2ae8b64f7c571d27e2741dcb625ba0f1fdb35c68cf4
BLAKE2b-256 checksum
How to use checksums
44baf102209cc1c334fddc89307e1a85e38bf7bb404913202456573ac448ebd2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release files / pdf2context-0.1.0-py3-none-any.whl

Download URL pdf2context-0.1.0-py3-none-any.whl
Size 24.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
353606c246556df5b76e9295beef40ede2a414935abdc357500029ea555279af
BLAKE2b-256 checksum
How to use checksums
61e812714f6dc63b0f708c58db0f55aa54be06e6f20c82c9b10432d1e89ca5db
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page