A fast, minimal PDF page renderer
Project description
rupdf
A fast, minimal PDF renderer in Rust with Python bindings. Takes pre-laid-out pages and renders them to PDF bytes.
Features
- Text with TTF/OTF fonts, horizontal/vertical alignment, and colors
- Font fallback chains — per-element list of fallback fonts for characters absent from the primary font's cmap (emoji, CJK, Arabic, etc.)
- Rectangles with stroke, fill, and rounded corners
- Lines with configurable width
- Images (PNG, JPEG, WebP, SVG)
- Barcodes (Code 128, GS1-128), Data Matrix (incl. GS1 DataMatrix), and QR codes
- Font subsetting - embeds only used glyphs
- Compression - optional zlib compression
Installation
pip install rupdf
Usage
import rupdf
doc = {
"metadata": {
"title": "My Document",
"author": "Jane Doe"
},
"pages": [
{
"size": (612, 792), # Letter size in points
"background": (255, 255, 255, 255),
"elements": [
{
"type": "text",
"x": 72,
"y": 72,
"text": "Hello, World!",
"font": "main",
"size": 24,
"color": (0, 0, 0, 255)
},
{
"type": "rect",
"x": 72,
"y": 120,
"w": 200,
"h": 100,
"stroke": 1.0,
"stroke_color": (0, 0, 0, 255),
"fill_color": (240, 240, 240, 255)
}
]
}
],
"resources": {
"fonts": {
"main": {"path": "/path/to/font.ttf"}
# Or: "main": {"bytes": font_bytes}
},
"images": {
"logo": {"path": "/path/to/logo.png"}
# Or: "logo": {"bytes": image_bytes}
}
}
}
# Render to PDF bytes
pdf_bytes = rupdf.render_pdf(doc, compress=True)
# Write to file
with open("output.pdf", "wb") as f:
f.write(pdf_bytes)
Coordinate System
- Origin: top-left corner of the page
- Units: points (1 point = 1/72 inch)
- Y-axis: increases downward
Common page sizes:
- Letter: 612 x 792 points
- A4: 595 x 842 points
Element Types
Text
{
"type": "text",
"x": 72,
"y": 72,
"text": "Hello",
"font": "font_ref", # Reference to fonts in resources
"font_fallback": [], # Optional list of fallback font refs; see "Font fallback" below
"missing_glyph_policy": "drop", # "drop" (default) or "raise"
"size": 12, # Font size in points
"color": (0, 0, 0, 255), # RGBA (optional, default black)
"align": "left", # "left", "center", or "right" (optional)
"vertical_anchor": "baseline" # "baseline", "capline", or "center" (optional)
}
Positioning:
(x, y)specifies the anchor point of the textaligncontrols horizontal alignment relative to x:"left"(default): text extends to the right of x"center": text is centered on x"right": text extends to the left of x
vertical_anchorcontrols vertical alignment relative to y:"baseline"(default): y is the text baseline"capline": y is the top of capital letters"center": y is the vertical center of capital letters
TextBox
Multi-line text with word wrapping, like Illustrator's "area type".
{
"type": "textbox",
"x": 72,
"y": 72,
"w": 200,
"h": 100,
"text": "Long text that will wrap within the box...",
"font": "font_ref",
"font_fallback": [], # Optional list of fallback font refs; see "Font fallback" below
"missing_glyph_policy": "drop", # "drop" (default) or "raise"
"size": 12,
"line_height": 14.4, # Optional, default = size * 1.2
"color": (0, 0, 0, 255), # Optional, default black
# Box alignment (how the box is positioned relative to x, y)
"box_align_x": "left", # "left", "center", or "right" (optional)
"box_align_y": "top", # "top", "center", or "bottom" (optional)
# Text alignment (how text is positioned inside the box)
"text_align_x": "left", # "left", "center", or "right" (optional)
"text_align_y": "baseline" # "top", "capline", "center", "baseline", or "bottom" (optional)
}
Two-Layer Alignment:
-
Box alignment - positions the box relative to
(x, y):box_align_x: left=x is left edge, center=x is center, right=x is right edgebox_align_y: top=y is top edge, center=y is center, bottom=y is bottom edge
-
Text alignment - positions text inside the box:
text_align_x: per-line horizontal alignment (left/center/right)text_align_y: vertical alignment of the text block:"top": ascender of first line at box top"capline": cap height of first line at box top"center": text block vertically centered"baseline"(default): last line's baseline at box bottom"bottom": descender of last line at box bottom
Notes:
- Text wraps at word boundaries to fit within
w - Overflow is clipped to box bounds
- Explicit
\nin text creates line breaks
Font fallback
Text and TextBox elements accept a font_fallback list of font aliases tried in order for any character the primary font's cmap doesn't cover. This is how you render emoji, CJK, Arabic, or any script outside your primary font's coverage without crashing or showing tofu.
doc = {
"pages": [{
"size": (612, 792),
"elements": [
{
"type": "text",
"x": 72, "y": 72,
"text": "Tony ❤ 山田",
"font": "body",
"font_fallback": ["emoji", "body_jp"],
"size": 12,
},
],
}],
"resources": {
"fonts": {
"body": {"path": "IBMPlexSans-Regular.otf"},
"emoji": {"path": "NotoEmoji-Regular.ttf"},
"body_jp": {"path": "IBMPlexSansJP-Regular.otf"},
},
},
}
Semantics:
- For each character, rupdf walks
[font, *font_fallback]and uses the first font whose cmap covers it. - The primary font drives line height, ascender, descender, baseline, and cap-height metrics. Fallback chars share that baseline so they don't shift layout.
- Inside one text element, the PDF content stream switches fonts inline (via
Tf) at each run boundary. Word-wrapping in TextBox honors per-character advances across fonts. - Fallback fonts not referenced by any character are not embedded.
missing_glyph_policy controls what happens when no font in the chain covers a character:
| Value | Behavior |
|---|---|
"drop" (default) |
Character silently omitted. Surrounding spaces and layout are preserved. |
"raise" |
RupdfError is raised, naming the primary font and the offending codepoint. |
"drop" is the right default for user-supplied text (customer names, free-text fields) where rendering must not fail. Use "raise" in tests or pipelines that want to detect unsupported codepoints early.
Rectangle
{
"type": "rect",
"x": 72,
"y": 72,
"w": 100,
"h": 50,
"stroke": 1.0, # Stroke width (0 for no stroke)
"stroke_color": (0, 0, 0, 255), # Optional
"fill_color": (255, 255, 255, 255), # Optional
"corner_radius": 10 # Optional, for rounded corners
}
Notes:
(x, y)is the top-left cornercorner_radiuscreates rounded corners; automatically clamped to half the smallest dimension
Line
{
"type": "line",
"x1": 72,
"y1": 72,
"x2": 200,
"y2": 72,
"stroke": 1.0,
"color": (0, 0, 0, 255)
}
Image
{
"type": "image",
"x": 72,
"y": 72,
"w": 200,
"h": 150,
"image_ref": "logo" # Reference to images in resources
}
Supported formats: PNG, JPEG, WebP (rasterized to 300 DPI), SVG (rendered as vectors).
Barcode (Code 128)
{
"type": "barcode",
"x": 72,
"y": 72,
"w": 200,
"h": 60,
"value": "ABC-123",
"human_readable": True, # Show text below barcode
"font": "font_ref", # Required if human_readable
"font_size": 10
}
GS1-128
A Code 128 variant with an FNC1 designator and Application Identifiers (AIs).
The value is a parenthesized string; FNC1 separators are inserted automatically
after variable-length fields, and fixed-length AIs (00, 01-04, 11-19, 20,
31xx-36xx, 41) have their data length validated.
{
"type": "gs1_128", # also "gs1-128" or "gs1"
"x": 72,
"y": 72,
"w": 300,
"h": 60,
"value": "(01)12345678901234(17)260101(10)BATCH123",
"human_readable": True, # renders the parenthesized form below the bars
"font": "font_ref",
"font_size": 9
}
Data Matrix (incl. GS1 DataMatrix)
# Plain ECC 200 Data Matrix
{
"type": "datamatrix",
"x": 72,
"y": 72,
"size": 80, # bounding-box dimension
"value": "UNIT-42",
"color": (0, 0, 0, 255), # optional
"background": (255, 255, 255, 255) # optional
}
# GS1 Data Matrix — same parenthesized (AI)data form as GS1-128
{
"type": "gs1_datamatrix", # also "gs1-datamatrix"
"x": 72,
"y": 72,
"size": 80,
"value": "(01)12345678901234(17)260101(10)BATCH123",
"shape": "square" # "any" (default), "square", or "rectangular"
}
Both square and rectangular shapes are valid GS1 Data Matrix per the GS1
General Specifications. "any" (the default) lets the encoder pick the
smallest-area symbol for the payload, which often turns out rectangular.
Pick "square" if you need the conventional square shape.
QR Code
{
"type": "qrcode",
"x": 72,
"y": 72,
"size": 100, # QR codes are square
"value": "https://example.com",
"color": (0, 0, 0, 255), # Foreground (dark modules)
"background": (255, 255, 255, 255) # Background (light modules)
}
Error Handling
try:
pdf = rupdf.render_pdf(doc)
except rupdf.RupdfError as e:
print(f"Failed to render: {e}")
Common errors:
- Missing font or image reference
- Invalid page dimensions
- Missing required element fields
- Character not found in font
Performance
Benchmarks comparing rupdf to ReportLab (10 iterations each):
| Benchmark | rupdf | ReportLab | Speedup |
|---|---|---|---|
| Empty page | 0.02ms | 0.27ms | 13x |
| 50 text lines | 0.82ms | 0.82ms | 1x |
| 100 rectangles | 0.19ms | 1.02ms | 5x |
| 10 pages | 1.62ms | 3.80ms | 2x |
Development
# Build
maturin develop
# Run tests
cargo test # Rust unit tests
pytest python/tests/ -v # Python tests
# Generate test PDF with all element types
python scripts/generate_test_pdf.py # Without images
python scripts/generate_test_pdf.py --svg logo.svg --png photo.png # With images
# Benchmarks
python benchmarks/run_benchmark.py
License
MIT
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 Distributions
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 rupdf-0.2.1.tar.gz.
File metadata
- Download URL: rupdf-0.2.1.tar.gz
- Upload date:
- Size: 2.3 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7110bbf1babc0474639e8b92fa6bd3870548afc692cbfe182de93420980b8456
|
|
| MD5 |
6f6f4734a96899db1b4f4274ca67fc44
|
|
| BLAKE2b-256 |
ac4b417f552fb20b42ed52448641144c5ab416ab73efdcad4596fcb6803d0fea
|
File details
Details for the file rupdf-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: rupdf-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 1.3 MB
- Tags: CPython 3.13, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ba4b9d7f3da6717fa6d37ded6481fec81b9e972cbf2925aa4c38d67755d969b8
|
|
| MD5 |
c7f83088373601d749de75ad7b7274f2
|
|
| BLAKE2b-256 |
75bdf768c65be7bc769609def142ec3bf8b3e93259ec8a61c8f7703c45e5435e
|
File details
Details for the file rupdf-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: rupdf-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 1.2 MB
- Tags: CPython 3.13, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2f97b71eea42da93e65f13115a4f0ecbd401819fba04fdba3c0bd0b06de0d05c
|
|
| MD5 |
04dc64dcc3cc41fc2e2fa9fb81cff23e
|
|
| BLAKE2b-256 |
65c842081f29b046b4b6dfdb5ab48e2bbe135890b1770b0b68de631aa68f3c4e
|
File details
Details for the file rupdf-0.2.1-cp313-cp313-macosx_11_0_arm64.whl.
File metadata
- Download URL: rupdf-0.2.1-cp313-cp313-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.1 MB
- Tags: CPython 3.13, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b9321832ace6ad4593007db960083195e21e7242b9d4f51ca6a8a9e0757abbcc
|
|
| MD5 |
d259357eb2b6c21874a067a83d3624aa
|
|
| BLAKE2b-256 |
1900c90a4f840e8a8386b420e5386d1bca3132b5d511bcc897e23abc46182ca1
|
File details
Details for the file rupdf-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: rupdf-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 1.3 MB
- Tags: CPython 3.12, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b1dcc1376c51a0f1bacc22a8e986ed751540a0af6e36f94d739998970344a40
|
|
| MD5 |
03ecfdec662e343e0c86e69cdfdc1a82
|
|
| BLAKE2b-256 |
1eead8492fd822ec6503a029f586f2fbbce302b0452f7148e2e88fd16dfa8b9c
|
File details
Details for the file rupdf-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: rupdf-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 1.2 MB
- Tags: CPython 3.12, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a18dc3d991ee0f3921d495694d79ec21bc4644b30ad75009a36e834fd9c4b984
|
|
| MD5 |
6b883192a94c024eed4e0ea131d1936e
|
|
| BLAKE2b-256 |
d811c84612d88f0b19e51ffe8e7fe950fe2944ad39dda4697efc0a2397266131
|
File details
Details for the file rupdf-0.2.1-cp312-cp312-macosx_11_0_arm64.whl.
File metadata
- Download URL: rupdf-0.2.1-cp312-cp312-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.1 MB
- Tags: CPython 3.12, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8dc6a215a1473b0e54622c264f0d6d36e47f638ef9a24f929c5bf90baeaaa913
|
|
| MD5 |
eaafcc767c0bcd73e8ba42c8e5fbc72f
|
|
| BLAKE2b-256 |
0441a300bd06fb325c3356ac71c751096fd478ce7ad18c0ffc89b98cdc92e7cc
|
File details
Details for the file rupdf-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: rupdf-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 1.3 MB
- Tags: CPython 3.11, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9dc6cb1fdb5d2483df82b85cfad21b1073d311b36b0fdd5614debb12944f4eac
|
|
| MD5 |
4fd03da4bfded22c51ca76057c5ea5c2
|
|
| BLAKE2b-256 |
3532b7384ce823f2fa4571efa46dc2ae250acd40e1e9bd8c72e15c9c93d0b0a8
|
File details
Details for the file rupdf-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: rupdf-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 1.2 MB
- Tags: CPython 3.11, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
15b635577f7502234bc7cf266d236822e228dd52c73dd0cb07604c95e6db0f5a
|
|
| MD5 |
891db3dedca2ad637e00b8829664dedd
|
|
| BLAKE2b-256 |
4a49220412aacc6d31c51da19402f29814c259f9f6271fd1deea2bf879fb7571
|
File details
Details for the file rupdf-0.2.1-cp311-cp311-macosx_11_0_arm64.whl.
File metadata
- Download URL: rupdf-0.2.1-cp311-cp311-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.1 MB
- Tags: CPython 3.11, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
67c1a10558d2a345b7c81cdcb955d85c79e36fa4915afbe60f727e973ab2d8ba
|
|
| MD5 |
f883bfde6c7da010ab2fc0e475bda966
|
|
| BLAKE2b-256 |
3b150f671349669cd4fbcf6d46f2963ca7d6a3657790c2788573ec0932c8329b
|