Skip to main content

blitz-py

CI PyPI

Render HTML/CSS to images from Python — no browser, no GPU, no JavaScript, no network.

Powered by Blitz, DioxusLabs' modular web engine: real CSS via Stylo (Servo/Firefox's style engine), flexbox/grid layout via Taffy, text shaping via Parley, and CPU rasterization via vello_cpu.

A 240×240 widget renders in ~1.5ms warm on an M-series Mac (~40ms for the first render). Output is deterministic and identical across platforms: the Inter font (SIL OFL 1.1) is bundled as the default face, so text renders the same on your laptop and in a fontless Alpine container.

What it looks like

Dashboard rendered from Tailwind v4 CSS: bento grid, SVG donut and sparkline, avatar stack, gradients

240x240 smart display composed from Templates with render_layers, ellipsized title, glow accent Bootstrap 5.3 card with buttons, badge, alert and progress bar

Unedited output: a Tailwind v4 dashboard (render_png — grid, SVG donut + sparkline, gradients), a 240×240 smart display (one render_layers call over four Template cells, ellipsized title, glow accent), and Bootstrap 5.3 components.

Install

pip install blitz-py

Prebuilt wheels (abi3, Python ≥ 3.10): Linux glibc + musl (x86_64, aarch64), macOS (arm64, x86_64), Windows (x64, arm64).

No 32-bit (armv7l) wheels — this is an explicit drop, not an oversight: the rendering stack (anyrender 0.11) does not compile on 32-bit targets, and Home Assistant itself removed 32-bit support as of 2025.12. Frozen 32-bit installs on Pi 2/3 cannot use this package; we'll revisit if the engine gains 32-bit support.

Usage

import blitz_py

png = blitz_py.render_png(
    """
    <style>
      body { margin: 0; background: #000; color: #fff; font-family: sans-serif; }
      .screen { display: flex; flex-direction: column; align-items: center;
                justify-content: center; height: 240px; }
      .temp { font-size: 64px; font-weight: 600; }
      .label { color: #8e8e93; }
    </style>
    <body><div class="screen">
      <div class="temp">21.5&deg;</div>
      <div class="label">Living room</div>
    </div></body>
    """,
    width=240,
    height=240,
)
with open("out.png", "wb") as f:
    f.write(png)

Or get raw pixels for Pillow:

from PIL import Image

w, h, rgba = blitz_py.render_rgba(html, width=240, height=240)
Image.frombytes("RGBA", (w, h), rgba).convert("RGB").save("out.jpg", quality=90)

Animated GIFs

Animated dashboard: radar sweep, equalizer bars, deploy progress, typewriter terminal — pure CSS keyframes

Everything above is Tailwind classes + CSS @keyframes (source): satellites on different orbital periods, a live-scrolling traffic chart (a periodic series drawn two cycles wide, translated one cycle per loop — new data appears to stream in), an indeterminate progress sweep, and a steps()-driven typewriter — 48 frames rendered in ~220ms, seamless 4s loop.

CSS animations are evaluated on a deterministic clock: render_frames renders the document at any list of timestamps (seconds), and Pillow assembles the GIF. Frames after the first reuse the parsed document, so they're fast — ~1ms per 240×240 frame:

fps, seconds = 12, 3.2
gif = blitz_py.render_gif(
    html, width=240, height=240,
    times=[i / fps for i in range(int(fps * seconds))],
)
open("widget.gif", "wb").write(gif)   # ~16KB, encoded natively in ~20ms

Anything @keyframes can express — transforms, opacity, colors — loops perfectly because you control the clock. The encoder uses one shared palette (colors=64 by default), no dithering, and transparency-based inter-frame deltas — the combination that keeps UI-animation GIFs small. Frame delays follow the spacing of times. See examples/animated_widget.py.

Need custom encoding (dithering, APNG/WebP, per-frame palettes)? render_frames returns raw RGBA frames for Pillow/ffmpeg.

Fast repeated renders: Template

For dashboards and device widgets that re-render the same document with fresh data, parse once and mutate by element id:

tpl = blitz_py.Template(html, width=240, height=240)
tpl.update(temp="21.5°", hum="48%")       # batch text update, one round-trip, atomic
tpl.set_html("alerts", "".join(f"<li>{a}</li>" for a in alerts))  # re-render a region
tpl.set_style("bar", "width", "62%")
tpl.set_attribute("icon", "src", data_uri)
jpeg = tpl.render_jpeg(quality=90)        # ~0.5ms — straight to the device
gif = tpl.render_gif(times=[i/12 for i in range(38)])  # animated, current data

