JSON-driven PDF generation over ReportLab
Project description
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 GitHub without installing it permanently. Perfect for one-off usage or testing.
Using uvx (Recommended - Fast)
# Run once without installing
uvx --from git+https://github.com/Shubhamnegi/reportlab-json-renderer.git pdf-renderer --help
# Render a PDF directly from GitHub
uvx --from git+https://github.com/Shubhamnegi/reportlab-json-renderer.git pdf-renderer render --input report.json --output report.pdf
Using pipx
# Run once without installing
pipx run git+https://github.com/Shubhamnegi/reportlab-json-renderer.git --help
# Or install permanently from GitHub
pipx install git+https://github.com/Shubhamnegi/reportlab-json-renderer.git
pdf-renderer --help
From Local Clone
# 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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file reportlab_json_renderer-0.1.0.tar.gz.
File metadata
- Download URL: reportlab_json_renderer-0.1.0.tar.gz
- Upload date:
- Size: 86.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8095e55bdd8ddb694f0a32da6ba1ec800fe3708964eef22f5ec15ec0d1558fd1
|
|
| MD5 |
58388388b58a6bb53fee1b0959afb116
|
|
| BLAKE2b-256 |
215d672e750290eb3117e713d611eca432894087906715e42f7a61ae2e4f68db
|
Provenance
The following attestation bundles were made for reportlab_json_renderer-0.1.0.tar.gz:
Publisher:
publish.yml on Shubhamnegi/reportlab-json-renderer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
reportlab_json_renderer-0.1.0.tar.gz -
Subject digest:
8095e55bdd8ddb694f0a32da6ba1ec800fe3708964eef22f5ec15ec0d1558fd1 - Sigstore transparency entry: 1869854201
- Sigstore integration time:
-
Permalink:
Shubhamnegi/reportlab-json-renderer@6b35699bc4a0a9e932c45ecbe34b23a08cc6e2bb -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Shubhamnegi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6b35699bc4a0a9e932c45ecbe34b23a08cc6e2bb -
Trigger Event:
push
-
Statement type:
File details
Details for the file reportlab_json_renderer-0.1.0-py3-none-any.whl.
File metadata
- Download URL: reportlab_json_renderer-0.1.0-py3-none-any.whl
- Upload date:
- Size: 65.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e2f521238e5c28ecfed9a63501767536615f49ed0098b6652379b0b3f81e2e73
|
|
| MD5 |
0f32898f430bb492668fe2ea5ed8eb3f
|
|
| BLAKE2b-256 |
46d9ad2f95fa710baa78acab5aba9e72543c7be97c776c8aa6d5d50342f2537e
|
Provenance
The following attestation bundles were made for reportlab_json_renderer-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on Shubhamnegi/reportlab-json-renderer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
reportlab_json_renderer-0.1.0-py3-none-any.whl -
Subject digest:
e2f521238e5c28ecfed9a63501767536615f49ed0098b6652379b0b3f81e2e73 - Sigstore transparency entry: 1869854210
- Sigstore integration time:
-
Permalink:
Shubhamnegi/reportlab-json-renderer@6b35699bc4a0a9e932c45ecbe34b23a08cc6e2bb -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Shubhamnegi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6b35699bc4a0a9e932c45ecbe34b23a08cc6e2bb -
Trigger Event:
push
-
Statement type: