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.
autoleaves 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.forcediscards 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)
| File | Size | Uploaded | |
|---|---|---|---|
| pdf2context-0.1.0.tar.gz | 30.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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