Templates are safe to share across threads (the document lives on its own worker thread), and renders release the GIL — 4 rendering threads get ~3.5× throughput.

Recipes

OG / social cards — deterministic, ~6ms per 1200×630 card, no Chromium to babysit:

OG card sample

card = blitz_py.render_png(OG_TEMPLATE, width=1200, height=630,
                           css_vars={"title": post.title, "kicker": post.tag})

Email-HTML previews — auto-height gives you the full message at client width:

png = blitz_py.render_png(email_html, width=600)   # height=None → sized to content

Visual snapshot tests — renders are byte-identical across platforms (CI-verified), so hashes are stable:

def test_widget_looks_right():
    png = blitz_py.render_png(widget_html, width=240, height=240)
    assert hashlib.sha256(png).hexdigest() == "9d62e494..."  # exact, on every OS

Device dashboards (the original use case) — Template + render_jpeg/render_gif for sub-millisecond updates pushed to small displays.

API

Six functions and a class, same keyword arguments:

render_png(html, *, width, height=None, ...) -> bytes       # PNG; height=None → content height
render_jpeg(html, *, width, height=None, quality=90, ...) -> bytes  # JPEG (opaque background)
render_rgba(html, *, width, height=None, ...) -> (w, h, bytes)   # raw RGBA pixels
render_frames(html, *, width, height, times, ...) -> (w, h, [bytes, ...])  # animation frames
render_gif(html, *, width, height, times, colors=64, ...) -> bytes  # looping GIF, native encoder
Template(html, *, width, height, ...)                       # parse once, re-render fast

Template methods: set_text(id, text) · update(**id_to_text) (batch, atomic) · set_html(id, fragment) (replace a region, new ids indexed) · set_style(id, prop, value) · set_attribute(id, name, value) · render_png/jpeg/rgba(time=...) · render_frames(times=...) · render_gif(times=..., colors=...)

Layered compositing

render_layers composites several documents and/or Templates into one surface in a single call — positions, paint order, alpha, and clipping handled in Rust, with native PNG/JPEG output. Per-layer blur and tint unlock effects the engine can't do in CSS, like text glow:

neon text glow via blurred tinted layers

frame = blitz_py.render_layers_jpeg(
    [
        {"html": backdrop_html, "width": 240, "height": 240},
        {"template": clock_cell, "x": 8,   "y": 8},     # Templates re-render in ~0.4ms
        {"template": temp_cell,  "x": 124, "y": 8},
        {"html": glow_html, "width": 240, "height": 240, "blur": 8, "tint": "#00d9ff"},
        {"html": glow_html, "width": 240, "height": 240},   # sharp pass on top
    ],
    width=240, height=240, background="#000000", quality=90,
)

Layers paint in list order (explicit z-order) and are clipped to their rects — a whole multi-widget display becomes one call.

Layout introspection

Ask the engine where things actually landed instead of mirroring CSS math in Python:

tpl.get_box("forecast")   # -> (x, y, width, height) in CSS px, post-layout
tpl.boxes()               # -> {id: rect} for every element with an id

Text utilities

Ellipsis, clamping, fitting and balancing — computed with the renderer's own shaper so they're exact:

blitz_py.ellipsize(title, max_width=120, font_size=14)          # "Living room te…"
blitz_py.line_clamp(desc, max_width=200, max_lines=2, font_size=13)
blitz_py.fit_font_size("23.5°C", max_width=180, max_size=72)    # hero autoscaling
blitz_py.wrap_balanced(headline, max_width=200, font_size=18)   # text-wrap: balance
blitz_py.measure_text_lines(text, font_size=13, max_width=200)  # per-line metrics
blitz_py.register_fonts([font_bytes])                           # once, process-wide

Text measurement

measure_text exposes the engine's own shaper (Parley + the same font collection used for rendering), so Python-side fitting logic — ellipsis, autoscaling, wrapping estimates — uses the same metrics the renderer will use, instead of a second font system that drifts:

w, h = blitz_py.measure_text("Living room temperature", font_size=16, font_weight=600)
_, wrapped_h = blitz_py.measure_text(long_text, font_size=14, max_width=208.0)

