reportlab-json-renderer
JSON-driven PDF generation over ReportLab.
An LLM/agent sends a compact JSON specification; this library validates it, resolves a template + theme, renders each block via ReportLab, and produces a polished PDF — no Python layout code is generated by the agent.
Requirements
- Python ≥ 3.11
Installation
pip install reportlab-json-renderer
For development (includes pytest, ruff, pre-commit):
pip install -e ".[dev]"
Quick Run (No Installation Needed)
Run the CLI directly from PyPI without installing it permanently. Perfect for one-off usage or testing.
Using uvx (Recommended - Fast)
# Run once without installing
uvx --from reportlab-json-renderer pdf-renderer --help
# Render a PDF directly from PyPI
uvx --from reportlab-json-renderer pdf-renderer render --input report.json --output report.pdf
Using pipx
# Run once without installing
pipx run reportlab-json-renderer --help
# Or install permanently from PyPI
pipx install reportlab-json-renderer
pdf-renderer --help
From Local Clone (Development)
# If you have the repo cloned locally
uvx --from . pdf-renderer --help
pipx run . pdf-renderer --help
Quick Start
Python API
from reportlab_json_renderer import render_pdf
result = render_pdf(spec=my_json_spec, output_path="/tmp/report.pdf")
print(result["path"], result["pages"])
CLI
# Render a spec to PDF
pdf-renderer render --input report.json --output report.pdf
# Validate without rendering
pdf-renderer validate --input report.json
# Export the JSON Schema
pdf-renderer schema --output schema.json
# List available templates
pdf-renderer templates
# List registered block types
pdf-renderer blocks
# Generate a sample spec
pdf-renderer sample --output sample.json
Why JSON-Driven PDFs?
When an LLM/agent needs to generate PDFs, there are three common approaches. This library is purpose-built for the agent workflow and outperforms the alternatives on safety, token efficiency, and determinism.
vs. Direct ReportLab Code Generation
The agent writes Python that imports ReportLab, creates a canvas, and places flowables. This is the default "just write code" approach.
| Dimension | Agent-generated ReportLab code | reportlab-json-renderer |
|---|---|---|
| Token usage | ~4,000–6,000 tokens for a 10-page report | ~800–1,200 tokens (~70% fewer) |
| Security | Agent emits executable Python — os.system(), file writes possible |
Agent never writes code; JSON is declarative and sandboxed |
| Error surface | SyntaxError, AttributeError from malformed generated code |
Schema validation catches issues before rendering |
| Consistency | Same spec → different layout each time (agent rewrites styles) | Same spec → identical PDF every time |
| Maintainability | Regenerated code varies per request; no single source of truth | Block renderers, themes, and templates are versioned backend code |
| Debugging | Stack traces point into generated code with no meaningful names | Errors map to block type and renderer with clear messages |
vs. HTML-to-PDF (WeasyPrint, wkhtmltopdf, Puppeteer)
The agent generates full HTML + CSS which is then converted to PDF.
| Dimension | HTML-to-PDF | reportlab-json-renderer |
|---|---|---|
| Token usage | ~3,000–4,000 tokens — verbose HTML structure + CSS | ~800–1,200 tokens — compact JSON blocks |
| Layout control | CSS is fragile — print media quirks, box model issues | Backend-owned ReportLab rendering — deterministic output |
| Security | Risk of injected <script>, external resource loading |
No executable code; asset_root blocks path traversal |
| Dependencies | Chromium, WK binary, or C libs (libpango, etc.) |
Pure Python — reportlab + pydantic only |
| Reproducibility | Browser/renderer version-dependent output drift | Deterministic pipeline with golden-file tests |
| Branding | CSS must be re-emitted every time | Theme and template resolved server-side; agent picks "theme": "green" |
Where the Token Savings Come From
The ~70% reduction comes from eliminating three categories of agent output:
- Style declarations — font sizes, colors, margins, padding → handled by themes and templates.
- Layout coordinates — x/y positioning, widths, heights → handled by ReportLab flowables and page templates.
- Boilerplate setup — imports, canvas creation, font registration → handled by the renderer pipeline.
The agent only describes what content to show (blocks, data, text), never how to lay it out.
Python API Reference
render_pdf(spec, output_path=None, allow_partial=False, asset_root=None)
Render a PDF from a validated JSON specification.
Parameters:
| Name | Type | Description |
|---|---|---|
spec |
dict |
JSON specification conforming to the report schema. |
output_path |
str | None |
Filesystem path for the generated PDF. If None, returns bytes only. |
allow_partial |
bool |
Continue after block render errors and return warnings instead of raising. Default: False. |
asset_root |
str | Path | None |
Directory boundary for local images. Relative paths resolve under this root and traversal outside it is rejected. Default: current working directory. |
Returns dict with keys:
| Key | Type | Description |
|---|---|---|
success |
bool |
True if rendering completed without errors. |
path |
str | None |
Output file path, if written. |
bytes |
bytes | None |
Raw PDF bytes when no output_path given. |
pages |
int |
Number of pages in the generated PDF. |
warnings |
list[str] |
Non-fatal warnings collected during render. |
metadata |
dict |
Echo of template and theme used. |
Warnings may come from schema post-validation checks, template block restrictions,
or explicit partial-render mode (allow_partial=True).
Raises ValidationError if the spec fails validation and RenderError if a
block cannot be rendered.
from reportlab_json_renderer import render_pdf
# Write to file
result = render_pdf(spec, output_path="/tmp/report.pdf")
# Bytes-only (no file written)
result = render_pdf(spec)
pdf_bytes = result["bytes"]
# Explicit partial rendering
result = render_pdf(spec, allow_partial=True)
# Restrict image loading to a known directory
result = render_pdf(spec, asset_root="/app/report-assets")
JSON Contract
See pdf-generator.md for the full specification.
See docs/json-schema.md for a human-readable field reference.
Minimal example:
{
"version": "1.0",
"template": "analytics_report_v1",
"theme": "green",
"metadata": {
"entity_name": "Demo Store",
"report_title": "Weekly Report",
"period": "11 Jun – 17 Jun 2026",
"generated_at": "2026-06-18",
"powered_by": "Public PDF Renderer",
"confidential": true
},
"page": {
"size": "A4",
"orientation": "portrait",
"margins": { "left_cm": 1.5, "right_cm": 1.5, "top_cm": 2.2, "bottom_cm": 2.0 }
},
"header": { "enabled": true, "variant": "default" },
"footer": { "enabled": true, "show_page_number": true },
"blocks": [
{ "type": "title", "entity": "Demo Store", "title": "Weekly Report", "subtitle": "11 Jun – 17 Jun 2026" }
]
}
An empty blocks list is valid and produces a single-page PDF with header and
footer only.
Built-in Templates
| Template | Description |
|---|---|
analytics_report_v1 |
Full-featured analytics report with charts, KPIs, and tables |
business_report_v1 |
Professional business document with structured sections |
compact_report_v1 |
Dense layout for data-heavy reports with minimal whitespace |
invoice_v1 |
Compact invoice layout with tables and totals (limited block types) |
proposal_v1 |
Sales proposal with hero sections and call-to-action blocks |
Built-in Themes
| Theme | Description |
|---|---|
green |
Green accent palette with dark text |
neutral |
Professional grayscale palette |
dark |
Dark background with light text |
Block Types (19)
title, section_header, paragraph, rich_text, kpi_grid, callout,
callout_group, table, matrix_table, insight_list, recommendations,
image, chart, two_column, page_break, spacer, divider, badge,
summary_box
Extending
This library is designed to be extended with custom blocks, themes, and templates.
Current implementation notes:
-
imageblocks currently support local filesystem paths only. -
Relative image paths are resolved under
asset_rootin the Python API and under the input JSON file's directory in the CLI. Traversal outside that root is rejected. -
image.fitis accepted by the schema but not yet applied by the renderer. -
Renders fail closed by default on block errors. Use
allow_partial=Trueonly if your application explicitly accepts partial output. -
The renderer enables deterministic PDF generation settings where practical so repeated renders of the same spec can produce identical bytes.
-
Custom blocks: Subclass
BaseBlockand register via the block registry. Seedocs/custom-blocks.md. -
Custom themes: Create a
Themedataclass and register via the theme registry. Seedocs/custom-themes.md. -
Custom templates: Create a
Templatedataclass and register via the template registry. Seedocs/custom-templates.md.
Development
# Lint
ruff check .
# Format
ruff format .
# Test
pytest
# Test with coverage
pytest --cov --cov-report=term-missing
# Rendered-PDF structural verification
pytest tests/test_golden.py
Project Structure
reportlab_json_renderer/
├── __init__.py # Public API: render_pdf()
├── renderer.py # Core rendering pipeline
├── cli.py # CLI entry point
├── schema/ # Pydantic models & validators
│ ├── base.py # ReportSpec, Block types, enums
│ └── validators.py # validate_spec, generate_schema_json
├── templates/ # Report templates (5 built-in)
│ ├── base.py # Template, PageSpec, build_template
│ └── registry.py # get_template, register_template, list_templates
├── themes/ # Colour/font themes (3 built-in)
│ ├── base.py # Theme, build_theme, DEFAULT_TONES
│ └── registry.py # get_theme, register_theme, list_themes
├── blocks/ # Block type renderers (19 built-in)
│ ├── base.py # BaseBlock ABC
│ └── registry.py # render_block, register, list_registered
├── assets/ # Fonts, logos
├── utils/ # Colours, units, text, charts, images, errors
└── tests/
└── fixtures/ # Test JSON files
Implementation Progress
See docs/implementation-checklist.md for the
original build checklist and docs/release-readiness-checklist.md
for current release work.
Metadata
Release files for reportlab-json-renderer 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| reportlab_json_renderer-0.3.0.tar.gz | 155.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| reportlab_json_renderer-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 236.5 kB
Release files / reportlab_json_renderer-0.3.0.tar.gz
| Download URL | reportlab_json_renderer-0.3.0.tar.gz |
|---|---|
| Size | 155.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7311da11a427de6fa4fe37db0662e0703b1b2a79a3d31135fe9f03659024b8d1
|
|
BLAKE2b-256 checksum How to use checksums |
9cdd7bc73fd83473197b8ff5d28390ff88dd8dfb55c560a130c5beac17629be4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 24, 2026.
Transparency logRelease files / reportlab_json_renderer-0.3.0-py3-none-any.whl
| Download URL | reportlab_json_renderer-0.3.0-py3-none-any.whl |
|---|---|
| Size | 81.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7e9c9b728908df01ba80b1b0d89527b331f0c713b8cd27a80599445a4fe363a1
|
|
BLAKE2b-256 checksum How to use checksums |
abccf813299155c477d6c4ff0a1bb120fcefc91d4c33afd0d8ddd37d16ab82fd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 24, 2026.
Transparency log