Skip to main content

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 text
  • align controls 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_anchor controls 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:

  1. 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 edge
    • box_align_y: top=y is top edge, center=y is center, bottom=y is bottom edge
  2. 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 \n in 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 corner
  • corner_radius creates 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rupdf-0.2.0.tar.gz (2.3 MB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

rupdf-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.3 MB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ x86-64

rupdf-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.2 MB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ ARM64

rupdf-0.2.0-cp313-cp313-macosx_11_0_arm64.whl (1.1 MB view details)

Uploaded CPython 3.13macOS 11.0+ ARM64

rupdf-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.3 MB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ x86-64

rupdf-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.2 MB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ ARM64

rupdf-0.2.0-cp312-cp312-macosx_11_0_arm64.whl (1.1 MB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

rupdf-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.3 MB view details)

Uploaded CPython 3.11manylinux: glibc 2.17+ x86-64

rupdf-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.2 MB view details)

Uploaded CPython 3.11manylinux: glibc 2.17+ ARM64

rupdf-0.2.0-cp311-cp311-macosx_11_0_arm64.whl (1.1 MB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

File details

Details for the file rupdf-0.2.0.tar.gz.

File metadata

  • Download URL: rupdf-0.2.0.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

Hashes for rupdf-0.2.0.tar.gz
Algorithm Hash digest
SHA256 a54c4d74ba57e36ea0fc8b2e7b029b2113eae3c13ed23f4bfc50e18f0721134b
MD5 08ff2e226a12f5ebca9347a812acc80f
BLAKE2b-256 2c396b27c0b1db907e792faa027b06d635e61c210618d8f790782ccc78cf0319

See more details on using hashes here.

File details

Details for the file rupdf-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for rupdf-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 2184de021f576f241bf8687232156d82065728f9c668a9a9bb0321972d1ed5a2
MD5 b3b0e7dc8c49bf3fe499e3ae7ad04087
BLAKE2b-256 00447d73cbe3e72b750590c135eec746235b43cb8c2a4ae01d08334d7ffb6a5d

See more details on using hashes here.

File details

Details for the file rupdf-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for rupdf-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 9666c9e369f05e9e36bfe6e02e931db6a219b529399a3f5a3af9774b78efa2ca
MD5 cec7ed3bfb7b2d06d9253cd3432cb0af
BLAKE2b-256 d3efb85e1ff651efc8d60a8f73790e5723bc6238183604e5ed5d970540d35488

See more details on using hashes here.

File details

Details for the file rupdf-0.2.0-cp313-cp313-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for rupdf-0.2.0-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 b6c8dc58647c7b8563713cf9b0d32edfa3661ede1daf83c3b7fb2b36f46660d9
MD5 b39185a608e5b667f61f26830793c18a
BLAKE2b-256 346bc72c4b9b7ec9175067e91d4fee7bdf6037688c035e211d25055598a18182

See more details on using hashes here.

File details

Details for the file rupdf-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for rupdf-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 21d4e1c7b016461d238cec4ab064fb56a90f9d2e566eac5900d9cd4004c404ca
MD5 66685aeefff49a12036cff987f0b32a4
BLAKE2b-256 afcb91e7a1a6d2191e0b72259fe36c559560a89278e8067273aeea49f882e6e6

See more details on using hashes here.

File details

Details for the file rupdf-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for rupdf-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 16c1207e8a0121ab17add7d1e54c39ee3d68a1a1d7addf8755fed88c194c64ce
MD5 b63a1e155d2666f35933377e98b4f9de
BLAKE2b-256 5ed0eada4fefc0292e962509f494ef06ace597bde7b807720642d62ccad7ee51

See more details on using hashes here.

File details

Details for the file rupdf-0.2.0-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for rupdf-0.2.0-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 ebb2b948eb566292ae2675adab8c0a04a0cd561323105897f85c71a4c2316cfe
MD5 6e437f2bd10feeff77efb226a548c9a9
BLAKE2b-256 e582331a5fc58a23955596679532966a70712c226139756c5b17abbb0288e60f

See more details on using hashes here.

File details

Details for the file rupdf-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for rupdf-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 7a4dbc1e56a614e7f096165729609cee2b09c4a473d4da7a5a551d7a21ddee4e
MD5 f5d4f0007059901a57b13cb2bf2aad8b
BLAKE2b-256 f2c01cabfc9d8d9b365742b18bbc53caef318a86a16fd37e6611d49c2c8885f5

See more details on using hashes here.

File details

Details for the file rupdf-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for rupdf-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 cf0a16f716f98a59f3b7109644e6865ec1b75dd290a9bc83d8be24aeb67ad53f
MD5 d79e8883061c07578f2eb6a74ce4fda9
BLAKE2b-256 de3fb45dd4670d5506fe0fc36d60705b8c52f1ed3c356f3aa4397e84cd159319

See more details on using hashes here.

File details

Details for the file rupdf-0.2.0-cp311-cp311-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for rupdf-0.2.0-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 c091d0f6d63fd8a4c7578591bdd42cede6324df497e9d60986813171cb6dee92
MD5 5cc28d3df8cf42c400b30ba618c3a675
BLAKE2b-256 424f2bf41043101ae3ebfcec0737a2d80441feeed7338191825fc830da636b61

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page