brewdoc
Document to Markdown for agent systems: PDF, Word, PPTX presentations, spreadsheets and static HTML in, one Markdown file plus a JSON receipt out. No models, no OCR, no network, deterministic (same file, same bytes). The receipt says what was rendered, what was dropped and what the route structurally cannot carry, so an agent never mistakes a silent gap for "the document does not say so".
Install
| channel | command |
|---|---|
| pip | pip install brewdoc==0.2.0 |
| uvx | uvx brewdoc==0.2.0 file.pdf |
| uv tool | uv tool install brewdoc==0.2.0 |
| Docker | RUN pip install brewdoc==0.2.0 |
Alternative, straight from git (no PyPI round-trip):
uvx --from git+https://github.com/kochetkov-ma/brewdoc@v0.2.0 brewdoc file.pdf
Requires Python >= 3.12. Base runtime dependencies: pdfplumber (pulls pypdfium2, Pillow,
cryptography) and python-calamine. HTML requires CPython 3.12-3.14 and the native Lexbor
parser from this exact conditional dependency:
selectolax==0.4.13; python_version < '3.15' and platform_python_implementation == 'CPython'
Other runtimes retain the .html route and Python API but explicitly refuse HTML. On supported
CPython, a missing or broken parser is an environment failure and --self-check exits 1.
On an unsupported HTML runtime, self-check verifies HTML refusal and checks all other formats.
Usage
brewdoc file.pdf --out file.md
brewdoc slides.pptx --out slides.md
brewdoc page.html --out page.md
brewdoc file.xlsx # no --out: receipt line, then the Markdown, on stdout
brewdoc file.xlsx --sheet Calc --sheet Inputs
brewdoc file.xlsm --artifact formula/sheet/000002=formulas.json
brewdoc file.xlsm --artifact vba/project/000001=vbaProject.bin
brewdoc --self-check # renders synthetic fixtures twice, compares sha256, exit 0 when green
--sheet is repeatable. Names are exact and case-sensitive. Output follows caller order while
keeping each sheet's full-workbook ordinal. --artifact is also repeatable. Use keys listed in the
Markdown artifact table. Both options are workbook-only. brewdoc validates every request and
output collision before writing.
Every successful document uses brewdoc.markdown/2. A quoted source title is followed by Metadata,
Artifacts, Known omissions, Contents, then anchored content units. PDF keys are page/000001,
workbook keys are sheet/000001, DOCX keys are chapter/000001, PPTX keys are slide/000001,
and HTML has one chapter/000001.
Metadata contains deterministic source identity, selection, content hashes, conversion tallies, and
artifact capabilities. It excludes paths, timestamps, permissions, host data, and unsupported author
metadata.
One JSON receipt line always goes to stdout first (wrapped here, unit_keys omitted):
{"artifacts": [], "broken_ligature_words": 0, "columns_split": 1,
"dropped": {"cid_survivors": 0, "control_chars": 0, "ligatures": 0, "nbsp": 0,
"non_ascii_replaced": 0, "page_numbers": 3, "pua_glyphs": 0,
"running_heads": 0, "soft_hyphens": 0},
"file_ok": true, "markdown_schema": "brewdoc.markdown/2",
"not_carried": ["images, figures and the text drawn inside them",
"a table that spans a page break",
"text rotated out of the horizontal reading order"],
"out": "file.md",
"reason": "pdf rendered: 7 pages, 1 tables, 26 text regions, 1 column splits",
"receipt_schema": "brewdoc.receipt/1",
"route": "pdf", "source": "nist-sp800-145.pdf", "tables": 1,
"text_regions": 26, "unit_kind": "page", "units": 7}
| key | meaning |
|---|---|
file_ok, reason |
false when the file was refused (no text layer, unreadable, unsupported suffix); reason names why, or summarises what was rendered |
route |
which reader ran: pdf, doc, presentation, sheet or html |
source |
the input file name, without its directory |
unit_kind, units |
the route's content unit and how many were rendered; one pair for every format. page for PDF, chapter for DOCX and HTML, slide for PPTX, sheet for a workbook, and none on a suffix brewdoc does not read |
tables, text_regions, columns_split |
what else was rendered |
receipt_schema |
the receipt key contract; brewdoc.receipt/1 replaced the per-format pages and sheets counters |
broken_ligature_words |
words with a ligature the font mapped to a stray code point, folded to ASCII, not repaired |
dropped |
sanitised items removed on purpose, counted by kind (running heads, page numbers, ...) |
not_carried |
what this route structurally cannot represent - read it before concluding a fact is absent |
out |
the Markdown path, or null when it went to stdout |
markdown_schema, unit_keys |
the contract and ordered full-source content keys; success only |
artifacts |
available keyed formula or opaque VBA outputs and requested output paths; success only |
selected_sheets |
the --sheet names in caller order; present only when sheets were selected |
Paths resolve against the current working directory; --out creates parent directories.
Exit code is non-zero on refusal; the receipt line is still printed.
Formats
| in | reader | notes |
|---|---|---|
.pdf |
pdfplumber | needs a text layer; route chosen per region, never per page |
.docx |
stdlib zip + XML | paragraphs and tables |
.pptx |
stdlib zip + XML | slides in declared order; text, tables, notes and picture metadata |
.xlsx .xlsm .xls .xlsb .ods |
python-calamine | selected sheets keep full-source ordinals; numbers as stored (85.0), dates as ISO days |
.html |
selectolax Lexbor | full static body or fragment, complete tables and source-declared controls; CPython 3.12-3.14 |
.xlsx and .xlsm expose original OOXML formulas as one deterministic JSON artifact per
formula-bearing selected sheet. Markdown still contains cached values. .xlsm and .xlsb can expose
one complete related vbaProject.bin as opaque bytes. brewdoc does not execute macros, decompile VBA,
report source-module counts, evaluate formulas, or infer dependency graphs.
HTML reads local strict UTF-8, with an optional BOM. It preserves meaningful Unicode, navigation, main content, sidebars and footers in recovered DOM order. It never fetches links or resources, executes scripts, submits forms or infers interactive values. Links use a restricted scheme policy; form actions remain inert metadata. Inline code outside tables becomes a literal fenced block; surrounding emphasis and links remain balanced. Safe links in literal code and around code/images retain destination records without changing the source literals. Table-cell code uses labelled escaped text. Quotes and list items containing standalone child blocks use explicit nesting boundaries in place of visual Markdown containers. Plain list paragraphs retain their item marker and continuation indentation. See HTML scope and limits.
The public Python API exposes HTML and presentation rendering and generic workbook artifact access:
import brewdoc
markdown, tally = brewdoc.render_html("page.html")
code, receipt, markdown = brewdoc.run("page.html", out="page.md")
markdown, tally = brewdoc.render_presentation("slides.pptx")
refs = brewdoc.list_book_artifacts("book.xlsm", sheets=("Calc",))
payload = brewdoc.read_book_artifact("book.xlsm", refs[0].key, sheets=("Calc",))
code, receipt, markdown = brewdoc.run(
"book.xlsm", out="book.md", sheets=("Calc",),
artifact_outputs={refs[0].key: "formulas.json"},
)
artifact_outputs maps artifact keys to paths. The CLI builds it from repeated --artifact KEY=PATH.
Planned formats, deferred ones and the never-list: FORMATS.md.
A planned format is refused by name today. The adapter contract and the ordered checklist for
adding one are in docs/architecture.md.
PDF regions, in the order tried:
| region looks like | rendered as |
|---|---|
| ruled on all sides | Markdown table from the drawn ruling |
| ruling unfinished (cells never closed) | re-read on the grid the ruling implies, kept only when it carries more text than the drawn ruling did |
| ruled across only (head and foot rules) | column grid from the rule segment ends |
| unruled, tabular band | layout-preserved text |
unruled, captioned Table N |
the band is bounded first (caption to next blank band), then read as a table |
| two-column prose | split at the gutter (relative density on the character x-histogram), left then right |
| plain prose | lines |
What it does not do
| gap | behaviour |
|---|---|
| OCR | a PDF with no text layer is refused by name, never rendered empty |
| PDF and DOCX images, figures, text drawn inside them | not carried; listed in not_carried |
| PPTX pictures | represented by deterministic name, size, alt text, caption, source and byte-count placeholders; pixels are never emitted |
| PPTX charts, SmartArt, audio, video and embedded objects | not carried; listed in not_carried |
| a table spanning a page break | columns recovered, wrapped rows stay separate rows |
| text rotated out of horizontal reading order | not carried |
| sub/superscripts | folded back into their line (NH4H2PO4, not NHHPO over 4 2 4) |
| broken font CMaps (ligatures mapped to wrong code points) | counted (broken_ligature_words), folded to ASCII, never guessed back |
| formula calculation | cached values in Markdown; passive source formulas only for .xlsx and .xlsm |
| readable VBA source | unavailable; .xlsm and .xlsb preserve a related project only as opaque bytes |
Determinism
No timestamps or unordered iteration. Two renders of one file have the same sha256;
--self-check pins it. PDF, DOCX, PPTX and workbook output remains ASCII. HTML Markdown uses
UTF-8 without BOM and LF newlines, with the same bytes in the API, file output and CLI stdout.
Receipt JSON keeps ASCII escaping.
Size and speed
These measurements cover the existing PDF/workbook baseline before the HTML parser addition. They do not measure HTML conversion or the new installed footprint.
| measure | value |
|---|---|
| installed footprint | ~45 MB (pdfplumber, pypdfium2, Pillow, cryptography, python-calamine) |
| import | 0.08 s |
| 130-page PDF | ~5.5 s |
| 14-sheet xlsx | ~0.9 s |
Benchmarks
Browse the corpus catalog, follow the measurement method, and inspect the full-run results. Completed runs include actual outputs and known failures; capture completion does not mean content quality passed.
Releasing
Tag-triggered, see RELEASING.md.
License
MIT, see LICENSE.
Metadata
Release files for brewdoc 0.2.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 | |
|---|---|---|---|
| brewdoc-0.2.0.tar.gz | 5.4 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| brewdoc-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 5.5 MB
Release files / brewdoc-0.2.0.tar.gz
| Download URL | brewdoc-0.2.0.tar.gz |
|---|---|
| Size | 5.4 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7085c35212568b92e4a2cea465f723ee3722abb70f4885e12c6f0d8231d9dcb2
|
|
BLAKE2b-256 checksum How to use checksums |
bf2f61e378b9d0946a8268fe979a2db4501ed9e598cd961f62e3876e88a6c88a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 11, 2026.
Transparency logRelease files / brewdoc-0.2.0-py3-none-any.whl
| Download URL | brewdoc-0.2.0-py3-none-any.whl |
|---|---|
| Size | 63.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
234a5c83497ac9f70e1530c0e36920657df93220539d3935fa632e3c7fe189de
|
|
BLAKE2b-256 checksum How to use checksums |
a1c78f64c7ed8eccf198f6afa9ee3be5bd8b101f4449f89034d49647bacb288c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 11, 2026.
Transparency log