def ellipsize(text, max_w, **kw):
    while text and blitz_py.measure_text(text + "…", **kw)[0] > max_w:
        text = text[:-1]
    return text + "…"
Argument Default Meaning
width, height required CSS-pixel viewport size
scale 1.0 Device-pixel ratio; output is width*scale × height*scale physical pixels. Use 2.0 for supersampled/hi-dpi output.
color_scheme "light" "light" or "dark" — drives @media (prefers-color-scheme: ...)
background "#ffffff" Base canvas color (#rgb, #rrggbb, #rrggbbaa), or None for transparent
base_url None Base for resolving relative URLs
css None Extra CSS appended after the document's styles (wins the cascade)
css_vars None Dict of CSS custom properties set on :root, e.g. {"accent": "#f00"}var(--accent)
fonts None List of font file bytes (TTF/OTF, variable fonts OK) to register
default_font_family None Family name to use for all CSS generic families (sans-serif, serif, ...) and as text fallback
allow_file_urls False Permit file:// URLs for images/resources

Images and resources

Rendering is fully offline. Embed images as data: URIs, or enable allow_file_urls=True and use file:// paths. http(s) URLs are intentionally ignored.

import base64
b64 = base64.b64encode(open("icon.png", "rb").read()).decode()
html = f'<img src="data:image/png;base64,{b64}" style="width:32px">'

CSS frameworks (Bootstrap, Tailwind, ...)

Any framework that ships as plain CSS works — inline it in a <style> tag:

css = open("bootstrap.min.css").read()  # fetch/cache it however you like
html = f"<style>{css}</style><body class='p-4'><div class='card'>...</div></body>"

Bootstrap 5 components (cards, buttons, badges, alerts, progress bars) render correctly. For Tailwind, run its build step and inline the generated CSS — the JS "Play CDN" won't work because there is no JavaScript engine. JS-driven behavior (modals opening, dropdowns) doesn't apply to static rendering anyway.

Fonts

Bundled Inter is the default for every CSS generic family and the Latin-script fallback, everywhere. Explicit family names (font-family: "Comic Sans MS") resolve against system fonts where available (macOS/Windows natively; Linux via fontconfig loaded at runtime if present — never a link dependency). To use your own font:

font = open("MyFont.ttf", "rb").read()
blitz_py.render_png(html, width=240, height=240,
                    fonts=[font], default_font_family="My Font")

@font-face also works with data: (or file://) sources — state the format explicitly, either as the unquoted CSS keyword or a bare extension string:

@font-face {
  font-family: MyWebFont;
  src: url(data:font/ttf;base64,...) format(truetype);  /* or format("ttf") */
}

WOFF/WOFF2 sources are supported too. local(...) sources and format-less data URIs are currently skipped by the engine.

Note on coverage: bundled Inter covers Latin scripts (plus Greek/Cyrillic). For CJK, Arabic, and other scripts on systems without suitable fonts, pass an appropriate font (e.g. a Noto variant) via fonts=.

What's supported

Modern CSS as implemented by Stylo/Taffy: flexbox, grid, gradients, border-radius, shadows, transforms, calc(), custom properties, media queries, SVG images, WOFF... No JavaScript, no @font-face fetching, no external resources. Blitz itself is pre-1.0: capable but not pixel-perfect against browsers.

Performance

Measured on an M-series Mac (arm64), each scenario in a fresh process, release build — reproduce with examples/bench.py:

Scenario Output px First render Warm render Peak RSS after 200 renders
<h1>Hello</h1> 200×100 35ms 0.5ms 42MB
240×240 widget @2× (flex + gradients) 480×480 32ms 1.8ms 45MB
Bootstrap 5.3 card (233KB CSS) 880×720 39ms 8.2ms 51MB
Tailwind v4 dashboard (the gallery image) 1520×1328 60ms 22ms 62MB
Long article 800×4000 56ms 21ms 73MB
Animated GIF: widget, 38 frames 240×240×38 56ms total 1.5ms/frame 76MB

GIF encoding on top of rendering (Pillow quantize + LZW, 38 frames): ~60ms, 18KB output.

More performance properties, all verified in CI or by examples/bench.py:

  • Template re-renders in ~0.4ms (parse and first style pass amortized away).
  • Thread scaling: the GIL is released during rendering; 4 threads → ~3.5× throughput.
  • Deterministic across platforms: CI renders a golden set on Linux, macOS, and Windows and asserts the outputs are byte-identical. Snapshot tests in your project can compare exact hashes.
  • Package ships type stubs (py.typed), so the API autocompletes and type-checks.

The first render pays a one-time system-font scan; after that the font collection is cached and cloned per render. Importing the module adds ~1MB RSS; memory stays flat under sustained rendering (no per-render growth — verified over 1000+ renders). On an Alpine/arm64 container the warm widget render measures ~0.8ms.

The GIL is released during rendering, so concurrent renders from Python threads scale and async event loops aren't blocked.

Why not a headless browser?

Playwright/Chromium render HTML too — at ~150MB+ of install, a browser process to babysit, and cold starts in the hundreds of milliseconds. blitz-py is a ~7MB self-contained wheel with millisecond renders, suitable for embedded targets like Home Assistant integrations generating widget images for small displays (its original use case).

License

MIT OR Apache-2.0. Bundled Inter font: SIL OFL 1.1 (assets/LICENSE-Inter.txt).

Download files

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

Source Distribution

blitz_py-0.4.1.tar.gz (1.3 MB view details)

Uploaded Source

Built Distributions

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

blitz_py-0.4.1-cp310-abi3-win_arm64.whl (4.7 MB view details)

Uploaded CPython 3.10+Windows ARM64

blitz_py-0.4.1-cp310-abi3-win_amd64.whl (5.2 MB view details)

Uploaded CPython 3.10+Windows x86-64

blitz_py-0.4.1-cp310-abi3-musllinux_1_2_x86_64.whl (5.9 MB view details)

Uploaded CPython 3.10+musllinux: musl 1.2+ x86-64

blitz_py-0.4.1-cp310-abi3-musllinux_1_2_aarch64.whl (5.6 MB view details)

Uploaded CPython 3.10+musllinux: musl 1.2+ ARM64

blitz_py-0.4.1-cp310-abi3-manylinux_2_28_x86_64.whl (5.7 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.28+ x86-64

blitz_py-0.4.1-cp310-abi3-manylinux_2_28_aarch64.whl (5.4 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.28+ ARM64

blitz_py-0.4.1-cp310-abi3-macosx_11_0_arm64.whl (5.0 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

blitz_py-0.4.1-cp310-abi3-macosx_10_12_x86_64.whl (5.4 MB view details)

Uploaded CPython 3.10+macOS 10.12+ x86-64

File details

Details for the file blitz_py-0.4.1.tar.gz.

File metadata

  • Download URL: blitz_py-0.4.1.tar.gz
  • Upload date:
  • Size: 1.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for blitz_py-0.4.1.tar.gz
Algorithm Hash digest
SHA256 3cac281f4c9a95eb59490709fc601e50c51f7a51088afc1bff7d10bee13a60d1
MD5 d54e940826e2965dce50d5c1f4793789
BLAKE2b-256 d16930ab8689fd2e1f17f8a2d0d549a41a37734a1d261e725194d5a8305a508f

See more details on using hashes here.

Provenance

The following attestation bundles were made for blitz_py-0.4.1.tar.gz:

Publisher: ci.yml on adrienbrault/blitz-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file blitz_py-0.4.1-cp310-abi3-win_arm64.whl.

File metadata

  • Download URL: blitz_py-0.4.1-cp310-abi3-win_arm64.whl
  • Upload date:
  • Size: 4.7 MB
  • Tags: CPython 3.10+, Windows ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for blitz_py-0.4.1-cp310-abi3-win_arm64.whl
Algorithm Hash digest
SHA256 aa2e263d1d721d541502e7a89d1bafcb4218ac825a11b12b87129f1c6e03a4f7
MD5 d31845c031b70f9bd5426288fafd25d0
BLAKE2b-256 b3e8dd0e384edb381e2b570c2737dec39d42f9feae2773da8f063de604831857

See more details on using hashes here.

Provenance

The following attestation bundles were made for blitz_py-0.4.1-cp310-abi3-win_arm64.whl:

Publisher: ci.yml on adrienbrault/blitz-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file blitz_py-0.4.1-cp310-abi3-win_amd64.whl.

File metadata

  • Download URL: blitz_py-0.4.1-cp310-abi3-win_amd64.whl
  • Upload date:
  • Size: 5.2 MB
  • Tags: CPython 3.10+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for blitz_py-0.4.1-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 c8f3f6b37cdbd929733d8cba9a03f1fb86d636d3d0cb2a31f310e3bb44ed35e3
MD5 cc22e80d048319d4185a7169096989a3
BLAKE2b-256 fb04dfb7ba870176391402c4f19fe566bdcb3e3b3ec9db778e2483cb28f9486f

See more details on using hashes here.

Provenance

The following attestation bundles were made for blitz_py-0.4.1-cp310-abi3-win_amd64.whl:

Publisher: ci.yml on adrienbrault/blitz-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file blitz_py-0.4.1-cp310-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for blitz_py-0.4.1-cp310-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 e3b5b467a059b2a22b13e315ef76ffbc8e5b4ca4c1157430149f86f953e39cc9
MD5 029dd6f911ff5a38bfe6e0ed9af5bba1
BLAKE2b-256 445449af323c9dda8f28bbd76443e2497a1df3ec0cc9c2e28b77e4b1e9451859

See more details on using hashes here.

Provenance

The following attestation bundles were made for blitz_py-0.4.1-cp310-abi3-musllinux_1_2_x86_64.whl:

Publisher: ci.yml on adrienbrault/blitz-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file blitz_py-0.4.1-cp310-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for blitz_py-0.4.1-cp310-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 452140e06c6eab395120d2b4aba9af86e4bf23f9acce7cb71e44c47410e451b4
MD5 3a26a124bad9a30bd9a2f5d816e320fd
BLAKE2b-256 113c6ea8c9e7e2421f2ed746fb6da8f660c96bca1e61aa99c1e6691efe306c82

See more details on using hashes here.

Provenance

The following attestation bundles were made for blitz_py-0.4.1-cp310-abi3-musllinux_1_2_aarch64.whl:

Publisher: ci.yml on adrienbrault/blitz-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file blitz_py-0.4.1-cp310-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for blitz_py-0.4.1-cp310-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 9120de34406d772ccdbc99f4d295d3475d8e6be6adaec3ae794f9d064c066b59
MD5 1cd1999313f7701c98cbe62404a454fd
BLAKE2b-256 125b92ddd73869d97fae00746f7106a3de548dcdb5206c492a4ca78ea12a1fee

See more details on using hashes here.

Provenance

The following attestation bundles were made for blitz_py-0.4.1-cp310-abi3-manylinux_2_28_x86_64.whl:

Publisher: ci.yml on adrienbrault/blitz-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file blitz_py-0.4.1-cp310-abi3-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for blitz_py-0.4.1-cp310-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 f6e466c6ec33a182b8332fd180a00e398218a0be3803ecccbeaebe723ceccc1b
MD5 33a72172d0db213dfbd96f67085bb05d
BLAKE2b-256 280477ab0ce405c5dcc8b0dd5e7771c9f0af3c4490c5e1f1bb3bbca87afe90e0

See more details on using hashes here.

Provenance

The following attestation bundles were made for blitz_py-0.4.1-cp310-abi3-manylinux_2_28_aarch64.whl:

Publisher: ci.yml on adrienbrault/blitz-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file blitz_py-0.4.1-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for blitz_py-0.4.1-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 68e67d691db6686969eb28a3fe8b848a77c9ed60d3f67e6de0d7fb405126d13a
MD5 dd555e4a0b9c165011f0081a22d9d751
BLAKE2b-256 6ceed1bab51554b037aa7bc3952ea553348671dbb6fd10b50e40113370830cb5

See more details on using hashes here.

Provenance

The following attestation bundles were made for blitz_py-0.4.1-cp310-abi3-macosx_11_0_arm64.whl:

Publisher: ci.yml on adrienbrault/blitz-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file blitz_py-0.4.1-cp310-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for blitz_py-0.4.1-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 c741bf19b595091b24a648102750f2e1cbb921b3a8261e5de39c009be422ce7c
MD5 caa7b4e829fc925b66d96fae7e16f034
BLAKE2b-256 6833ad2da49bfe337373a6700b3e67133b904648fc7665e89a0a769cc90fb8df

See more details on using hashes here.

Provenance

The following attestation bundles were made for blitz_py-0.4.1-cp310-abi3-macosx_10_12_x86_64.whl:

Publisher: ci.yml on adrienbrault/blitz-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

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