Show HN: Emboss – a Python PDF engine that keeps documents as structured data 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. 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. python from emboss import Document doc = Document title="Q3 Report", style="finance" doc.heading "Revenue Analysis", level=1 doc.paragraph "Revenue increased 12% year over year." doc.table headers= "Region", "Q3" , rows= "North America", "$2.4M" doc.save "report.pdf" No coordinates. No manual page-break handling. No separate accessibility pass. Emboss treats a PDF as structured, self-describing data with a deterministic, accessible, verifiable lifecycle, not as final ink. Six properties set it apart: - 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. - Pure-Python core, one hard dependency. Only fonttools is required; pydantic, pikepdf, cryptography, and the MCP SDK are optional extras. Easy to vendor, audit, and deploy in restricted environments. - Byte-identical output. No timestamps, no random ids; the file /ID is 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. - 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. - 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. - 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. The one-line version: an Emboss document is structured data you can also read. 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 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. 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. | Outcome | How Emboss delivers it | |---|---| Trust & audit | Byte-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. | Lower cost | LLM edits are node-scoped: changing one paragraph in a 50-page report costs a paragraph's tokens, not the report's. | Data never drifts | Tables 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. | Compliance built in | PDF/UA accessibility, PDF/A archival, Factur-X/ZUGFeRD e-invoicing, and PAdES/eIDAS signatures, all in one engine see the table below . | AI-native distribution | An 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. | | Standard | What it is for | Regulatory driver | |---|---|---| PDF/UA-1 | Accessibility tagged, assistive-tech ready | EU Accessibility Act June 2025 , US Section 508, EN 301 549 | PDF/A-2b, PDF/A-3b | Archival / long-term retention | Records retention, filing ingest | Factur-X / ZUGFeRD EN 16931 | Electronic invoicing XML inside the PDF | France B2B mandate Sept 2026 , EU e-invoicing | PAdES B-B / B-T ETSI EN 319 142 | Legally recognized e-signatures | eIDAS EU | PDF/X-4 ISO 15930-7 | Print / prepress output | Commercial print | WTPDF 1.0 Reuse | Clean re-extraction for RAG / reuse | AI and data pipelines | Every conformance claim is checkable: emboss verify out.pdf --conformance ua1 also 2b , 3b runs the real veraPDF validator, and the CI conformance job runs it on every push, not a mock. | Capability | Emboss | Typical PDF tools ReportLab, WeasyPrint, Prince, headless Chrome | |---|---|---| | Rendering | Native typesetting engine, no browser | HTML-to-PDF or a browser engine | | Determinism | Byte-identical, hash-verifiable | Timestamps / random ids; not guaranteed | | Accessibility | PDF/UA tagged by default, veraPDF-checked | Manual or absent | | Data in the file | Table carries its own CSV; spec + text index embedded | Table flattened to ink; data gone | | Edit after render | Node-scoped patch, structure preserved | Regenerate, or overlay hacks | | Review round-trip | Annotation resolves to node + character range | No structure to resolve against | | Signatures | PKCS 7, DocMDP, PAdES, append-only coverage check | Basic or none | | AI integration | First-class MCP server, grounded answers | None | | Dependencies | Pure Python, fonttools only rest optional | Native libs / a browser runtime | Dependency security. The only hard dependency is fonttools ; pydantic , pikepdf , cryptography , and mcp are optional extras, and Emboss never imports two of their own transitive dependencies pillow , starlette directly. Because those two set no upper bound, Emboss pins a known-patched floor on them in its own project.optional-dependencies pillow =12.3.0 , starlette =1.3.1 , so pip install emboss-pdf verify / mcp / dev / all resolves to a patched transitive chain rather than whatever floor those packages declare. pip-audit against the full dependency set base plus every extra reports no known vulnerabilities. Emboss 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. Deterministic, hash-verifiable output. The same Document always renders to the same bytes: no timestamps, no random identifiers, the file /ID derived 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 also ua1 , 3b 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 . Self-describing PDFs. doc.render embed spec=True embeds the document's own EmbossSpec JSON, its node-keyed layout map, and a reflowable Markdown twin as real /AF file attachments. Document.from pdf reconstructs 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 : the foundation for auditing, redlining, and programmatic review of what landed where on the page. Document diff and redline. diff documents matches 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 turns 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 from the command line. A reproducibility manifest. doc.render manifest=True attaches a deterministic emboss-manifest.json : the spec's sha256, the Emboss version, every embedded font's sha256, and any non-default render options. emboss.reproduce and emboss reproduce report.pdf closes the loop: recover the document, re-render it, and structurally verify the two PDFs agree. Redaction by construction. Document.redact rules matches 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=... produces an ISO 32000-1 certification signature declaring what changes if any are permitted after signing. PAdES eIDAS baseline signatures. sign pdf pades ... / amend sign pades ... produce 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 embeds 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, ... emits an ISO 15930-7 print/prepress file with a CMYK output intent; Document wtpdf=True declares Well-Tagged PDF 1.0 Reuse conformance, and verify wtpdf self-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 Emboss 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. Every 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. Where Emboss is differentiated | Dimension | Emboss | Typical general-purpose PDF tools | |---|---|---| | Input a model can be constrained to | JSON schema, provider-native structured outputs, Markdown | Imperative code or HTML/CSS; no schema contract | | Validation and repair of imperfect input | Built in truncation repair, per-block recovery | Not provided | | Deterministic byte-identical output | Yes, test-verified | Generally not guaranteed | | PDF/UA accessibility tagging | On by default | Opt-in or unavailable | | Conformance evidence | Real veraPDF check in CI and emboss verify | Varies; rarely built in | | Self-describing, recoverable document | embed spec + from pdf | No | | Stable node ids and layout map | Yes | No | | Document diff to a redlined PDF | Yes | No | | Redaction that never enters the content stream | Yes | Typically overlay-based | Where established tools are ahead This side matters just as much. Stated plainly: | Dimension | Emboss | Typical general-purpose PDF tools | |---|---|---| | Complex-script shaping Arabic, Indic | Not supported | Several engines support it | | Arbitrary HTML and CSS input | Not supported | HTML/CSS engines accept it | | Image formats | PNG and JPEG | Often broader | | Production maturity | New, released 2026 | Many have 10 to 20 years in production | For 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. Existing 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. Emboss 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. Deterministic output. The same document produces byte-identical PDFs across runs and machines. No timestamps, no random identifiers; the file /ID is derived from content. This makes output hash-verifiable for filings and diffable in CI. assert doc.render == doc.render always true Structural correctness. Cross-reference offsets are recorded as bytes are written, so the xref table cannot disagree with the file. Content never overflows. Everything is measured before anything is placed, so text running off the page is not a failure mode. Tagged by default. Every document ships with a complete PDF/UA structure tree. pip install emboss-pdf Requires Python 3.10+ and fonttools . Optional extras: pip install emboss-pdf all pydantic + pikepdf + cryptography pip install emboss-pdf llm pydantic schemas for LLM structured output pip install emboss-pdf verify pikepdf for PDF structural verification pip install emboss-pdf signing cryptography for digital signatures pip install emboss-pdf dev full dev environment pytest, mypy, ruff An 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 / \mathcal / \mathfrak . Registering the set gives embedded fonts with full kerning and ligatures, no system font files required: python from emboss import Document from emboss.bundled fonts import register bundled fonts doc = Document title="Report" register bundled fonts doc.fonts The three text families are also available under the aliases "emboss serif" , "emboss sans" , and "emboss mono" . Two lower-level helpers, bundled font path family, bold=, italic= and BUNDLED FAMILIES , are available for callers that want the file paths directly. python from emboss import Document doc = Document title="Quarterly Report", author="Finance Team", style="finance" doc.heading "Executive Summary", level=1 doc.paragraph "Revenue grew 12% year over year, driven by enterprise expansion." doc.table headers= "Metric", "Q3 2025", "Q3 2024", "Change" , rows= "Revenue", "$24.1M", "$21.5M", "+12%" , "EBITDA", "$8.2M", "$6.9M", "+19%" , , doc.bullets "North America: +15%", "EMEA: +8%", "APAC: +11%" doc.save "quarterly report.pdf" python from emboss.spec import Document, Heading, Paragraph, Table doc = Document title="Quarterly Report", style="finance", content= Heading text="Executive Summary", level=1 , Paragraph content="Revenue grew 12% year over year." , Table headers= "Metric", "Value" , rows= "Revenue", "$24.1M" , "EBITDA", "$8.2M" , , , pdf bytes = doc.render Tier 1: Spec prompt full quality, recommended -- pass the EmbossSpec format with your prompt: python from emboss import spec prompt, Document system = spec prompt style="finance" compact EmbossSpec description for LLMs response = client.messages.create model="claude-sonnet-5", system=system, messages= {"role": "user", "content": "Create a Q3 financial report"} , doc = Document.from json response.text doc.save "report.pdf" Tier 2: One-liner zero friction : python from emboss import generate generate "Create a quarterly financial report", style="finance", output="report.pdf", provider="anthropic" or "openai" Tier 3: Markdown works with any LLM output : python from emboss import Document md = llm.generate "Write a quarterly report..." any LLM, any provider doc = Document.from markdown md, style="finance" doc.save "report.pdf" All three tiers produce identical PDF quality -- the typography engine Knuth-Plass, optical margins, kerning runs the same regardless of input format. A whole directory, one PDF. A documentation team adds a build step rather than calling an API: emboss build ./docs -o handbook.pdf Files are concatenated in alphabetical order numeric prefixes like 01-intro.md work naturally , or from an explicit .order file listing names one per line; the first file's front matter supplies the title and style. Available in Python as build from directory "./docs" . Code that cannot drift from its source. A fenced code block can load its body from a file instead of being typed inline: python file=examples/settle.py lines=10-20 lines=A-B selects a 1-based inclusive range; marker=NAME selects a region NAME / endregion or BEGIN NAME / END NAME 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=... . Robust parsing. LLM output is messy; the parser is built for it: doc = Document.from json text, strict=False, default: repair instead of raising smart=True, content intelligence: typography, tables, auto-style on warning=print, one message per repair performed spec prompt teaches 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 - Truncated JSON cut off mid-generation is repaired before parsing - When validation fails, recovery is per block: invalid blocks are coerced to paragraphs or dropped, the rest of the document renders strict=True raises on any malformed input instead of repairing Specs can also request layout features directly: "toc": true inserts an auto-generated table of contents, and "page": {"columns": 2, "column gap": 18} sets multi-column geometry. Documents 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. The spec is the single source of truth. From it, Emboss derives: - The visual PDF typography, layout, pagination - The accessibility structure tree PDF/UA tags - HTML, Markdown, and DOCX exports via adapters { "title": "Annual Report", "style": "corporate", "content": {"type": "heading", "text": "Overview", "level": 1}, {"type": "paragraph", "content": "Company performance exceeded targets."}, {"type": "table", "headers": "Q1", "Q2" , "rows": "$1M", "$1.2M" } } | Element | Description | Key Parameters | |---|---|---| Heading | Section heading H1-H6 | text , level 1-6 | Paragraph | Body text with inline formatting | content str, TextRun, or list | BulletList | Unordered list with nesting | items strings or sublists | NumberedList | Ordered list with nesting | items , start | Table | Data table with header row | headers , rows , caption | Image | Embedded image | source path or bytes , width , caption | CodeBlock | Syntax-highlighted code | code , language , line numbers | MathBlock | LaTeX-style math notation | source , display | Chart | Data visualization | chart type , labels , values | Callout | Admonition box note/warning/tip | content , variant , title | Footnote | Numbered footnote | content | SvgBlock | Inline SVG vector graphic | source , width , height | BibliographyBlock | Formatted citation list | citations | HorizontalRule | Visual separator | thickness , color | PageBreak | Force a page break optionally to a named page styles geometry | page style | BlockQuote | Quoted passage set off with an accent bar | content , attribution | CoverPage | Full-page title composition, forces a page break | title , subtitle , authors , date , kicker | Abstract | Indented abstract block with a keywords line | text , keywords | Authors | Centered grid of author entries | authors name, affiliation, email | PullQuote | Large-type offset quotation | text , attribution | StatTiles | Row of bordered statistic tiles, colored deltas | stats label, value, delta | TableOfContents | Visible listing with dot leaders and page numbers | title , depth , source headings/figures/tables | Appendix | Lettered section Appendix A, B, ... with its own A.1 numbering | title , content | Index | Back-of-book index resolved from index terms marks | title | Glossary | Alphabetized term/definition list with auto-linked first occurrences | entries , title | | Diagram | Node/edge graph with automatic layout, via doc.diagram ... | nodes , edges , direction , caption | python from emboss import Paragraph, TextRun Paragraph content= TextRun text="Regular text, " , TextRun text="bold text", bold=True , TextRun text=", " , TextRun text="italic text", italic=True , TextRun text=", and " , TextRun text="colored text", color="2563eb" , Seven professionally designed stylesheets are built in. Each encodes type scale, leading, table rules, and spacing so output looks authored without choosing a single measurement. | Preset | Body Font | Heading Font | Body Size | Alignment | Use Case | |---|---|---|---|---|---| legal | Times | Times | 11.5pt | Justified | Contracts, briefs, pleadings | finance | Helvetica | Helvetica | 10pt | Left | Reports, filings, data sheets | academic | Times | Helvetica | 11.5pt | Justified | Papers, dissertations, theses | corporate | Helvetica | Helvetica | 10.5pt | Left | Memos, policies, manuals | minimal | Helvetica | Helvetica | 9.5pt | Left | Data exports, compact reports | journal | Times | Times | 10.5pt | Justified | Journals, periodicals | brief | Helvetica | Helvetica | 10.5pt | Left | Executive briefs, one-pagers | Use a preset by name doc = Document style="finance" Or create a custom stylesheet from emboss import Style, StyleSheet custom = StyleSheet name="custom", body=Style font family="Helvetica", font size=11.0, align="justify" , h1=Style font size=18.0, bold=True , ... other elements doc = Document style=custom A BrandKit is 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. python from emboss import Document, BrandKit brand = BrandKit name="Acme", version="2.1", primary="1f4e79", accent="1f8a70", ink="1a1a1a", muted="6b7280", heading font="Emboss Serif", body font="Emboss Sans", footer text="Acme Corp -- Confidential", doc = Document title="Board Deck", style="corporate", brand=brand - Heading and table-header colors take the brand primary ; body and secondary text take ink / muted . 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 returns a deterministic N-color chart palette derived from primary and accent or the explicit palette tuple, padded with derived colors if it runs short -- charts under a brand stay on-brand without hand-picked colors. brand.to dict / BrandKit.from dict round-trip a brand kit through JSON logo bytes travel as base64 , for storing a brand centrally and loading it per render. Emboss 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: greedy widths: 216, 172, 168, 162, 176 variance 367 optimal widths: 216, 224, 202, 198 variance 110 The algorithm: - Models text as Box word , Glue elastic space , and Penalty breakpoint items - Considers all legal breakpoints simultaneously - Penalizes abrupt spacing changes and tight/loose lines - Flags hyphen breaks and caps hyphen ladders: a large demerit stops runs of more than 3 consecutive hyphenated lines - Splits any word wider than the measure into character-level pieces long URLs, identifiers , so lines never overflow even in narrow columns - Falls back to greedy breaking only for pathological input The full Knuth-Liang en-US pattern set is bundled, plus a no-break list for domain terms Inc. , LLC , EBITDA , plaintiff that should never be split. Glyph advances and kerning come from the font file via fonttools . Text is emitted with the PDF TJ operator 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. Base-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 . Automatic curly quotes, em/en dashes, fractions, ellipses, and non-breaking spaces before units. Applied during text processing, not as a post-process. Automatic 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. Register 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: python from emboss import Document, TextRun doc = Document title="Report" doc.fonts.register "cjk", "/path/to/NotoSansJP-Regular.ttf" doc.paragraph TextRun "日本語のテストです。", font family="cjk" Base-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. 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. python from emboss import PageSpec Standard sizes page = PageSpec.letter 8.5 x 11 in page = PageSpec.a4 210 x 297 mm page = PageSpec.legal 8.5 x 14 in Custom dimensions and margins in points, 72pt = 1 inch page = PageSpec width=612, height=792, margin top=72, margin bottom=72, margin left=90, margin right=72, Multi-column layout page = PageSpec.a4 columns=2, column gap=18 A4 landscape, tuned for wide tables and diagrams page = PageSpec.a4 landscape=True Also built in: PageSpec.a5 and the tight-margin PageSpec.compact A5, tuned for phone and tablet PDF readers . A document can switch page geometry mid-flow for one wide table or diagram, then switch back, via page styles and PageBreak.page style : doc = Document title="Annual Report", page styles={"wide": PageSpec.a4 landscape=True }, doc.paragraph "Portrait content here." doc.page break page style="wide" subsequent pages use the wide geometry doc.table headers= ... , rows= ... doc.page break no page style: reverts to the document default 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 Every document is tagged with a complete PDF/UA structure tree: - Headings become /H1 through /H6 - Paragraphs become /P - Tables become /Table with /TH and /TD cells; header cells carry /Scope - Lists become /L with /LI , /Lbl , and /LBody - Images, SVG blocks, and charts get /Figure with the alt text emitted as /Alt a description is auto-derived when none is given - Hyperlinks are written as /Link annotations and tagged as /Link structure elements with OBJR references, as PDF/UA requires - Running heads, page numbers, Bates stamps, and watermarks are marked /Artifact - Tagged documents declare the PDF/UA-1 identifier in their XMP metadata python from emboss.pdf.verify import verify pdf result = verify pdf doc.render print result VerificationReport ok=True, has struct tree=True, ... verify pdf is 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. For actual ISO conformance, emboss.pdf.verify.verify conformance and the emboss verify --conformance CLI 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 job in .github/workflows/ci.yml installs the official veraPDF binary and runs the test suite against it on every push and pull request. emboss verify report.pdf --conformance 2b PDF/A-2b emboss verify report.pdf --conformance ua1 PDF/UA-1 emboss verify report.pdf --conformance 3b PDF/A-3b with attachments python from emboss.pdf.verify import verify conformance report = verify conformance doc.render , flavour="ua1" print report.compliant bool print report.violations list RuleViolation : clause, description, count Requires the verapdf executable on PATH , or VERAPDF PATH pointing at it; otherwise verify conformance raises with an install hint rather than silently skipping the check. A rendered PDF can carry enough of its own provenance to be reconstructed later, without a side channel or a database lookup. doc = Document title="Q3 Report", style="finance" doc.heading "Revenue", level=1 doc.paragraph "Revenue increased 12% year over year." pdf bytes = doc.render embed spec=True embed spec=True attaches three real /AF associated file attachments to the PDF: | Attachment | Contents | /AFRelationship | |---|---|---| emboss-spec.json | The canonical EmbossSpec JSON for exact reconstruction | Source | emboss-layout.json | The node id - page/bounding-box layout map | Supplement | emboss-textmap.json | The node id - per-character text-position index | Supplement | emboss-doc.md | A reflowable Markdown twin of the document | Alternative | python from emboss import Document recovered = Document.from pdf "report.pdf" Document.from pdf tries the embedded emboss-spec.json first, an exact reconstruction. If it is missing or strict=True is 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 to require the exact path and raise instead. layout = doc.layout map {"n1a2b3c4": {"page": 0, "x0": 72.0, "y0": 690.2, "x1": 540.0, "y1": 706.4} , ...} Every 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 resolves 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 recovery, 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." Before a PDF leaves the building, emboss strip removes all of this: emboss strip report.pdf -o external.pdf python from emboss import strip pdf clean bytes = strip pdf pdf bytes strip pdf operates on already-rendered bytes: it drops the /AF attachments and the /Names /EmbeddedFiles tree, clears /Producer and /Creator plus the XMP CreatorTool /document-history fields, and removes the structure tree's /IDTree and per-element /ID node ids -- while keeping Title, Author, and date fields. Output stays deterministic. A 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 : doc.table headers= "Quarter", "Bookings" , rows= "Q1", "1,240" , "Q2", "1,510" , "Q3", "1,880" , caption="Bookings by quarter", attach data=True, embeds the table as an /AF CSV attachment Emboss writes the table or the chart's series out as a CSV and embeds it as a real /AF associated-file attachment on that element -- not a separate side file. The visible page is unchanged; the data rides along inside the same PDF. How a reader opens the attachment. Attachment support is a PDF-reader feature, not something the document controls, so the steps differ by reader: | Reader | How to open the embedded data | |---|---| | Adobe Acrobat / Reader | View - Show/Hide - Side panels - Attachments, then double-click the CSV | | Firefox built-in viewer | Click the paperclip icon in the left toolbar, then the file | | Preview macOS , Chrome | No attachment UI -- these readers do not surface /AF files; use Acrobat or Firefox, or extract programmatically below | Any embedded file can also be pulled out without a viewer, straight from the bytes: python import io, pikepdf with pikepdf.open io.BytesIO pdf bytes as pdf: csv = pdf.attachments "table-1-data.csv" .get file .read bytes Beyond recovering a document's content, manifest=True records what it takes to reproduce its exact rendered bytes: pdf bytes = doc.render manifest=True attaches emboss-manifest.json The 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 , color mode , tagged , toc , page number format , page numbers , front matter pages . doc.reproducibility manifest ... builds the same dict without rendering. emboss reproduce report.pdf php from emboss import reproduce report = reproduce "report.pdf" recover - re-render - structurally compare print report.ok bool reproduce recovers 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. A verifiable record of what generated a document -- which model, from what prompt, and who reviewed it -- folded into the same manifest: python from emboss import Document, GeneratorInfo generator = GeneratorInfo.from prompt "Write a quarterly financial report", model="claude-sonnet-5", provider="anthropic", reviewed by="R. Patel", reviewed at="2026-07-29", doc = Document title="Q3 Report" doc.paragraph "Revenue increased 12% year over year." pdf bytes = doc.render manifest=True, generator=generator GeneratorInfo.from prompt hashes the prompt prompt sha256 rather than storing it, so the manifest proves a specific, reproducible input produced this output without disclosing potentially sensitive prompt content. reviewed by / reviewed at are 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 , which returns None not an exception when no generator record is present -- an honest absence, not a guess. Document.generator = GeneratorInfo ... sets it once for every subsequent render; generate ..., manifest=True the one-liner LLM tier auto-populates model, provider, and the prompt hash unless a generator= override is given. This 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 / sign pdf pades , so provenance can be part of a verifiable chain of custody, not a self-reported claim. doc.patch node id, changes returns a new Document with the one block carrying node id replaced dataclasses.replace block, changes under 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. diff documents matches 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= , a prior render and the block was edited in place rather than replaced. python from emboss import diff documents, render redline result = diff documents old doc, new doc print len result.added , len result.removed , len result.changed redline bytes = render redline old doc, new doc, result render redline renders 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. emboss diff old.pdf new.pdf -o redline.pdf A 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. Render with embed spec=True so the text map emboss-textmap.json is present, then read the markup back: python from emboss.annotations import extract comments, merge comments doc.save "v1.pdf", embed spec=True reviewers mark this up in their reader comments = extract comments "v1-legal.pdf" comments = merge comments extract comments "v1-legal.pdf" , extract comments "v1-finance.pdf" Each comment carries the reviewer, page, type, their text, and a resolution keyed to a stable node id and character range: {"id": "c-07", "type": "strikeout", "author": "R. Patel", "page": 4, "node id": "sec-risk.p3", "anchor text": "exposure exceeds $4.2M", "char range": 120, 143 , "comment": "Overstates it, use the netted figure", "resolution": "exact", "status": "open"} The resolution is never omitted, so a mis-resolved comment cannot pass silently: | State | Meaning | Behaviour | |---|---|---| exact | One node and a character range | Patchable, phrase-level | node | One node, no character range | Patchable, whole-node | spanning | Crosses two or more nodes | Reports every node id; never split silently | unanchored | No content beneath the annotation | Surfaced with page and rect; never dropped | Comments 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: python from emboss.review import propose patches, apply replacements, redline propose patches doc, comments, replacements={} what would change new doc = apply replacements doc, comments, {"c-07": "the netted $2.8M"} redline pdf = redline doc, new doc proves the change For an exact resolution over plain text, only the objected-to phrase is spliced; everything else stays byte-identical. review html comments renders a self-contained triage report with the unresolved count loud at the top. emboss render spec.json -o v1.pdf --embed-spec embeds spec, layout, text map emboss review v1-legal.pdf v1-finance.pdf -o comments.json emboss review v1-legal.pdf --html review.html triage view, no server emboss apply comments.json --spec spec.json propose only emboss apply comments.json --spec spec.json --edits edits.json \ -o v2.pdf --redline redline.pdf apply and prove emboss review exits 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. Make 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 carries its own EmbossSpec JSON, a per-character text index, and any CSV data attached to its tables. pip install "emboss-pdf mcp " { "mcpServers": { "emboss": { "command": "emboss-mcp" } } } The server built on the standard FastMCP API, served over stdio exposes tools for the novel capabilities: render document , get document spec and get document text exact answers from the embedded JSON , list embedded data / extract embedded data pull a table's source CSV back out , extract review comments annotations resolved to nodes , revision history traceability , plus verify document and get spec schema . 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. The tools are plain, tested functions emboss.mcp server.dispatch , callable from your own code without an MCP client. Pre-configured document factories for common formats: python from emboss.templates import memo, report, letter, invoice Create a memo with pre-set styling doc = memo title="Project Update", author="Engineering Team" doc.heading "Status", level=2 doc.paragraph "All milestones are on track." doc.save "memo.pdf" Create an invoice doc = invoice title="Invoice 2024-0147", author="Acme Corp" doc.table headers= "Item", "Qty", "Price" , rows= "Widget", "100", "$5.00" , "Gadget", "50", "$12.00" , doc.save "invoice.pdf" Available templates: memo , report , letter , invoice , academic paper , legal brief , slide deck , data sheet . SlideDeck builds a presentation deck from designed slide layouts, complete color themes, and a fit-to-slide guarantee: no slide ever spills onto a continuation page. python from emboss.slides import SlideDeck deck = SlideDeck "Q3 Board Review", presenter="Ana Ruiz", date="Oct 2026", theme="boardroom" deck.title slide subtitle="Board update" deck.section divider "Results" deck.stat slide "Key metrics", "ARR", "$12.4M", "+18%" , "Churn", "2.1%", "-0.3%" deck.bullet slide "Highlights", "Enterprise ARR up 22%", "Net retention at 118%" deck.chart slide "Growth", chart element deck.quote slide "The window for category leadership is now.", attribution="CEO" deck.closing slide "Thank you", contact="ana@acme.com" deck.save "board deck.pdf" Layouts : title slide , section divider , content slide single or layout="two-column" , bullet slide , stat slide , chart slide , quote slide , code slide , closing slide . Four built-in themes , each a complete, WCAG-contrast-checked color system: boardroom navy and gold , horizon dusk blue and coral , carbon near-black and electric blue , meadow forest and spring green . Aliases default , dark , light , minimal resolve 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. deck.build returns the underlying Document for further customization before .render / .save . python from emboss import Document, LegalFeatures, PageSpec doc = Document title="Memorandum of Understanding", style="legal", page=PageSpec.letter margin left=108 , legal=LegalFeatures watermark="CONFIDENTIAL", watermark opacity=0.12, line numbering=True, bates prefix="ACME-", bates start=1, bates digits=6, bates position="bottom-right", , 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 The 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. python from emboss import Document, Approval, RevisionEntry doc = Document title="Design History File", style="legal" doc.document control doc id="DHF-2026-014", version="3.0", status="Approved", effective date="2026-08-01", classification="Controlled", owner="Quality Engineering", approvals= Approval name="R. Patel", role="Quality Manager", date="2026-07-28" , Approval name="M. Osei", role="Regulatory Affairs", date="2026-07-29" , , revisions= RevisionEntry version="2.0", date="2026-03-01", author="R. Patel", summary="Added risk analysis section" , RevisionEntry version="3.0", date="2026-07-28", author="R. Patel", summary="Updated verification protocol" , , The 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 tagging all come from the same machinery an ordinary doc.table ... uses, not a hand-drawn panel. It round-trips through the JSON spec and renders in the HTML and Markdown adapters. Fillable text, checkbox, and dropdown fields for intake and KYC-style documents, laid out in the normal document flow rather than manually positioned: doc.heading "Client Intake", level=1 doc.text field "full name", label="Full name" doc.checkbox field "accredited investor", label="I am an accredited investor" doc.dropdown field "entity type", options= "Individual", "Corporation", "Trust" , label="Entity type" Each field produces a genuine AcroForm widget -- /FT /Tx , /Btn , or /Ch with the correct /V , /Ff flags required , multiline , the combo bit for a dropdown's /Opt , and a real checkbox /Yes / /Off appearance -- not a drawn rectangle. Every field is tagged as real PDF/UA structure a Form -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 , and a duplicate field name across a document raises rather than producing an AcroForm two fields silently share. A 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 embeds the XML as a factur-x.xml /AF attachment, forces PDF/A-3, and writes the ZUGFeRD/Factur-X XMP metadata. python from emboss import Document, Invoice, Party, InvoiceLine invoice = Invoice invoice number="INV-2026-001", issue date="20260115", currency="EUR", seller=Party name="Acme GmbH", country code="DE", vat id="DE123456789" , buyer=Party name="Beispiel SA", country code="FR" , lines= InvoiceLine name="Consulting", quantity="10", unit price="150.00", net amount="1500.00", tax rate percent="19" , , doc = Document title="Invoice INV-2026-001", style="finance" ... render the human-readable invoice with tables/stat tiles ... doc.attach facturx invoice embeds factur-x.xml, forces PDF/A-3 doc.save "invoice.pdf" Invoice.validate 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 returns the XML directly if you want to inspect or transmit it on its own. python from emboss import HeaderFooter doc = Document header=HeaderFooter left="Acme Corp", center="Confidential", right="{page} of {pages}", separator=True, , footer=HeaderFooter center="Generated by Emboss", , Placeholders {page} and {pages} are resolved at render time. Automatic figure, table, and equation numbering with label-based cross-references: python from emboss import Document, Table, Image from emboss.crossref import CrossReferenceIndex doc = Document title="Research Paper", style="academic" doc.table headers= "Variable", "Value" , rows= "x", "42" , caption="Experimental results", label="results-table", doc.paragraph "As shown in @results-table, the value of x is 42." The @label syntax resolves to "Table 1", "Figure 2", etc., with automatic counter tracking via NumberingContext . Captions are numbered automatically "Figure 1: ...", "Table 1: ..." , and each @label reference becomes a clickable in-document link a GoTo action targeting the labeled element's page . Section numbering is available by setting number sections = True on the document; headings are numbered hierarchically and @label references to sections resolve to the section number. Embed SVG vector graphics as native PDF paths, no rasterization: doc.svg """