{"slug": "show-hn-emboss-a-python-pdf-engine-that-keeps-documents-as-structured-data", "title": "Show HN: Emboss – a Python PDF engine that keeps documents as structured data", "summary": "Emboss, a new Python PDF engine, generates deterministic, byte-identical PDFs from structured data or Markdown without a browser, ensuring hash-verifiable and diffable output for CI workflows. The engine features native Knuth-Plass typesetting, PDF/UA accessibility by default, and embeds source data at the element level, with over 1,900 tests backing its closed-loop lifecycle from generation to verification.", "body_md": "Turn structured or LLM-generated content into accessible, print-ready PDFs. Describe a document once, as a declarative spec or as Markdown, and Emboss handles typesetting, PDF/UA tagging, and the full PDF structure. Output is deterministic and byte-identical across runs, so every PDF is hash-verifiable and diffable in CI.\n\n``` python\nfrom emboss import Document\n\ndoc = Document(title=\"Q3 Report\", style=\"finance\")\ndoc.heading(\"Revenue Analysis\", level=1)\ndoc.paragraph(\"Revenue increased 12% year over year.\")\ndoc.table(headers=[\"Region\", \"Q3\"], rows=[[\"North America\", \"$2.4M\"]])\ndoc.save(\"report.pdf\")\n```\n\nNo coordinates. No manual page-break handling. No separate accessibility pass.\n\nEmboss treats a PDF as structured, self-describing data with a deterministic, accessible, verifiable lifecycle, not as final ink. Six properties set it apart:\n\n-\n**Native typesetting, no browser.** Emboss is a real typesetting engine (Knuth-Plass line breaking, true font metrics), not HTML-to-PDF like WeasyPrint or Prince, and not headless Chrome. That is why output is deterministic and why there is no Chromium or Node attack surface. It runs anywhere, including locked-down servers. -\n**Pure-Python core, one hard dependency.** Only`fonttools`\n\nis required; pydantic, pikepdf, cryptography, and the MCP SDK are optional extras. Easy to vendor, audit, and deploy in restricted environments. -\n**Byte-identical output.** No timestamps, no random ids; the file`/ID`\n\nis derived from content. The same document produces the same bytes on every machine, so a filing can be hash-pinned and a change can be code-reviewed and diffed in CI. Almost no other PDF toolchain can say this. -\n**Accessibility by default.** The PDF/UA structure tree is derived from the same semantic model that drives layout, so tags cannot drift from content. This matters now that the EU Accessibility Act (June 2025) makes accessible PDFs mandatory for many businesses. Conformance is checked against the real veraPDF validator in CI, not mocked. -\n**Data embedded at the element level, a table** A table or chart can carry its own source CSV inside the PDF as a per-element attachment, so the figure a reader sees and the data an analyst needs are the same file and can never drift apart. Every other PDF flattens a table to ink; Emboss keeps the data live inside the document.*is*its spreadsheet. -\n**Self-describing and node-keyed, one closed loop.** Each document embeds its own spec, a per-character text index, and a stable id on every block. Generate, review, edit, sign, verify, and reproduce all key off that same structure, so the document stays coherent and machine-actionable through its whole lifecycle. Competitors give you one stage; Emboss gives you the loop, backed by 1,900+ tests.\n\nThe one-line version: **an Emboss document is structured data you can also read.**\n\n[What Makes Emboss Different](#what-makes-emboss-different)[For Decision-Makers](#for-decision-makers)[Built for Enterprise](#built-for-enterprise)[How Emboss Compares](#how-emboss-compares)[Why Emboss](#why-emboss)[Installation](#installation)[Quick Start](#quick-start)[EmbossSpec Format](#embossspec-format)[Document Elements](#document-elements)[Style Presets](#style-presets)[BrandKit](#brandkit)[Typography Engine](#typography-engine)[Layout System](#layout-system)[Accessibility and Conformance (PDF/UA)](#accessibility-and-conformance-pdfua)[Self-Describing PDFs](#self-describing-pdfs)[Document Diff and Redline](#document-diff-and-redline)[Review Round-Trip](#review-round-trip)[MCP Server](#mcp-server)[Templates](#templates)[Executive Decks](#executive-decks)[Domain Features](#domain-features)[Cross-References and Numbering](#cross-references-and-numbering)[SVG Embedding](#svg-embedding)[Diagrams](#diagrams)[Multi-Column Layout](#multi-column-layout)[Math Notation](#math-notation)[Code Blocks](#code-blocks)[Charts](#charts)[PDF/A Archival Output](#pdfa-archival-output)[CMYK and Print Production](#cmyk-and-print-production)[Digital Signatures](#digital-signatures)[Incremental Amendment](#incremental-amendment)[Redaction](#redaction)[Validation](#validation)[Adapters](#adapters)[API Reference](#api-reference)[Performance](#performance)[Development](#development)[Architecture](#architecture)[Roadmap](#roadmap)[License](#license)\n\n**What it is.** A Python engine that turns structured or LLM-generated content into accessible, print-ready PDFs, and treats the resulting PDF as structured data you can also read, not as final ink.\n\n**The problem it solves.** Enterprises generate documents at scale (reports, invoices, filings, contracts) but lose the structure the moment they become PDFs: the data behind a table is gone, accessibility is a costly afterthought, edits mean regeneration, and \"what changed / who signed / is this the real version\" become manual audits. Emboss keeps the structure inside the document for its entire life.\n\n| Outcome | How Emboss delivers it |\n|---|---|\nTrust & audit |\nByte-identical deterministic output (hash-pinnable filings), self-verifying provenance (`reproduce` ), append-only signatures that flag anything added but unsigned, and tables that refuse to render if the totals do not add up. |\nLower cost |\nLLM edits are node-scoped: changing one paragraph in a 50-page report costs a paragraph's tokens, not the report's. |\nData never drifts |\nTables and charts embed their own source CSV; the figure an executive sees and the data an analyst verifies are the same file, the same version. |\nCompliance built in |\nPDF/UA accessibility, PDF/A archival, Factur-X/ZUGFeRD e-invoicing, and PAdES/eIDAS signatures, all in one engine (see the table below). |\nAI-native distribution |\nAn MCP server makes every document callable from Claude: generate, query with certainty from the embedded structure, and edit, with explicit \"not in this document\" grounding so it never fabricates. |\n\n| Standard | What it is for | Regulatory driver |\n|---|---|---|\nPDF/UA-1 |\nAccessibility (tagged, assistive-tech ready) | EU Accessibility Act (June 2025), US Section 508, EN 301 549 |\nPDF/A-2b, PDF/A-3b |\nArchival / long-term retention | Records retention, filing ingest |\nFactur-X / ZUGFeRD (EN 16931) |\nElectronic invoicing (XML inside the PDF) | France B2B mandate (Sept 2026), EU e-invoicing |\nPAdES B-B / B-T (ETSI EN 319 142) |\nLegally recognized e-signatures | eIDAS (EU) |\nPDF/X-4 (ISO 15930-7) |\nPrint / prepress output | Commercial print |\nWTPDF 1.0 (Reuse) |\nClean re-extraction for RAG / reuse | AI and data pipelines |\n\nEvery conformance claim is checkable: `emboss verify out.pdf --conformance ua1`\n\n(also `2b`\n\n, `3b`\n\n) runs the real veraPDF validator, and the CI conformance job runs it on every push, not a mock.\n\n| Capability | Emboss | Typical PDF tools (ReportLab, WeasyPrint, Prince, headless Chrome) |\n|---|---|---|\n| Rendering | Native typesetting engine, no browser | HTML-to-PDF or a browser engine |\n| Determinism | Byte-identical, hash-verifiable | Timestamps / random ids; not guaranteed |\n| Accessibility | PDF/UA tagged by default, veraPDF-checked | Manual or absent |\n| Data in the file | Table carries its own CSV; spec + text index embedded | Table flattened to ink; data gone |\n| Edit after render | Node-scoped patch, structure preserved | Regenerate, or overlay hacks |\n| Review round-trip | Annotation resolves to node + character range | No structure to resolve against |\n| Signatures | PKCS#7, DocMDP, PAdES, append-only coverage check | Basic or none |\n| AI integration | First-class MCP server, grounded answers | None |\n| Dependencies | Pure Python, `fonttools` only (rest optional) |\nNative libs / a browser runtime |\n\n**Dependency security.** The only hard dependency is `fonttools`\n\n; `pydantic`\n\n, `pikepdf`\n\n, `cryptography`\n\n, and `mcp`\n\nare optional extras, and Emboss never imports two of their own transitive dependencies (`pillow`\n\n, `starlette`\n\n) directly. Because those two set no upper bound, Emboss pins a known-patched floor on them in its own `[project.optional-dependencies]`\n\n(`pillow>=12.3.0`\n\n, `starlette>=1.3.1`\n\n), so `pip install emboss-pdf[verify]`\n\n/ `[mcp]`\n\n/ `[dev]`\n\n/ `[all]`\n\nresolves to a patched transitive chain rather than whatever floor those packages declare. `pip-audit`\n\nagainst the full dependency set (base plus every extra) reports no known vulnerabilities.\n\nEmboss is built around guarantees that matter once a PDF leaves a notebook and enters a filing, an audit trail, or a distribution pipeline -- not marketing claims, but specific, checkable code paths.\n\n**Deterministic, hash-verifiable output.** The same`Document`\n\nalways renders to the same bytes: no timestamps, no random identifiers, the file`/ID`\n\nderived from content. Two runs, two machines, one hash -- renders are byte-diffable in CI.**PDF/UA tagged by default, with real conformance evidence.** Every document ships a complete structure tree, and`emboss verify out.pdf --conformance 2b`\n\n(also`ua1`\n\n,`3b`\n\n) shells out to the actual veraPDF CLI and reports its verdict. This is not a simulated check: the CI conformance job installs the real veraPDF binary and runs the test suite against it on every push (`.github/workflows/ci.yml`\n\n).**Self-describing PDFs.**`doc.render(embed_spec=True)`\n\nembeds the document's own EmbossSpec JSON, its node-keyed layout map, and a reflowable Markdown twin as real`/AF`\n\nfile attachments.`Document.from_pdf()`\n\nreconstructs an equivalent document from those attachments, and falls back to walking the PDF/UA structure tree -- recovering headings, paragraphs, tables, and lists in order -- if the attachments were stripped.**Stable node ids and a queryable layout map.** Every block gets a deterministic id and a page/bounding-box entry in`doc.layout_map()`\n\n: the foundation for auditing, redlining, and programmatic review of what landed where on the page.**Document diff and redline.**`diff_documents()`\n\nmatches blocks between two documents by node id and classifies each as added, removed, or changed, with a word-level diff on text-bearing blocks;`render_redline()`\n\nturns that into a real PDF with struck-through deletions, underlined insertions, margin change-bars on added content, and a summary page. Also available as`emboss diff old.pdf new.pdf -o redline.pdf`\n\nfrom the command line.**A reproducibility manifest.**`doc.render(manifest=True)`\n\nattaches a deterministic`emboss-manifest.json`\n\n: the spec's sha256, the Emboss version, every embedded font's sha256, and any non-default render options.`emboss.reproduce()`\n\n(and`emboss reproduce report.pdf`\n\n) closes the loop: recover the document, re-render it, and structurally verify the two PDFs agree.**Redaction by construction.**`Document.redact(rules)`\n\nmatches whole blocks by node id, regex, predicate, or type and removes or replaces them*before*layout, so redacted text never reaches a content stream -- not a black box painted over extractable text.**DocMDP certification signatures.**`sign_pdf(..., certify=True, docmdp_permission=...)`\n\nproduces an ISO 32000-1 certification signature declaring what changes (if any) are permitted after signing.**PAdES (eIDAS) baseline signatures.**`sign_pdf_pades(...)`\n\n/`amend_sign_pades(...)`\n\nproduce ETSI EN 319 142 PAdES-BASELINE signatures (B-B, and B-T with an RFC 3161 timestamp): the EU-recognized profile for legal signature validity, on top of the existing append-only signing.**Factur-X / ZUGFeRD e-invoicing.**`Document.attach_facturx(invoice)`\n\nembeds an EN 16931 Cross Industry Invoice XML on PDF/A-3, validated for arithmetic consistency before rendering (France B2B mandate, Sept 2026).**PDF/X-4 print output and WTPDF 1.0.**`Document(pdfx=True, ...)`\n\nemits an ISO 15930-7 print/prepress file with a CMYK output intent;`Document(wtpdf=True)`\n\ndeclares Well-Tagged PDF 1.0 Reuse conformance, and`verify_wtpdf()`\n\nself-checks the tagging.**BrandKit.** A single versioned brand object (palette, fonts, logo) applies across every document built from it, so a rebrand touches one object instead of every document.**CMYK and PDF/A archival output** for print production and long-term retention pipelines.**Long-form apparatus**: numbered appendices, an alphabetized back-of-book index with resolved page numbers, a glossary with auto-linked first occurrences, and a visible table of contents with real page numbers and dot leaders.removes embedded files, provenance-revealing metadata, and internal structure-tree node ids from a rendered PDF before it goes to an external party.`emboss strip`\n\nEmboss is not a general-purpose drawing library or an HTML-to-PDF converter. It is built for the pipeline where structured or model-generated content becomes a trustworthy document, and the comparison is most honest at the level of tool categories rather than head-to-head with a specific product.\n\nEvery claim in the Emboss column below is verifiable against this repository and its test suite. The right column describes what general-purpose PDF tooling (imperative drawing libraries and HTML/CSS engines) *typically* does; it is a category statement, not a claim about any one product, and specifics for a given tool should be verified against that tool.\n\n**Where Emboss is differentiated**\n\n| Dimension | Emboss | Typical general-purpose PDF tools |\n|---|---|---|\n| Input a model can be constrained to | JSON schema, provider-native structured outputs, Markdown | Imperative code or HTML/CSS; no schema contract |\n| Validation and repair of imperfect input | Built in (truncation repair, per-block recovery) | Not provided |\n| Deterministic byte-identical output | Yes, test-verified | Generally not guaranteed |\n| PDF/UA accessibility tagging | On by default | Opt-in or unavailable |\n| Conformance evidence | Real veraPDF check in CI and `emboss verify` |\nVaries; rarely built in |\n| Self-describing, recoverable document | `embed_spec` + `from_pdf` |\nNo |\n| Stable node ids and layout map | Yes | No |\n| Document diff to a redlined PDF | Yes | No |\n| Redaction that never enters the content stream | Yes | Typically overlay-based |\n\n**Where established tools are ahead**\n\nThis side matters just as much. Stated plainly:\n\n| Dimension | Emboss | Typical general-purpose PDF tools |\n|---|---|---|\n| Complex-script shaping (Arabic, Indic) | Not supported | Several engines support it |\n| Arbitrary HTML and CSS input | Not supported | HTML/CSS engines accept it |\n| Image formats | PNG and JPEG | Often broader |\n| Production maturity | New, released 2026 | Many have 10 to 20 years in production |\n\nFor turning structured or LLM-generated content into accessible, verifiable, auditable PDFs, Emboss covers ground the general category does not. For arbitrary HTML rendering, complex-script typesetting, or a long track record in production, an established tool is the safer choice.\n\nExisting Python PDF libraries are page-description tools: you place ink at coordinates, and the resulting file has no idea that a given string was a heading. Accessibility tags, if available at all, are painted on after the fact and tend to disagree with the visible page.\n\nEmboss inverts that. The semantic document is the source of truth, and both the visual output and the structure tree are derived from it. They cannot drift apart, because they come from the same description.\n\n**Deterministic output.** The same document produces byte-identical PDFs across runs and machines. No timestamps, no random identifiers; the file `/ID`\n\nis derived from content. This makes output hash-verifiable for filings and diffable in CI.\n\n```\nassert doc.render() == doc.render()   # always true\n```\n\n**Structural correctness.** Cross-reference offsets are recorded as bytes are written, so the xref table cannot disagree with the file.\n\n**Content never overflows.** Everything is measured before anything is placed, so text running off the page is not a failure mode.\n\n**Tagged by default.** Every document ships with a complete PDF/UA structure tree.\n\n```\npip install emboss-pdf\n```\n\nRequires Python 3.10+ and `fonttools`\n\n. Optional extras:\n\n```\npip install emboss-pdf[all]       # pydantic + pikepdf + cryptography\npip install emboss-pdf[llm]       # pydantic schemas for LLM structured output\npip install emboss-pdf[verify]    # pikepdf for PDF structural verification\npip install emboss-pdf[signing]   # cryptography for digital signatures\npip install emboss-pdf[dev]       # full dev environment (pytest, mypy, ruff)\n```\n\nAn OFL-licensed font set ships with the package (Source Serif 4, Source Sans 3, Source Code Pro; ~2.7MB, Latin, Greek, and Cyrillic coverage), plus Emboss Math, a modified subset of STIX Two Math carrying the mathematical alphanumeric glyphs used by `\\mathbb`\n\n/`\\mathcal`\n\n/`\\mathfrak`\n\n. Registering the set gives embedded fonts with full kerning and ligatures, no system font files required:\n\n``` python\nfrom emboss import Document\nfrom emboss.bundled_fonts import register_bundled_fonts\n\ndoc = Document(title=\"Report\")\nregister_bundled_fonts(doc.fonts)\n```\n\nThe three text families are also available under the aliases `\"emboss serif\"`\n\n, `\"emboss sans\"`\n\n, and `\"emboss mono\"`\n\n. Two lower-level helpers, `bundled_font_path(family, bold=, italic=)`\n\nand `BUNDLED_FAMILIES`\n\n, are available for callers that want the file paths directly.\n\n``` python\nfrom emboss import Document\n\ndoc = Document(title=\"Quarterly Report\", author=\"Finance Team\", style=\"finance\")\ndoc.heading(\"Executive Summary\", level=1)\ndoc.paragraph(\"Revenue grew 12% year over year, driven by enterprise expansion.\")\ndoc.table(\n    headers=[\"Metric\", \"Q3 2025\", \"Q3 2024\", \"Change\"],\n    rows=[\n        [\"Revenue\", \"$24.1M\", \"$21.5M\", \"+12%\"],\n        [\"EBITDA\",  \"$8.2M\",  \"$6.9M\",  \"+19%\"],\n    ],\n)\ndoc.bullets([\"North America: +15%\", \"EMEA: +8%\", \"APAC: +11%\"])\ndoc.save(\"quarterly_report.pdf\")\npython\nfrom emboss.spec import Document, Heading, Paragraph, Table\n\ndoc = Document(\n    title=\"Quarterly Report\",\n    style=\"finance\",\n    content=[\n        Heading(text=\"Executive Summary\", level=1),\n        Paragraph(content=\"Revenue grew 12% year over year.\"),\n        Table(\n            headers=[\"Metric\", \"Value\"],\n            rows=[[\"Revenue\", \"$24.1M\"], [\"EBITDA\", \"$8.2M\"]],\n        ),\n    ],\n)\npdf_bytes = doc.render()\n```\n\n**Tier 1: Spec prompt** (full quality, recommended) -- pass the EmbossSpec format with your prompt:\n\n``` python\nfrom emboss import spec_prompt, Document\n\nsystem = spec_prompt(style=\"finance\")  # compact EmbossSpec description for LLMs\nresponse = client.messages.create(\n    model=\"claude-sonnet-5\",\n    system=system,\n    messages=[{\"role\": \"user\", \"content\": \"Create a Q3 financial report\"}],\n)\ndoc = Document.from_json(response.text)\ndoc.save(\"report.pdf\")\n```\n\n**Tier 2: One-liner** (zero friction):\n\n``` python\nfrom emboss import generate\n\ngenerate(\"Create a quarterly financial report\",\n         style=\"finance\", output=\"report.pdf\",\n         provider=\"anthropic\")  # or \"openai\"\n```\n\n**Tier 3: Markdown** (works with any LLM output):\n\n``` python\nfrom emboss import Document\n\nmd = llm.generate(\"Write a quarterly report...\")  # any LLM, any provider\ndoc = Document.from_markdown(md, style=\"finance\")\ndoc.save(\"report.pdf\")\n```\n\nAll three tiers produce identical PDF quality -- the typography engine (Knuth-Plass, optical margins, kerning) runs the same regardless of input format.\n\n**A whole directory, one PDF.** A documentation team adds a build step rather than calling an API:\n\n```\nemboss build ./docs -o handbook.pdf\n```\n\nFiles are concatenated in alphabetical order (numeric prefixes like `01-intro.md`\n\nwork naturally), or from an explicit `.order`\n\nfile listing names one per line; the first file's front matter supplies the title and style. Available in Python as `build_from_directory(\"./docs\")`\n\n.\n\n**Code that cannot drift from its source.** A fenced code block can load its body from a file instead of being typed inline:\n\npython file=examples/settle.py lines=10-20\n`lines=A-B`\n\nselects a 1-based inclusive range; `marker=NAME`\n\nselects a `# region NAME`\n\n/ `# endregion`\n\n(or `# BEGIN NAME`\n\n/ `# END NAME`\n\n) block instead. The included text is dedented and flows through the normal syntax highlighter, so a documented example is always the real code, not a copy that quietly went stale. Available directly as `include_source(path, lines=..., marker=...)`\n\n.\n\n**Robust parsing.** LLM output is messy; the parser is built for it:\n\n```\ndoc = Document.from_json(\n    text,\n    strict=False,      # default: repair instead of raising\n    smart=True,        # content intelligence: typography, tables, auto-style\n    on_warning=print,  # one message per repair performed\n)\n```\n\n`spec_prompt()`\n\nteaches the exact vocabulary the validator accepts, so valid output is the common case- Synonym type tags and field spellings are normalized to the canonical vocabulary on both parse paths\n- Truncated JSON (cut off mid-generation) is repaired before parsing\n- When validation fails, recovery is per block: invalid blocks are coerced to paragraphs or dropped, the rest of the document renders\n`strict=True`\n\nraises on any malformed input instead of repairing\n\nSpecs can also request layout features directly: `\"toc\": true`\n\ninserts an auto-generated table of contents, and `\"page\": {\"columns\": 2, \"column_gap\": 18}`\n\nsets multi-column geometry.\n\nDocuments are defined using **EmbossSpec**, a declarative JSON-serializable specification. Every document element is a typed Python dataclass that can be constructed programmatically, deserialized from JSON, or generated by an LLM via structured output.\n\nThe spec is the single source of truth. From it, Emboss derives:\n\n- The visual PDF (typography, layout, pagination)\n- The accessibility structure tree (PDF/UA tags)\n- HTML, Markdown, and DOCX exports (via adapters)\n\n```\n{\n  \"title\": \"Annual Report\",\n  \"style\": \"corporate\",\n  \"content\": [\n    {\"type\": \"heading\", \"text\": \"Overview\", \"level\": 1},\n    {\"type\": \"paragraph\", \"content\": \"Company performance exceeded targets.\"},\n    {\"type\": \"table\", \"headers\": [\"Q1\", \"Q2\"], \"rows\": [[\"$1M\", \"$1.2M\"]]}\n  ]\n}\n```\n\n| Element | Description | Key Parameters |\n|---|---|---|\n`Heading` |\nSection heading (H1-H6) | `text` , `level` (1-6) |\n`Paragraph` |\nBody text with inline formatting | `content` (str, TextRun, or list) |\n`BulletList` |\nUnordered list with nesting | `items` (strings or sublists) |\n`NumberedList` |\nOrdered list with nesting | `items` , `start` |\n`Table` |\nData table with header row | `headers` , `rows` , `caption` |\n`Image` |\nEmbedded image | `source` (path or bytes), `width` , `caption` |\n`CodeBlock` |\nSyntax-highlighted code | `code` , `language` , `line_numbers` |\n`MathBlock` |\nLaTeX-style math notation | `source` , `display` |\n`Chart` |\nData visualization | `chart_type` , `labels` , `values` |\n`Callout` |\nAdmonition box (note/warning/tip) | `content` , `variant` , `title` |\n`Footnote` |\nNumbered footnote | `content` |\n`SvgBlock` |\nInline SVG vector graphic | `source` , `width` , `height` |\n`BibliographyBlock` |\nFormatted citation list | `citations` |\n`HorizontalRule` |\nVisual separator | `thickness` , `color` |\n`PageBreak` |\nForce a page break (optionally to a named `page_styles` geometry) |\n`page_style` |\n`BlockQuote` |\nQuoted passage set off with an accent bar | `content` , `attribution` |\n`CoverPage` |\nFull-page title composition, forces a page break | `title` , `subtitle` , `authors` , `date` , `kicker` |\n`Abstract` |\nIndented abstract block with a keywords line | `text` , `keywords` |\n`Authors` |\nCentered grid of author entries | `authors` (name, affiliation, email) |\n`PullQuote` |\nLarge-type offset quotation | `text` , `attribution` |\n`StatTiles` |\nRow of bordered statistic tiles, colored deltas | `stats` (label, value, delta) |\n`TableOfContents` |\nVisible listing with dot leaders and page numbers | `title` , `depth` , `source` (headings/figures/tables) |\n`Appendix` |\nLettered section (Appendix A, B, ...) with its own `A.1` numbering |\n`title` , `content` |\n`Index` |\nBack-of-book index resolved from `index_terms` marks |\n`title` |\n`Glossary` |\nAlphabetized term/definition list with auto-linked first occurrences | `entries` , `title` |\n| Diagram | Node/edge graph with automatic layout, via `doc.diagram(...)` |\n`nodes` , `edges` , `direction` , `caption` |\n\n``` python\nfrom emboss import Paragraph, TextRun\n\nParagraph(content=[\n    TextRun(text=\"Regular text, \"),\n    TextRun(text=\"bold text\", bold=True),\n    TextRun(text=\", \"),\n    TextRun(text=\"italic text\", italic=True),\n    TextRun(text=\", and \"),\n    TextRun(text=\"colored text\", color=\"2563eb\"),\n])\n```\n\nSeven professionally designed stylesheets are built in. Each encodes type scale, leading, table rules, and spacing so output looks authored without choosing a single measurement.\n\n| Preset | Body Font | Heading Font | Body Size | Alignment | Use Case |\n|---|---|---|---|---|---|\n`legal` |\nTimes | Times | 11.5pt | Justified | Contracts, briefs, pleadings |\n`finance` |\nHelvetica | Helvetica | 10pt | Left | Reports, filings, data sheets |\n`academic` |\nTimes | Helvetica | 11.5pt | Justified | Papers, dissertations, theses |\n`corporate` |\nHelvetica | Helvetica | 10.5pt | Left | Memos, policies, manuals |\n`minimal` |\nHelvetica | Helvetica | 9.5pt | Left | Data exports, compact reports |\n`journal` |\nTimes | Times | 10.5pt | Justified | Journals, periodicals |\n`brief` |\nHelvetica | Helvetica | 10.5pt | Left | Executive briefs, one-pagers |\n\n```\n# Use a preset by name\ndoc = Document(style=\"finance\")\n\n# Or create a custom stylesheet\nfrom emboss import Style, StyleSheet\ncustom = StyleSheet(\n    name=\"custom\",\n    body=Style(font_family=\"Helvetica\", font_size=11.0, align=\"justify\"),\n    h1=Style(font_size=18.0, bold=True),\n    # ... other elements\n)\ndoc = Document(style=custom)\n```\n\nA `BrandKit`\n\nis an immutable, versioned brand object layered on top of any style preset at render time. Set it once on the document and colors, fonts, and footer text propagate everywhere, so a rebrand is a single-object change rather than a find-and-replace across every document.\n\n``` python\nfrom emboss import Document, BrandKit\n\nbrand = BrandKit(\n    name=\"Acme\",\n    version=\"2.1\",\n    primary=\"1f4e79\",\n    accent=\"1f8a70\",\n    ink=\"1a1a1a\",\n    muted=\"6b7280\",\n    heading_font=\"Emboss Serif\",\n    body_font=\"Emboss Sans\",\n    footer_text=\"Acme Corp -- Confidential\",\n)\n\ndoc = Document(title=\"Board Deck\", style=\"corporate\", brand=brand)\n```\n\n- Heading and table-header colors take the brand\n`primary`\n\n; body and secondary text take`ink`\n\n/`muted`\n\n. Any brand color that fails 4.5:1 contrast against white is automatically darkened for text use, while the raw brand color is kept for fills and rules. `brand.series_palette(n)`\n\nreturns a deterministic N-color chart palette derived from`primary`\n\nand`accent`\n\n(or the explicit`palette`\n\ntuple, padded with derived colors if it runs short) -- charts under a brand stay on-brand without hand-picked colors.`brand.to_dict()`\n\n/`BrandKit.from_dict()`\n\nround-trip a brand kit through JSON (logo bytes travel as base64), for storing a brand centrally and loading it per render.\n\nEmboss uses the Knuth-Plass algorithm from Knuth and Plass (1981) for optimal paragraph line breaking. Unlike greedy algorithms that fill each line independently, Knuth-Plass treats the paragraph as a shortest-path problem and minimizes total demerits across all lines:\n\n```\ngreedy   widths: [216, 172, 168, 162, 176]   variance 367\noptimal  widths: [216, 224, 202, 198]        variance 110\n```\n\nThe algorithm:\n\n- Models text as Box (word), Glue (elastic space), and Penalty (breakpoint) items\n- Considers all legal breakpoints simultaneously\n- Penalizes abrupt spacing changes and tight/loose lines\n- Flags hyphen breaks and caps hyphen ladders: a large demerit stops runs of more than 3 consecutive hyphenated lines\n- Splits any word wider than the measure into character-level pieces (long URLs, identifiers), so lines never overflow even in narrow columns\n- Falls back to greedy breaking only for pathological input\n\nThe full Knuth-Liang en-US pattern set is bundled, plus a no-break list for domain terms (`Inc.`\n\n, `LLC`\n\n, `EBITDA`\n\n, `plaintiff`\n\n) that should never be split.\n\nGlyph advances and kerning come from the font file (via fonttools). Text is emitted with the PDF `TJ`\n\noperator so kerning pairs are actually applied between glyphs. Kerning reads GPOS pair positioning in full: class-based (Format 2) pairs resolved lazily and memoized, Extension (LookupType 9) wrappers unwrapped, and Value2 adjustments summed.\n\nBase-14 font metrics are built in, so valid PDFs are produced with no font files present. The built-in AFM widths cover more than ASCII: en/em dashes, curly quotes, and accented Latin letters resolve to correct advances (accents via NFD decomposition to the base letter). For embedded fonts out of the box, see [Bundled Fonts](#bundled-fonts).\n\nAutomatic curly quotes, em/en dashes, fractions, ellipses, and non-breaking spaces before units. Applied during text processing, not as a post-process.\n\nAutomatic fi, fl, ff, ffi, and ffl ligature substitution on embedded fonts, gated per glyph on the font's cmap. Base-14 fonts carry no ligature glyphs and are left untouched.\n\nRegister a CJK-capable font (Noto Sans JP/SC/KR, or any TTF/OTF with CJK glyph coverage) and Chinese, Japanese, and Korean text renders correctly -- real glyphs, correct widths, correct line wrapping:\n\n``` python\nfrom emboss import Document, TextRun\n\ndoc = Document(title=\"Report\")\ndoc.fonts.register(\"cjk\", \"/path/to/NotoSansJP-Regular.ttf\")\ndoc.paragraph(TextRun(\"日本語のテストです。\", font_family=\"cjk\"))\n```\n\nBase-14 fonts (Helvetica, Times) carry no CJK glyphs; without a registered font, CJK characters are substituted out with a recorded warning rather than silently corrupting the page. Once a font is registered, the line breaker treats CJK Unified Ideographs, Hiragana, Katakana, and Hangul Syllables as break-opportune between any two characters (unlike Latin text, CJK carries no spaces between words), so long CJK passages wrap instead of overflowing. Which characters actually render depends on the registered font's own glyph coverage, as with any font -- a font covering only Japanese will not carry Simplified Chinese glyphs; [Noto Sans CJK](https://github.com/notofonts/noto-cjk) covers Chinese, Japanese, and Korean in one family if a document mixes scripts.\n\n**Not supported, stated honestly:** right-to-left scripts (Arabic, Hebrew) and complex-script shaping (Indic scripts requiring glyph reordering and ligature substitution via a shaping engine like HarfBuzz) are a separate, larger project and remain unimplemented; RTL/complex-script text is still substituted with a warning today.\n\n``` python\nfrom emboss import PageSpec\n\n# Standard sizes\npage = PageSpec.letter()                        # 8.5 x 11 in\npage = PageSpec.a4()                            # 210 x 297 mm\npage = PageSpec.legal()                         # 8.5 x 14 in\n\n# Custom dimensions and margins (in points, 72pt = 1 inch)\npage = PageSpec(\n    width=612, height=792,\n    margin_top=72, margin_bottom=72,\n    margin_left=90, margin_right=72,\n)\n\n# Multi-column layout\npage = PageSpec.a4(columns=2, column_gap=18)\n\n# A4 landscape, tuned for wide tables and diagrams\npage = PageSpec.a4(landscape=True)\n```\n\nAlso built in: `PageSpec.a5()`\n\nand the tight-margin `PageSpec.compact()`\n\n(A5, tuned for phone and tablet PDF readers).\n\nA document can switch page geometry mid-flow for one wide table or diagram, then switch back, via `page_styles`\n\nand `PageBreak.page_style`\n\n:\n\n```\ndoc = Document(\n    title=\"Annual Report\",\n    page_styles={\"wide\": PageSpec.a4(landscape=True)},\n)\ndoc.paragraph(\"Portrait content here.\")\ndoc.page_break(page_style=\"wide\")   # subsequent pages use the wide geometry\ndoc.table(headers=[...], rows=[...])\ndoc.page_break()                    # no page_style: reverts to the document default\n```\n\n**Widow and orphan control**: prevents single lines stranded at page tops/bottoms** Keep-with-next**: headings stay with their following paragraph** Keep-together**: atomic blocks (callouts, small tables) never split across pages** Table splitting**: large tables break across pages with header row repetition** Column balancing**: multi-column layouts balance content across columns\n\nEvery document is tagged with a complete PDF/UA structure tree:\n\n- Headings become\n`/H1`\n\nthrough`/H6`\n\n- Paragraphs become\n`/P`\n\n- Tables become\n`/Table`\n\nwith`/TH`\n\nand`/TD`\n\ncells; header cells carry`/Scope`\n\n- Lists become\n`/L`\n\nwith`/LI`\n\n,`/Lbl`\n\n, and`/LBody`\n\n- Images, SVG blocks, and charts get\n`/Figure`\n\nwith the alt text emitted as`/Alt`\n\n(a description is auto-derived when none is given) - Hyperlinks are written as\n`/Link`\n\nannotations and tagged as`/Link`\n\nstructure elements with`OBJR`\n\nreferences, as PDF/UA requires - Running heads, page numbers, Bates stamps, and watermarks are marked\n`/Artifact`\n\n- Tagged documents declare the PDF/UA-1 identifier in their XMP metadata\n\n``` python\nfrom emboss.pdf.verify import verify_pdf\n\nresult = verify_pdf(doc.render())\nprint(result)  # VerificationReport(ok=True, has_struct_tree=True, ...)\n```\n\n`verify_pdf`\n\nis a structural check on the bytes Emboss just wrote (well-formed xref, matching object offsets, a present structure tree). It is not a conformance check by itself.\n\nFor actual ISO conformance, `emboss.pdf.verify.verify_conformance`\n\n(and the `emboss verify --conformance`\n\nCLI flag) invoke the real veraPDF CLI as a subprocess and parse its JSON report -- this is not a simulation, and it is not optional in CI: the `conformance`\n\njob in `.github/workflows/ci.yml`\n\ninstalls the official veraPDF binary and runs the test suite against it on every push and pull request.\n\n```\nemboss verify report.pdf --conformance 2b     # PDF/A-2b\nemboss verify report.pdf --conformance ua1    # PDF/UA-1\nemboss verify report.pdf --conformance 3b     # PDF/A-3b (with attachments)\npython\nfrom emboss.pdf.verify import verify_conformance\n\nreport = verify_conformance(doc.render(), flavour=\"ua1\")\nprint(report.compliant)    # bool\nprint(report.violations)   # list[RuleViolation]: clause, description, count\n```\n\nRequires the `verapdf`\n\nexecutable on `PATH`\n\n, or `VERAPDF_PATH`\n\npointing at it; otherwise `verify_conformance`\n\nraises with an install hint rather than silently skipping the check.\n\nA rendered PDF can carry enough of its own provenance to be reconstructed later, without a side channel or a database lookup.\n\n```\ndoc = Document(title=\"Q3 Report\", style=\"finance\")\ndoc.heading(\"Revenue\", level=1)\ndoc.paragraph(\"Revenue increased 12% year over year.\")\n\npdf_bytes = doc.render(embed_spec=True)\n```\n\n`embed_spec=True`\n\nattaches three real `/AF`\n\n(associated file) attachments to the PDF:\n\n| Attachment | Contents | `/AFRelationship` |\n|---|---|---|\n`emboss-spec.json` |\nThe canonical EmbossSpec JSON for exact reconstruction | `Source` |\n`emboss-layout.json` |\nThe node id -> page/bounding-box layout map | `Supplement` |\n`emboss-textmap.json` |\nThe node id -> per-character text-position index | `Supplement` |\n`emboss-doc.md` |\nA reflowable Markdown twin of the document | `Alternative` |\n\n``` python\nfrom emboss import Document\n\nrecovered = Document.from_pdf(\"report.pdf\")\n```\n\n`Document.from_pdf`\n\ntries the embedded `emboss-spec.json`\n\nfirst, an exact reconstruction. If it is missing (or `strict=True`\n\nis not set and no attachment was ever embedded), it falls back to a degraded reconstruction by walking the PDF/UA structure tree and pulling text out of the content streams by marked-content id: headings, paragraphs, tables, lists, block quotes, footnotes, and code blocks come back with correct text and order (and their original stable node ids), even when styling and exact spec fields are lost. Pass `strict=True`\n\nto require the exact path and raise instead.\n\n```\nlayout = doc.layout_map()\n# {\"n1a2b3c4\": [{\"page\": 0, \"x0\": 72.0, \"y0\": 690.2, \"x1\": 540.0, \"y1\": 706.4}], ...}\n```\n\nEvery top-level block gets a stable id -- an explicit one if you set it, otherwise a deterministic token derived from the element's type, position, and content, so the same document assigns the same ids on any machine and across runs. `doc.layout_map()`\n\nresolves every id to where it actually landed on the page, in PDF coordinates. Node ids and the layout map are the mechanism behind `from_pdf`\n\nrecovery, the diff/redline tooling below, and any downstream annotation or programmatic-review workflow that needs to key off \"this specific block, wherever it ends up.\"\n\nBefore a PDF leaves the building, `emboss strip`\n\nremoves all of this:\n\n```\nemboss strip report.pdf -o external.pdf\npython\nfrom emboss import strip_pdf\n\nclean_bytes = strip_pdf(pdf_bytes)\n```\n\n`strip_pdf`\n\noperates on already-rendered bytes: it drops the `/AF`\n\nattachments and the `/Names /EmbeddedFiles`\n\ntree, clears `/Producer`\n\nand `/Creator`\n\nplus the XMP `CreatorTool`\n\n/document-history fields, and removes the structure tree's `/IDTree`\n\nand per-element `/ID`\n\nnode ids -- while keeping Title, Author, and date fields. Output stays deterministic.\n\nA single table or chart can carry its own source data inside the PDF, so the numbers a reader sees are one double-click away from the spreadsheet behind them. Set `attach_data=True`\n\n:\n\n```\ndoc.table(\n    headers=[\"Quarter\", \"Bookings\"],\n    rows=[[\"Q1\", \"1,240\"], [\"Q2\", \"1,510\"], [\"Q3\", \"1,880\"]],\n    caption=\"Bookings by quarter\",\n    attach_data=True,   # embeds the table as an /AF CSV attachment\n)\n```\n\nEmboss writes the table (or the chart's series) out as a CSV and embeds it as a real `/AF`\n\nassociated-file attachment on that element -- not a separate side file. The visible page is unchanged; the data rides along inside the same PDF.\n\n**How a reader opens the attachment.** Attachment support is a PDF-reader feature, not something the document controls, so the steps differ by reader:\n\n| Reader | How to open the embedded data |\n|---|---|\n| Adobe Acrobat / Reader | View -> Show/Hide -> Side panels -> Attachments, then double-click the CSV |\n| Firefox (built-in viewer) | Click the paperclip icon in the left toolbar, then the file |\n| Preview (macOS), Chrome | No attachment UI -- these readers do not surface `/AF` files; use Acrobat or Firefox, or extract programmatically (below) |\n\nAny embedded file can also be pulled out without a viewer, straight from the bytes:\n\n``` python\nimport io, pikepdf\n\nwith pikepdf.open(io.BytesIO(pdf_bytes)) as pdf:\n    csv = pdf.attachments[\"table-1-data.csv\"].get_file().read_bytes()\n```\n\nBeyond recovering a document's content, `manifest=True`\n\nrecords what it takes to reproduce its exact rendered bytes:\n\n```\npdf_bytes = doc.render(manifest=True)   # attaches emboss-manifest.json\n```\n\nThe manifest is a deterministic JSON summary: the spec's sha256, the running Emboss version, every embedded font's sha256, and any render options that differ from their defaults (`pdfa`\n\n, `color_mode`\n\n, `tagged`\n\n, `toc`\n\n, `page_number_format`\n\n, `page_numbers`\n\n, `front_matter_pages`\n\n). `doc.reproducibility_manifest(...)`\n\nbuilds the same dict without rendering.\n\n```\nemboss reproduce report.pdf\nphp\nfrom emboss import reproduce\n\nreport = reproduce(\"report.pdf\")   # recover -> re-render -> structurally compare\nprint(report.ok)                   # bool\n```\n\n`reproduce()`\n\nrecovers the document from the PDF (via the embedded spec, or the degraded structure-tree path), re-renders it, and reports whether the two PDFs agree structurally -- same page count and same visible text per page, not a byte-for-byte match (attaching a manifest to the reproduction would itself shift bytes across the attachment boundary). A way to catch drift between what a PDF claims to be and what actually generated it.\n\nA verifiable record of what generated a document -- which model, from what prompt, and who reviewed it -- folded into the same manifest:\n\n``` python\nfrom emboss import Document, GeneratorInfo\n\ngenerator = GeneratorInfo.from_prompt(\n    \"Write a quarterly financial report\",\n    model=\"claude-sonnet-5\", provider=\"anthropic\",\n    reviewed_by=\"R. Patel\", reviewed_at=\"2026-07-29\",\n)\ndoc = Document(title=\"Q3 Report\")\ndoc.paragraph(\"Revenue increased 12% year over year.\")\npdf_bytes = doc.render(manifest=True, generator=generator)\n```\n\n`GeneratorInfo.from_prompt`\n\nhashes the prompt (`prompt_sha256`\n\n) rather than storing it, so the manifest proves a specific, reproducible input produced this output without disclosing potentially sensitive prompt content. `reviewed_by`\n\n/`reviewed_at`\n\nare plain caller-supplied strings, recorded only when a review is a deliberate act, never populated from the wall clock. Read it back with `read_generator_info(pdf_bytes)`\n\n, which returns `None`\n\n(not an exception) when no generator record is present -- an honest absence, not a guess. `Document.generator = GeneratorInfo(...)`\n\nsets it once for every subsequent render; `generate(..., manifest=True)`\n\n(the one-liner LLM tier) auto-populates model, provider, and the prompt hash unless a `generator=`\n\noverride is given.\n\nThis closes a real gap: as AI-content disclosure obligations broaden, no other PDF tool can prove which model produced a document and whether a human reviewed it. It composes with everything else here -- the reproducibility manifest is already signable via `sign_pdf`\n\n/`sign_pdf_pades`\n\n, so provenance can be part of a verifiable chain of custody, not a self-reported claim.\n\n`doc.patch(node_id, **changes)`\n\nreturns a new `Document`\n\nwith the one block carrying `node_id`\n\nreplaced (`dataclasses.replace(block, **changes)`\n\nunder the hood) -- everything else, and the original document, untouched. Useful when a caller (an LLM given one block's id and a small diff, or an editing UI) only needs to change a single block without regenerating the whole spec.\n\n`diff_documents`\n\nmatches blocks between two documents by their stable node id (assigning ids first if either side lacks them), and classifies each as added, removed, changed, or unchanged. A matched pair counts as \"changed\" only when both sides already carry the same id for that block -- typically because the id was assigned once (an explicit `id=`\n\n, a prior render) and the block was edited in place rather than replaced.\n\n``` python\nfrom emboss import diff_documents, render_redline\n\nresult = diff_documents(old_doc, new_doc)\nprint(len(result.added), len(result.removed), len(result.changed))\n\nredline_bytes = render_redline(old_doc, new_doc, result)\n```\n\n`render_redline`\n\nrenders the new document through the normal measure/paginate/render pipeline with revision marks baked in, not overlaid: deleted words struck through in crimson, inserted words underlined in forest green, added blocks marked with a real measured change-bar in the left margin, and a prepended summary page listing what was removed. Changed headings and pull quotes keep their new text with a redlined note paragraph underneath, since they carry no per-run styling to inline a word diff into.\n\n```\nemboss diff old.pdf new.pdf -o redline.pdf\n```\n\nA reviewer highlights, strikes out, or comments on a rendered PDF in whatever they already use -- Acrobat, Preview, Chrome. Because an Emboss PDF carries a text-position index, each annotation resolves back to the exact node and character range it covers, not a guess at a rectangle. This is the piece other PDFs cannot do: they have no structure to resolve a comment against.\n\nRender with `embed_spec=True`\n\nso the text map (`emboss-textmap.json`\n\n) is present, then read the markup back:\n\n``` python\nfrom emboss.annotations import extract_comments, merge_comments\n\ndoc.save(\"v1.pdf\", embed_spec=True)   # reviewers mark this up in their reader\n\ncomments = extract_comments(\"v1-legal.pdf\")\ncomments = merge_comments(\n    extract_comments(\"v1-legal.pdf\"), extract_comments(\"v1-finance.pdf\")\n)\n```\n\nEach comment carries the reviewer, page, type, their text, and a resolution keyed to a stable node id and character range:\n\n```\n{\"id\": \"c-07\", \"type\": \"strikeout\", \"author\": \"R. Patel\", \"page\": 4,\n \"node_id\": \"sec-risk.p3\", \"anchor_text\": \"exposure exceeds $4.2M\",\n \"char_range\": [120, 143], \"comment\": \"Overstates it, use the netted figure\",\n \"resolution\": \"exact\", \"status\": \"open\"}\n```\n\nThe `resolution`\n\nis never omitted, so a mis-resolved comment cannot pass silently:\n\n| State | Meaning | Behaviour |\n|---|---|---|\n`exact` |\nOne node and a character range | Patchable, phrase-level |\n`node` |\nOne node, no character range | Patchable, whole-node |\n`spanning` |\nCrosses two or more nodes | Reports every node id; never split silently |\n`unanchored` |\nNo content beneath the annotation | Surfaced with page and rect; never dropped |\n\nComments are never auto-applied -- that would ship a document nobody approved. Turning a free-text comment into a concrete replacement is a separate step (often a model); Emboss applies it deterministically and proves the change:\n\n``` python\nfrom emboss.review import propose_patches, apply_replacements, redline\n\npropose_patches(doc, comments, replacements={})          # what would change\nnew_doc = apply_replacements(doc, comments, {\"c-07\": \"the netted $2.8M\"})\nredline_pdf = redline(doc, new_doc)                       # proves the change\n```\n\nFor an `exact`\n\nresolution over plain text, only the objected-to phrase is spliced; everything else stays byte-identical. `review_html(comments)`\n\nrenders a self-contained triage report with the unresolved count loud at the top.\n\n```\nemboss render spec.json -o v1.pdf --embed-spec       # embeds spec, layout, text map\nemboss review v1-legal.pdf v1-finance.pdf -o comments.json\nemboss review v1-legal.pdf --html review.html        # triage view, no server\nemboss apply comments.json --spec spec.json                       # propose only\nemboss apply comments.json --spec spec.json --edits edits.json \\\n    -o v2.pdf --redline redline.pdf                               # apply and prove\n```\n\n`emboss review`\n\nexits non-zero when any comment is unresolved, so a pipeline can gate on it. Flattened annotations (Preview's flattened export) are burned into the page and cannot be recovered by anything; a stripped PDF (no text map) resolves at page-and-rect granularity only, never a wrong guess.\n\nMake Emboss documents callable from an AI assistant that speaks the [Model Context Protocol](https://modelcontextprotocol.io), such as Claude Desktop. Configure it once, and the assistant can generate PDFs and answer questions about them **from their embedded structure, not from guessing at rendered pixels** -- because an Emboss PDF made with `embed_spec=True`\n\ncarries its own EmbossSpec JSON, a per-character text index, and any CSV data attached to its tables.\n\n```\npip install \"emboss-pdf[mcp]\"\n{\n  \"mcpServers\": {\n    \"emboss\": { \"command\": \"emboss-mcp\" }\n  }\n}\n```\n\nThe server (built on the standard FastMCP API, served over stdio) exposes tools for the novel capabilities: `render_document`\n\n, `get_document_spec`\n\nand `get_document_text`\n\n(exact answers from the embedded JSON), `list_embedded_data`\n\n/ `extract_embedded_data`\n\n(pull a table's source CSV back out), `extract_review_comments`\n\n(annotations resolved to nodes), `revision_history`\n\n(traceability), plus `verify_document`\n\nand `get_spec_schema`\n\n. Every query tool takes a file path, so one server answers questions about every PDF you generate. See the [MCP Server guide](https://ggchamp85.github.io/Emboss/mcp) for the step-by-step Claude Desktop setup.\n\nThe tools are plain, tested functions (`emboss.mcp_server.dispatch`\n\n), callable from your own code without an MCP client.\n\nPre-configured document factories for common formats:\n\n``` python\nfrom emboss.templates import memo, report, letter, invoice\n\n# Create a memo with pre-set styling\ndoc = memo(title=\"Project Update\", author=\"Engineering Team\")\ndoc.heading(\"Status\", level=2)\ndoc.paragraph(\"All milestones are on track.\")\ndoc.save(\"memo.pdf\")\n\n# Create an invoice\ndoc = invoice(title=\"Invoice #2024-0147\", author=\"Acme Corp\")\ndoc.table(\n    headers=[\"Item\", \"Qty\", \"Price\"],\n    rows=[[\"Widget\", \"100\", \"$5.00\"], [\"Gadget\", \"50\", \"$12.00\"]],\n)\ndoc.save(\"invoice.pdf\")\n```\n\nAvailable templates: `memo`\n\n, `report`\n\n, `letter`\n\n, `invoice`\n\n, `academic_paper`\n\n, `legal_brief`\n\n, `slide_deck`\n\n, `data_sheet`\n\n.\n\n`SlideDeck`\n\nbuilds a presentation deck from designed slide layouts, complete color themes, and a fit-to-slide guarantee: no slide ever spills onto a continuation page.\n\n``` python\nfrom emboss.slides import SlideDeck\n\ndeck = SlideDeck(\"Q3 Board Review\", presenter=\"Ana Ruiz\", date=\"Oct 2026\", theme=\"boardroom\")\ndeck.title_slide(subtitle=\"Board update\")\ndeck.section_divider(\"Results\")\ndeck.stat_slide(\"Key metrics\", [(\"ARR\", \"$12.4M\", \"+18%\"), (\"Churn\", \"2.1%\", \"-0.3%\")])\ndeck.bullet_slide(\"Highlights\", [\"Enterprise ARR up 22%\", \"Net retention at 118%\"])\ndeck.chart_slide(\"Growth\", chart_element)\ndeck.quote_slide(\"The window for category leadership is now.\", attribution=\"CEO\")\ndeck.closing_slide(\"Thank you\", contact=\"ana@acme.com\")\ndeck.save(\"board_deck.pdf\")\n```\n\n**Layouts**:`title_slide`\n\n,`section_divider`\n\n,`content_slide`\n\n(single or`layout=\"two-column\"`\n\n),`bullet_slide`\n\n,`stat_slide`\n\n,`chart_slide`\n\n,`quote_slide`\n\n,`code_slide`\n\n,`closing_slide`\n\n.**Four built-in themes**, each a complete, WCAG-contrast-checked color system:`boardroom`\n\n(navy and gold),`horizon`\n\n(dusk blue and coral),`carbon`\n\n(near-black and electric blue),`meadow`\n\n(forest and spring green). Aliases`default`\n\n,`dark`\n\n,`light`\n\n,`minimal`\n\nresolve to one of the four.**Fit-to-slide**: every slide is measured after composition; if it would overflow, type and spacing scale down in steps (to a floor of 0.8x) until it fits one slide, or a clear error names which slide and by how much it overflowed rather than silently clipping content.- Charts dropped into a themed deck automatically pick up the theme's chart palette unless colors are set explicitly.\n`deck.build()`\n\nreturns the underlying`Document`\n\nfor further customization before`.render()`\n\n/`.save()`\n\n.\n\n``` python\nfrom emboss import Document, LegalFeatures, PageSpec\n\ndoc = Document(\n    title=\"Memorandum of Understanding\",\n    style=\"legal\",\n    page=PageSpec.letter(margin_left=108),\n    legal=LegalFeatures(\n        watermark=\"CONFIDENTIAL\",\n        watermark_opacity=0.12,\n        line_numbering=True,\n        bates_prefix=\"ACME-\",\n        bates_start=1,\n        bates_digits=6,\n        bates_position=\"bottom-right\",\n    ),\n)\n```\n\n**Bates numbering**: sequential identifiers for legal discovery** Line numbering**: continuous line numbers in the left margin (court filings)** Watermarks**: diagonal text overlay with configurable opacity\n\nThe line between a nice PDF and a controlled document (ISO 9001, IEC 62304, medical device, aerospace) is a control block: a document identifier, version, status, an approvals table, and a revision history.\n\n``` python\nfrom emboss import Document, Approval, RevisionEntry\n\ndoc = Document(title=\"Design History File\", style=\"legal\")\ndoc.document_control(\n    doc_id=\"DHF-2026-014\",\n    version=\"3.0\",\n    status=\"Approved\",\n    effective_date=\"2026-08-01\",\n    classification=\"Controlled\",\n    owner=\"Quality Engineering\",\n    approvals=[\n        Approval(name=\"R. Patel\", role=\"Quality Manager\", date=\"2026-07-28\"),\n        Approval(name=\"M. Osei\", role=\"Regulatory Affairs\", date=\"2026-07-29\"),\n    ],\n    revisions=[\n        RevisionEntry(version=\"2.0\", date=\"2026-03-01\", author=\"R. Patel\",\n                      summary=\"Added risk analysis section\"),\n        RevisionEntry(version=\"3.0\", date=\"2026-07-28\", author=\"R. Patel\",\n                      summary=\"Updated verification protocol\"),\n    ],\n)\n```\n\nThe block expands into real, fully tagged tables before layout -- a metadata grid, an Approvals table, and a Revision History table -- so pagination, header repetition on long revision histories, and PDF/UA `/Table`\n\ntagging all come from the same machinery an ordinary `doc.table(...)`\n\nuses, not a hand-drawn panel. It round-trips through the JSON spec and renders in the HTML and Markdown adapters.\n\nFillable text, checkbox, and dropdown fields for intake and KYC-style documents, laid out in the normal document flow rather than manually positioned:\n\n```\ndoc.heading(\"Client Intake\", level=1)\ndoc.text_field(\"full_name\", label=\"Full name\")\ndoc.checkbox_field(\"accredited_investor\", label=\"I am an accredited investor\")\ndoc.dropdown_field(\"entity_type\", options=[\"Individual\", \"Corporation\", \"Trust\"],\n                   label=\"Entity type\")\n```\n\nEach field produces a genuine AcroForm widget -- `/FT /Tx`\n\n, `/Btn`\n\n, or `/Ch`\n\nwith the correct `/V`\n\n, `/Ff`\n\nflags (`required`\n\n, `multiline`\n\n, the combo bit for a dropdown's `/Opt`\n\n), and a real checkbox `/Yes`\n\n/`/Off`\n\nappearance -- not a drawn rectangle. Every field is tagged as real PDF/UA structure (a `Form`\n\n-mapped structure element with the same object-reference mechanism link annotations already use), so a document containing form fields stays PDF/UA-tagged with no regression. Fields coexist with signature fields in one `/AcroForm`\n\n, and a duplicate field name across a document raises rather than producing an AcroForm two fields silently share.\n\nA visual invoice PDF can carry a machine-readable EN 16931 Cross Industry Invoice XML inside it, for the France B2B mandate (September 2026) and EU electronic invoicing. `Document.attach_facturx`\n\nembeds the XML as a `factur-x.xml`\n\n`/AF`\n\nattachment, forces PDF/A-3, and writes the ZUGFeRD/Factur-X XMP metadata.\n\n``` python\nfrom emboss import Document, Invoice, Party, InvoiceLine\n\ninvoice = Invoice(\n    invoice_number=\"INV-2026-001\",\n    issue_date=\"20260115\",\n    currency=\"EUR\",\n    seller=Party(name=\"Acme GmbH\", country_code=\"DE\", vat_id=\"DE123456789\"),\n    buyer=Party(name=\"Beispiel SA\", country_code=\"FR\"),\n    lines=[\n        InvoiceLine(name=\"Consulting\", quantity=\"10\", unit_price=\"150.00\",\n                    net_amount=\"1500.00\", tax_rate_percent=\"19\"),\n    ],\n)\n\ndoc = Document(title=\"Invoice INV-2026-001\", style=\"finance\")\n# ... render the human-readable invoice with tables/stat tiles ...\ndoc.attach_facturx(invoice)          # embeds factur-x.xml, forces PDF/A-3\ndoc.save(\"invoice.pdf\")\n```\n\n`Invoice.validate()`\n\n(called before any XML is produced) reconciles the line net amounts, the per-rate tax, and the grand total, so an invoice whose numbers do not add up raises rather than emitting a non-conformant document. The default profile is EN 16931 (COMFORT); MINIMUM, BASIC, and EXTENDED select the guideline URN. `build_cii_xml(invoice)`\n\nreturns the XML directly if you want to inspect or transmit it on its own.\n\n``` python\nfrom emboss import HeaderFooter\n\ndoc = Document(\n    header=HeaderFooter(\n        left=\"Acme Corp\",\n        center=\"Confidential\",\n        right=\"{page} of {pages}\",\n        separator=True,\n    ),\n    footer=HeaderFooter(\n        center=\"Generated by Emboss\",\n    ),\n)\n```\n\nPlaceholders `{page}`\n\nand `{pages}`\n\nare resolved at render time.\n\nAutomatic figure, table, and equation numbering with label-based cross-references:\n\n``` python\nfrom emboss import Document, Table, Image\nfrom emboss.crossref import CrossReferenceIndex\n\ndoc = Document(title=\"Research Paper\", style=\"academic\")\ndoc.table(\n    headers=[\"Variable\", \"Value\"],\n    rows=[[\"x\", \"42\"]],\n    caption=\"Experimental results\",\n    label=\"results-table\",\n)\ndoc.paragraph(\"As shown in @results-table, the value of x is 42.\")\n```\n\nThe `@label`\n\nsyntax resolves to \"Table 1\", \"Figure 2\", etc., with automatic counter tracking via `NumberingContext`\n\n. Captions are numbered automatically (\"Figure 1: ...\", \"Table 1: ...\"), and each `@label`\n\nreference becomes a clickable in-document link (a GoTo action targeting the labeled element's page).\n\nSection numbering is available by setting `number_sections = True`\n\non the document; headings are numbered hierarchically and `@label`\n\nreferences to sections resolve to the section number.\n\nEmbed SVG vector graphics as native PDF paths, no rasterization:\n\n```\ndoc.svg(\"\"\"\n<svg width=\"200\" height=\"100\">\n  <rect x=\"10\" y=\"10\" width=\"80\" height=\"80\" fill=\"#2563eb\"/>\n  <circle cx=\"150\" cy=\"50\" r=\"40\" fill=\"#dc2626\"/>\n</svg>\n\"\"\", width=200, height=100)\n```\n\nA substantial subset of static SVG is supported, using only the standard library:\n\n**Shapes**:`rect`\n\n,`circle`\n\n,`ellipse`\n\n,`line`\n\n,`polygon`\n\n,`polyline`\n\n.**Full path data**:`path`\n\nsupports`M/L/H/V/C/S/Q/T/A/Z`\n\n, including relative lowercase forms and implicit command repetition -- elliptical arcs (`A`\n\n) are converted to cubic Bezier segments.**Transforms**:`translate`\n\n,`scale`\n\n,`rotate`\n\n(with an optional pivot),`skewX`\n\n,`skewY`\n\n, and`matrix`\n\n, composed correctly through nested`<g>`\n\ngroups.**Gradients**:`linearGradient`\n\nand`radialGradient`\n\n, including`href`\n\nstop inheritance, rendered as banded fills;`gradient_shading`\n\nbuilds the equivalent true PDF type 2/3`/Shading`\n\ndictionary for callers wiring page resources directly.**Clipping**:`clipPath`\n\ncompiles to a real PDF clip (`W n`\n\n).**Opacity**: element and group`opacity`\n\n/`fill-opacity`\n\n/`stroke-opacity`\n\nemit real`ExtGState`\n\n(`gs`\n\n) operators, not simulated transparency.**Text**:`<text>`\n\nwith`text-anchor`\n\n, using base-14 metrics mapped from the requested`font-family`\n\n.: reference-based reuse with a bounded reference depth to guard against cycles.`<use>`\n\n/`<defs>`\n\n- Excluded by design: filters, animation, masks, patterns, and CSS beyond the\n`style`\n\nattribute.\n\nNode/edge graphs (architecture diagrams, flowcharts, state machines) with automatic layout: no coordinates to compute by hand.\n\n```\ndoc.diagram(\n    nodes=[\n        {\"id\": \"api\", \"label\": \"API Gateway\"},\n        {\"id\": \"auth\", \"label\": \"Auth Service\"},\n        {\"id\": \"db\", \"label\": \"User Store\", \"shape\": \"store\"},\n        {\"id\": \"ok\", \"label\": \"Response\", \"shape\": \"rounded\"},\n    ],\n    edges=[\n        {\"src\": \"api\", \"dst\": \"auth\", \"label\": \"verify\"},\n        {\"src\": \"auth\", \"dst\": \"db\"},\n        {\"src\": \"db\", \"dst\": \"auth\", \"style\": \"dashed\"},\n        {\"src\": \"auth\", \"dst\": \"ok\"},\n    ],\n    caption=\"Login flow\",\n)\n```\n\n**Layout is automatic**: a longest-path layering assigns nodes to layers, barycenter sweeps order nodes within each layer to reduce edge crossings, and cycles are handled by reversing back edges for layout purposes while rendering them along their true direction.**Node shapes**:`box`\n\n,`rounded`\n\n,`decision`\n\n(diamond),`store`\n\n(database cylinder),`start_end`\n\n(pill).**Edges**: solid or`style=\"dashed\"`\n\n, with optional labels; self-loops render as a small loop back into the node.- Renders as native vector graphics (an\n`SvgBlock`\n\nunder the hood), with a deterministic, auto-generated`/Alt`\n\ndescription summarizing the node and edge count -- no diagram ships without accessible alt text. - Also available as a fenced Markdown block: a\n```` diagram`\n\nfence with`id: Label [shape]`\n\nnode lines,`a -> b: label`\n\n(or`-->`\n\nfor dashed) edge lines, and an optional`direction: right`\n\nline. - Lower-level API:\n`emboss.diagrams.layout_diagram`\n\n,`render_diagram_svg`\n\n,`diagram_alt_text`\n\n, and the`DiagramNode`\n\n/`DiagramEdge`\n\ndataclasses.\n\nBeyond the general flowchart, three purpose-built diagram types cover most technical-documentation needs. Each is described as a plain node/edge list, laid out automatically, and rendered as native vector graphics with auto-generated `/Alt`\n\ntext.\n\n**Cloud / deployment architecture** -- `doc.architecture_diagram(nodes, edges, groups=...)`\n\n. Nodes carry a `service`\n\nglyph (`compute`\n\n, `database`\n\n, `storage`\n\n, `queue`\n\n, `gateway`\n\n, `cache`\n\n, `cdn`\n\n, `function`\n\n, `loadbalancer`\n\n, `user`\n\n, `external`\n\n, `generic`\n\n) and an optional `group`\n\n; groups nest to draw zones such as a VPC wrapping public and private subnets. Edges are solid or `dashed`\n\nwith labels. Relabel the nodes and the same layout describes an AWS, Azure, or GCP topology.\n\n```\ndoc.architecture_diagram(\n    nodes=[\n        {\"id\": \"alb\", \"label\": \"ALB\", \"service\": \"loadbalancer\", \"group\": \"public\"},\n        {\"id\": \"ec2\", \"label\": \"EC2 - App\", \"service\": \"compute\", \"group\": \"private\"},\n        {\"id\": \"rds\", \"label\": \"RDS\", \"service\": \"database\", \"group\": \"private\"},\n    ],\n    edges=[(\"alb\", \"ec2\"), (\"ec2\", \"rds\", \"SQL\")],\n    groups=[\n        {\"id\": \"public\", \"label\": \"Public Subnet\", \"node_ids\": [\"alb\"]},\n        {\"id\": \"private\", \"label\": \"Private Subnet\", \"node_ids\": [\"ec2\", \"rds\"]},\n        {\"id\": \"vpc\", \"label\": \"VPC\", \"node_ids\": [\"public\", \"private\"]},\n    ],\n    caption=\"AWS three-tier architecture\",\n)\n```\n\n**Sequence** -- `doc.sequence_diagram(participants, messages)`\n\n. Draws one lifeline per participant; messages carry a `style`\n\nof `sync`\n\n(solid, filled arrowhead), `async`\n\n(open arrowhead), or `return`\n\n(dashed), and `activate=True`\n\nopens an activation bar on the receiver. Self-messages render as a loop.\n\n```\ndoc.sequence_diagram(\n    participants=[{\"id\": \"u\", \"label\": \"Client\"}, {\"id\": \"api\", \"label\": \"API\"}],\n    messages=[\n        {\"from\": \"u\", \"to\": \"api\", \"label\": \"POST /settle\", \"style\": \"sync\",\n         \"activate\": True},\n        {\"from\": \"api\", \"to\": \"u\", \"label\": \"202 Accepted\", \"style\": \"return\"},\n    ],\n)\n```\n\n**Entity-relationship** -- `doc.er_diagram(entities, relationships)`\n\n. Each entity lists typed attributes with a `key`\n\nof `PK`\n\n/`FK`\n\n; each relationship carries a label and `from_card`\n\n/`to_card`\n\ncardinality (`1`\n\n, `N`\n\n, ...).\n\n```\ndoc.er_diagram(\n    entities=[\n        {\"id\": \"account\", \"name\": \"Account\", \"attributes\": [\n            {\"name\": \"id\", \"key\": \"PK\", \"type\": \"uuid\"},\n        ]},\n        {\"id\": \"payment\", \"name\": \"Payment\", \"attributes\": [\n            {\"name\": \"account_id\", \"key\": \"FK\", \"type\": \"uuid\"},\n        ]},\n    ],\n    relationships=[\n        {\"from\": \"account\", \"to\": \"payment\", \"label\": \"makes\",\n         \"from_card\": \"1\", \"to_card\": \"N\"},\n    ],\n)\n```\n\nAll three are exposed in the Pydantic schema and the LLM spec prompt, so a model can emit them directly as `architecture_diagram`\n\n/ `sequence_diagram`\n\n/ `er_diagram`\n\nspec blocks.\n\nA ```` mermaid`\n\nfence in Markdown, or `parse_mermaid(source)`\n\ndirectly, parses common Mermaid diagram source onto Emboss's own diagram builders above, so the large existing corpus of docs already written in Mermaid renders as native vector graphics, not an image.\n\nmermaid\nflowchart LR\n  A[Start] --> B{OK?}\n  B -->|yes| C[(Store)]\n  B -.->|no| A\n`flowchart`\n\n/`graph`\n\n(with `TD`\n\n/`TB`\n\n/`BT`\n\n/`LR`\n\n/`RL`\n\ndirection), `sequenceDiagram`\n\n(`->>`\n\nsync, `-->>`\n\nreturn, `-)`\n\nasync, `+`\n\n/`-`\n\nactivation), and `erDiagram`\n\n(attribute blocks and `||--o{`\n\n-style cardinality) are supported. An unsupported kind (`classDiagram`\n\n, `stateDiagram`\n\n) raises `MermaidError`\n\nnaming it, rather than rendering something wrong. In Markdown, a diagram that fails to parse degrades to a code block showing the source, with a warning via `on_warning`\n\n, unless `strict=True`\n\nis passed to `parse_markdown`\n\n.\n\n``` python\nfrom emboss import parse_mermaid\n\nblock = parse_mermaid(source)   # an SvgBlock, addable with doc.add(block)\npython\nfrom emboss import Document, PageSpec\n\ndoc = Document(\n    title=\"Newsletter\",\n    page=PageSpec.a4(columns=2, column_gap=18),\n)\ndoc.heading(\"Top Stories\", level=1)\ndoc.paragraph(\"Content flows automatically across columns...\")\n```\n\nContent flows between columns automatically. Column-spanning elements (headings, wide tables) are supported via the `column_span`\n\nstyle property.\n\nLaTeX-style math rendering with support for common commands:\n\n```\ndoc.math(r\"E = mc^2\")\ndoc.math(r\"\\int_{0}^{\\infty} e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}\")\ndoc.math(r\"\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}\")\ndoc.math(r\"f(x) = \\begin{cases} x^2 & x \\ge 0 \\\\ -x & x < 0 \\end{cases}\")\n```\n\nSupported: superscripts, subscripts, fractions (`\\frac`\n\n), square roots (`\\sqrt`\n\n), integrals, summations, Greek letters, `\\text{}`\n\n, `\\mathcal`\n\n, `\\mathbb`\n\n, `\\mathbf`\n\n, `\\mathrm`\n\n, `\\operatorname`\n\n, and more.\n\n`\\begin{...}`\n\nwith `&`\n\ncolumn separators and `\\\\`\n\nrow breaks:\n\n- Matrices:\n`matrix`\n\n,`pmatrix`\n\n,`bmatrix`\n\n,`vmatrix`\n\n,`Bmatrix`\n\n- Piecewise:\n`cases`\n\n- Aligned equations:\n`aligned`\n\n,`align`\n\n,`align*`\n\n,`split`\n\n- Gathered equations:\n`gathered`\n\n,`gather`\n\n,`gather*`\n\n`\\mathbb`\n\n, `\\mathcal`\n\n/`\\mathscr`\n\n, and `\\mathfrak`\n\nrender actual double-struck, script, and fraktur glyphs (Unicode Mathematical Alphanumeric Symbols, with the letterlike-symbol exceptions like `\\mathbb{R}`\n\nand `\\mathcal{H}`\n\nmapped to their reserved codepoints) from the bundled Emboss Math font, not a font-substitution approximation:\n\n```\ndoc.math(r\"\\mathbb{R}^n \\to \\mathcal{H}\")\n```\n\nPresentation MathML is accepted alongside LaTeX. `parse_math`\n\n(and `MathBlock`\n\n/`doc.math`\n\n) auto-detects a source starting with `<math`\n\nand routes it through `emboss.mathml.parse_mathml`\n\ninto the same AST the LaTeX parser produces, so layout and rendering are shared:\n\n```\ndoc.math('<math><mfrac><mi>a</mi><mi>b</mi></mfrac></math>')\n```\n\nSupported elements: `mi`\n\n/`mn`\n\n/`mo`\n\ntokens, `mrow`\n\n, `mfrac`\n\n, `msqrt`\n\n/`mroot`\n\n, `msup`\n\n/`msub`\n\n/`msubsup`\n\n, `munder`\n\n/`mover`\n\n/`munderover`\n\n(mapped onto the display-limit machinery), `mtable`\n\n/`mtr`\n\n/`mtd`\n\n, `mfenced`\n\n, `mtext`\n\n, `mspace`\n\n, `mstyle`\n\n, and `semantics`\n\n(annotations ignored). HTML5/MathML named entities are expanded and namespaces stripped, using only the standard library; malformed input raises `ValueError`\n\nrather than rendering garbage.\n\nLayout comes from real metrics, not approximations: glyph dimensions use the base-14 AFM tables (Times italic for variables, Times roman for text, Symbol for operators), inter-atom spacing follows the TeX spacing classes (thin, medium, thick), and in display mode `\\sum`\n\n, `\\prod`\n\n, `\\int`\n\n, and `\\lim`\n\nplace their limits above and below the operator.\n\nSyntax-highlighted code with optional line numbers:\n\n``` python\ndoc.code_block(\n    code='def hello():\\n    print(\"Hello, world!\")',\n    language=\"python\",\n    line_numbers=True,\n)\n```\n\nBuilt-in highlighters for Python, JavaScript, TypeScript, SQL, JSON, HTML, CSS, Rust, Go, Java, C, and more.\n\nData visualization rendered as native PDF vector graphics:\n\n```\ndoc.chart(\n    chart_type=\"bar\",\n    labels=[\"Q1\", \"Q2\", \"Q3\", \"Q4\"],\n    values=[120, 150, 180, 210],\n    caption=\"Quarterly Revenue ($M)\",\n)\n```\n\nChart types: `bar`\n\n, `line`\n\n, `pie`\n\n, `scatter`\n\n.\n\nA finance or exec-brief pipeline produces figures from a data export, not a hand-typed list -- `table_from_csv`\n\n/`chart_from_csv`\n\nread a CSV path, file object, CSV text, or a pandas DataFrame (duck-typed; pandas stays an optional, never-required dependency) directly into a table or chart:\n\n```\ndoc.table_from_csv(\"bookings.csv\", caption=\"Bookings by region\", verify_totals=True)\ndoc.chart_from_csv(\"bookings.csv\", chart_type=\"bar\", attach_data=True)\n```\n\nBecause both methods ultimately call the existing `Document.table`\n\n/`Document.chart`\n\n, every other keyword composes for free: `verify_totals=True`\n\nrefuses to render if a CSV-sourced Total row doesn't add up, exactly as it would for a hand-typed table; `attach_data=True`\n\nembeds the exact CSV that fed the chart inside the PDF, so the visual and its source data travel as one file. Currency, thousands separators, and percent signs in CSV cells (`\"$1,234.50\"`\n\n) are parsed automatically (the same `arithmetic.parse_number`\n\nthe totals check uses), so a raw finance export plots and totals correctly without pre-cleaning. `chart_from_csv`\n\ntreats the first column as labels and every other numeric column as a series by default, or accepts explicit `value_columns=`\n\n/`category_column=`\n\nby name or index.\n\nGenerate PDF/A-2b compliant output for long-term archival:\n\n```\ndoc = Document(\n    title=\"Annual Filing\",\n    pdfa=True,       # enables PDF/A-2b conformance\n    tagged=True,     # required for PDF/A\n)\n```\n\nIncludes XMP metadata, sRGB ICC output intent, and all required PDF/A catalog entries.\n\nSet `color_mode=\"cmyk\"`\n\nand every draw path (text, rules, tables, charts, shapes) emits CMYK operators (`k`\n\n/`K`\n\n) instead of RGB:\n\n``` python\nfrom emboss import Document, Paragraph, TextRun\n\ndoc = Document(\n    title=\"Print Run\",\n    color_mode=\"cmyk\",\n    pdfa=True,   # output intent switches to a CMYK profile (N=4)\n)\ndoc.paragraph([TextRun(text=\"Brand red\", color=\"cmyk(0,100,95,0)\")])\ndoc.paragraph([TextRun(text=\"Spot ink\", color=\"spot(PANTONE 485 C,0,100,95,0)\")])\ndoc.save(\"print_run.pdf\")\n```\n\n`cmyk(c,m,y,k)`\n\ncolor strings work anywhere a color is accepted; RGB colors are converted when the document is in CMYK mode`spot(name,c,m,y,k)`\n\ndefines a named spot color, emitted as a PDF Separation color space with a CMYK fallback- PDF/A output in CMYK mode uses a CMYK OutputIntent instead of sRGB\n- Redaction and signature appearances follow the document's color mode\n\nSign PDFs with X.509 certificates. A visual signature field is placed at render time; the PKCS#7 signature is injected afterward:\n\n``` python\nfrom emboss import Document, SignatureField\nfrom emboss.signing import sign_pdf\n\ndoc = Document(\n    title=\"Approval\",\n    signatures=[\n        SignatureField(\n            page_index=0, x=360, y=60, signer_name=\"Jane Doe\",\n            reason=\"Approved\", location=\"San Francisco\",\n        )\n    ],\n)\npdf_bytes = doc.render()\n\nsigned_bytes = sign_pdf(pdf_bytes, \"signer-key.pem\", \"signer.pem\")\n```\n\nRequires `pip install emboss-pdf[signing]`\n\n.\n\n`sign_pdf(..., certify=True, docmdp_permission=2)`\n\nproduces an ISO 32000-1 12.8.2.3 certification signature: the /DocMDP entry it asserts states what changes (if any) are permitted after signing (`1`\n\nforbids any further changes, `2`\n\n-- the default -- permits form fill-in and further signing, `3`\n\nadditionally permits annotations). A certifying signature must be the document's first signature field; `build_docmdp_reference`\n\n, `build_certifying_signature`\n\n, and `build_perms_dict`\n\n(in `emboss.signing`\n\n) build the underlying /Reference, signature field, and catalog /Perms entries for callers assembling the PDF at a lower level than `Document.render`\n\n.\n\nKeep the distinction sharp: **content edits go through the spec ( render, a new file); attestations append.** Signatures, approvals, and annotations are added by appending an incremental revision that never rewrites the bytes already in the file. The original bytes stay a byte-exact prefix of the result, which is exactly what lets a signature's\n\n`/ByteRange`\n\nattest to everything written before it.\n\n``` python\nfrom emboss import amend_sign, revision_history, format_history\n\nsigned = amend_sign(pdf_bytes, cert=\"legal.pem\", key=\"legal.key\",\n                    reason=\"Approved: Legal\", name=\"R. Patel\")\nsigned = amend_sign(signed, cert=\"finance.pem\", key=\"finance.key\",\n                    reason=\"Approved: Finance\", name=\"M. Osei\")\n\nprint(format_history(signed))\nRev  Kind        Bytes             Signer        Coverage\n--------------------------------------------------------------\n  0  base        0-6582                          covers by rev 1,2\n  1  signature   6582-24003        R. Patel      signed\n  2  signature   24003-41560       M. Osei       signed\n```\n\nThe headline check is coverage: a revision appended *after* a signature that no signature's `/ByteRange`\n\ncovers is detected and reported. That is the audit question -- what was added that nobody signed -- and it falls out of the `/Prev`\n\nchain for free.\n\n```\nemboss amend contract.pdf --sign --cert legal.pem --key legal.key --reason \"Approved: Legal\"\nemboss history contract.pdf\nemboss verify contract.pdf --revisions      # exits non-zero on uncovered appended content\n```\n\n`amend_pdf`\n\nhandles non-signature attestations (a placed annotation, an added attachment) the same append-only way. DocMDP is enforced: if the base certifies with `/P=1`\n\n(no changes) or `/P=2`\n\n(signatures only), an amendment that exceeds the permitted scope raises rather than producing an illegal file. Amended files are valid but no longer linearized, since the appended revision sits after the original `%%EOF`\n\n-- inherent to incremental updates, and the price of never rewriting prior bytes.\n\nDeterminism, restated for signing.Each revision is byte-identical to what it was when created. Amendments append; they never rewrite. The document remains reproducible from its embedded spec at any revision. This is stronger than a blanket \"byte-identical output\" claim, because it survives signing.\n\nTwo redaction models are available, and they are not equally honest. **Construction-time redaction** (`Document.redact`\n\n) is the one to use for anything that actually matters: it matches whole content blocks -- by stable node id, a regex or predicate over the block's plain text, or element type -- and removes or replaces them *before* layout or rendering, so the real text never reaches a content stream.\n\nBecause redaction runs before the document embeds anything, a removed block is absent from **every** embedded artifact too, not just the page: the `emboss-spec.json`\n\n, the text index, the layout map, the Markdown twin, the reproducibility manifest, and any table's attached CSV. This is verified in `tests/test_redaction_leak.py`\n\n, and it is the property that keeps \"self-describing\" from quietly defeating \"redacted\". One honest limitation: redaction is **block-granular**. A value you keep in an un-redacted block stays there, including in a kept table's embedded CSV, so to remove a value from a table you redact the table (or drop `attach_data`\n\n), not just a paragraph elsewhere.\n\n``` python\nfrom emboss import Callout, RedactionRule\n\nrules = [\n    RedactionRule(name=\"ssn\", pattern=r\"\\d{3}-\\d{2}-\\d{4}\"),\n    RedactionRule(name=\"internal-notes\", element_type=Callout, mode=\"remove\"),\n]\nredacted_doc = doc.redact(rules)\npdf_bytes = redacted_doc.render()\n\nredacted_doc.redaction_log   # what was removed, by which rule, before removal -- an audit trail, never auto-attached to output\n```\n\n`mode=\"placeholder\"`\n\n(the default) replaces a matched block with same-length filler text so the layout doesn't visibly reflow, then covers the filler's own rendered footprint with an opaque box -- the box conceals filler, never the original content. `mode=\"remove\"`\n\ndrops the block outright. Placeholder mode covers the common text-bearing block types (`Heading`\n\n, `Paragraph`\n\n, `BlockQuote`\n\n, `Callout`\n\n, `Footnote`\n\n, `PullQuote`\n\n, `BulletList`\n\n, `NumberedList`\n\n, `Table`\n\n, `CodeBlock`\n\n); match other element types with `mode=\"remove\"`\n\n.\n\nThe older `RedactionMark`\n\n/`Document.redactions`\n\nmechanism draws opaque rectangles directly onto an already-rendered page and is kept for callers masking content that never went through a `Document`\n\nat all (it is also what the [diff/redline](#document-diff-and-redline) change-bars use) -- the underlying text is still in the content stream underneath the box, so it is not a substitute for construction-time redaction of real secret text.\n\nFor payloads that need to travel with the PDF but stay opaque to anyone without a password, `Document.attach_encrypted(name, data, password)`\n\nqueues an AES-256-GCM-encrypted `/AF`\n\nattachment (`relationship=\"EncryptedPayload\"`\n\n), decrypted later with `emboss.decrypt_attachment`\n\n.\n\nConstraints are checked before rendering. Repairable issues are fixed automatically; genuine errors are reported:\n\n``` python\nfrom emboss import ConstraintValidator\n\nresult = ConstraintValidator().validate(doc)\nfor issue in result.issues:\n    print(issue)\n# fixed/mathematical: column widths rescaled to fit page\n# fixed/structural: orphan heading moved to next page\n```\n\nExport to multiple formats from a single document spec:\n\n``` python\nfrom emboss.adapters.html_export import to_html\nfrom emboss.adapters.markdown_export import to_markdown\nfrom emboss.adapters.docx_export import to_office_dict\n\nhtml_str = to_html(doc)\nmd_str = to_markdown(doc)\noffice_data = to_office_dict(doc)\npython\nfrom emboss.adapters.pydantic_schema import DocumentSpec, generate_json_schema\n\n# Get the JSON Schema for LLM structured output\nschema = generate_json_schema()\n\n# Parse an LLM-generated JSON document\ndoc = DocumentSpec.model_validate_json(json_string).to_document()\npdf_bytes = doc.render()\nDocument(\n    title=\"\",              # Document title (appears in PDF metadata and title block)\n    author=\"\",             # Author name\n    subject=\"\",            # Subject line\n    keywords=\"\",           # Comma-separated keywords\n    language=\"en-US\",      # BCP 47 language tag\n    style=\"corporate\",     # Preset name or StyleSheet instance\n    brand=None,            # BrandKit layered on top of `style`\n    page=PageSpec(),       # Page dimensions and margins\n    page_styles={},        # Name -> PageSpec, switched via PageBreak.page_style\n    content=[],            # List of block elements\n    header=None,           # HeaderFooter for page headers\n    footer=None,           # HeaderFooter for page footers\n    page_numbers=True,     # Show page numbers\n    tagged=True,           # Generate PDF/UA structure tree\n    legal=None,            # LegalFeatures (Bates, line numbers, watermark)\n    pdfa=False,            # Generate PDF/A-2b (or -3b with embed_spec) output\n    toc=False,             # Generate table of contents\n    color_mode=\"rgb\",      # \"rgb\" or \"cmyk\" (print production)\n    signatures=None,       # Digital signature fields\n)\ndoc.heading(text, level=1)\ndoc.paragraph(content)\ndoc.bullets(items)\ndoc.numbered(items)\ndoc.table(headers, rows)\ndoc.image(source, width=None, caption=None)\ndoc.chart(chart_type, labels, values)\ndoc.diagram(nodes, edges=(), direction=\"down\", caption=None)\ndoc.code_block(code, language=\"text\")\ndoc.math(source)\ndoc.footnote(content)\ndoc.callout(content, variant=\"note\")\ndoc.svg(source)\ndoc.rule()\ndoc.page_break(page_style=None)\ndoc.cover(title); doc.abstract(text); doc.authors(authors); doc.pull_quote(text)\ndoc.stat_tiles(stats); doc.table_of_contents(); doc.appendix(title, *blocks)\ndoc.index(); doc.glossary(entries)\ndoc.render(linearize=False, embed_spec=False) -> bytes\ndoc.save(path, linearize=False, embed_spec=False)\ndoc.layout_map() -> dict\n\nDocument.from_pdf(source, strict=False)   # recover a Document from a rendered PDF\n```\n\n- Layout is pure Python with Knuth-Plass optimal line breaking\n- Font metrics are cached with LRU caching\n- A typical 10-page document renders in ~50-100ms\n- 200-page documents are supported with ~20-50MB peak memory\n- O(n) linear scaling with document size\n\n```\ngit clone https://github.com/GGChamp85/Emboss.git\ncd Emboss\npython -m venv .venv\nsource .venv/bin/activate\npip install -e \".[dev]\"\npytest                          # run all tests\npytest tests/test_emboss.py     # run core tests\npytest -q                       # quiet mode\nruff check src/ tests/          # linting\nmypy src/emboss/                # type checking\npython examples/financial_report.py\npython examples/legal_pleading.py\npython examples/showcase.py\nphp\nDocument (EmbossSpec)\n    |\n    v\nConstraintValidator --> validate + auto-fix\n    |\n    v\nLayoutEngine --> measure (font metrics) --> paginate (Knuth-Plass)\n    |\n    v\nRenderer --> ContentStream (PDF operators) + StructureTree (PDF/UA tags)\n    |\n    v\nPDFAssembler --> byte-exact PDF with content-derived /ID\nsrc/emboss/\n  spec.py               # Document data model (EmbossSpec)\n  writer.py             # Render pipeline: measure -> paginate -> render\n  styles.py             # Cascading style system + presets\n  constraints.py        # Validation + auto-fix\n  layout/\n    engine.py           # Layout engine: measurement + pagination\n  typography/\n    font_metrics.py     # Glyph metrics, kerning, subsetting\n    line_breaking.py    # Knuth-Plass line breaker\n    hyphenation.py      # Knuth-Liang hyphenation\n    ligatures.py        # fi/fl/ffi/ffl substitution\n    numbers.py          # Deterministic display number formatting\n  pdf/\n    assembler.py        # Low-level PDF object writer\n    fonts.py            # Font resource building + subsetting\n    objects.py          # PDF object types (Dict, Array, Stream)\n    streams.py          # Content stream operators (text, shapes)\n    tags.py             # PDF/UA structure tree builder\n    verify.py           # Structural verification + real veraPDF conformance\n    attachments.py      # /AF embedded-file attachments (PDF/A-3)\n  adapters/\n    html_export.py      # HTML export\n    markdown_export.py  # Markdown export\n    docx_export.py      # DOCX export\n    pydantic_schema.py  # JSON Schema / Pydantic models for LLM\n  math_render.py        # LaTeX math parser + renderer, math alphabets\n  mathml.py             # Presentation MathML parser\n  code_highlight.py     # Syntax highlighting\n  charts.py             # Chart rendering\n  chart_facts.py        # Chart fact extraction + caption verification\n  images.py             # Image handling\n  svg.py                # SVG parser + renderer (paths, gradients, clipping)\n  diagrams.py           # Node/edge diagram layout + SVG rendering\n  crossref.py           # Cross-reference resolution\n  numbering.py          # Figure/table auto-numbering\n  templates.py          # Document templates\n  bibliography.py       # Citation formatting\n  colors.py             # Color palettes + theming\n  brandkit.py           # Versioned brand object\n  bundled_fonts.py      # Bundled OFL font set + registration\n  intelligence.py       # Content analysis + smart typography\n  toc.py                # Table of contents generation\n  pdfa.py               # PDF/A-2b/A-3b conformance\n  signing.py            # Digital signatures\n  redaction.py          # Content redaction\n  slides.py             # SlideDeck + slide/presentation layout\n  nodeid.py             # Stable node ids + layout map\n  recovery.py           # Spec embedding, from_pdf recovery, strip_pdf\n  manifest.py           # Reproducibility manifest + reproduce()\n  diff.py               # Node-keyed document diff + redline rendering\n  generate.py           # LLM prompt, structured generation, spec parsing\n  markdown.py           # Markdown -> EmbossSpec parser\n```\n\n| Phase | Scope | Status |\n|---|---|---|\n| 1 | Core engine, typography, layout, tagging | Done |\n| 2 | Unicode CIDFont, code highlighting, math, bibliography, charts | Done |\n| 3 | Images, SVG, TOC, multi-column, templates | Done |\n| 4 | PDF/A, redaction, digital signatures | Done |\n| 5 | Cross-references, custom headers/footers, numbered lists | Done |\n| 6 | Micro-typography, GPOS kerning, performance, CMYK | Done |\n| 7 | BrandKit, SlideDeck, diagrams, MathML, real veraPDF conformance, self-describing PDFs (`embed_spec` /`from_pdf` /`strip` ), document diff and redline, reproducibility manifest, construction-time redaction, DocMDP certification signatures, encrypted attachments, node-scoped patching |\nDone |\n| 8 | Text-position index and annotation round-trip, incremental amendment with signature coverage, MCP server, Factur-X/ZUGFeRD, table arithmetic validation, PAdES/eIDAS baseline signatures, PDF/X-4, WTPDF 1.0, Mermaid parsing, include-from-source, `emboss build` , controlled-document block |\nDone |\n\nApache-2.0. See [LICENSE](/GGChamp85/Emboss/blob/main/LICENSE) for the full text.", "url": "https://wpnews.pro/news/show-hn-emboss-a-python-pdf-engine-that-keeps-documents-as-structured-data", "canonical_source": "https://github.com/GGChamp85/Emboss", "published_at": "2026-07-30 06:11:15+00:00", "updated_at": "2026-07-30 06:22:37.102720+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "generative-ai"], "entities": ["Emboss", "Python", "PDF/UA", "EU Accessibility Act", "Knuth-Plass", "fonttools", "pydantic", "pikepdf"], "alternates": {"html": "https://wpnews.pro/news/show-hn-emboss-a-python-pdf-engine-that-keeps-documents-as-structured-data", "markdown": "https://wpnews.pro/news/show-hn-emboss-a-python-pdf-engine-that-keeps-documents-as-structured-data.md", "text": "https://wpnews.pro/news/show-hn-emboss-a-python-pdf-engine-that-keeps-documents-as-structured-data.txt", "jsonld": "https://wpnews.pro/news/show-hn-emboss-a-python-pdf-engine-that-keeps-documents-as-structured-data.jsonld"}}