Skip to content

Render

render

Rendering helpers for deckui keys, cards, and touchscreen.

ImageFetchError

Bases: Exception

Raised when a remote image cannot be fetched or decoded.

Source code in src/deckui/render/image_fetch.py
class ImageFetchError(Exception):
    """Raised when a remote image cannot be fetched or decoded."""

RenderMetrics

Computed rendering metrics for a specific device.

All fields are derived from the device's :class:~deckui.runtime.capabilities.DeviceCapabilities — there are no model-specific defaults. Panel dimensions are computed without margins or gaps; consumers are responsible for any spacing they want to apply in their own SVG/layout.

Parameters:

Name Type Description Default
caps DeviceCapabilities

Device capabilities to derive metrics from.

required
Source code in src/deckui/render/metrics.py
class RenderMetrics:
    """Computed rendering metrics for a specific device.

    All fields are derived from the device's
    :class:`~deckui.runtime.capabilities.DeviceCapabilities` — there are
    no model-specific defaults. Panel dimensions are computed without
    margins or gaps; consumers are responsible for any spacing they want
    to apply in their own SVG/layout.

    Parameters
    ----------
    caps
        Device capabilities to derive metrics from.
    """

    def __init__(self, caps: DeviceCapabilities) -> None:
        self._caps = caps

        self.key_size: tuple[int, int] = (caps.key_pixel_width, caps.key_pixel_height)
        self.key_image_format = caps.key_image_format
        self.key_count = caps.key_count

        self.touchscreen_width = caps.touchscreen_width
        self.touchscreen_height = caps.touchscreen_height
        self.panel_count = caps.panel_count

        if caps.has_touchscreen and self.panel_count > 0:
            self.panel_width = caps.touchscreen_width // self.panel_count
            self.panel_height = caps.touchscreen_height
        else:
            self.panel_width = 0
            self.panel_height = 0

        self.screen_width = caps.screen_width
        self.screen_height = caps.screen_height

        self.dial_count = caps.dial_count

RasterizeError

Bases: Exception

Raised when SVG rasterisation fails.

Source code in src/deckui/render/svg_rasterize.py
class RasterizeError(Exception):
    """Raised when SVG rasterisation fails."""

SvgRasterizer

Bases: Protocol

Protocol for SVG-to-PNG rasterisation backends.

Any object that implements :meth:rasterize with the correct signature can be used as a backend.

Source code in src/deckui/render/svg_rasterize.py
@runtime_checkable
class SvgRasterizer(Protocol):
    """Protocol for SVG-to-PNG rasterisation backends.

    Any object that implements :meth:`rasterize` with the correct
    signature can be used as a backend.
    """

    def rasterize(self, svg_data: bytes, width: int, height: int) -> bytes:
        """Convert raw SVG bytes to PNG bytes.

        Parameters
        ----------
        svg_data : bytes
            Raw SVG content.
        width : int
            Desired output width in pixels.
        height : int
            Desired output height in pixels.

        Returns
        -------
        bytes
            Rasterised PNG image bytes.

        Raises
        ------
        RasterizeError
            If rasterisation fails.
        """
        pass  # pragma: no cover

rasterize

rasterize(svg_data: bytes, width: int, height: int) -> bytes

Convert raw SVG bytes to PNG bytes.

Parameters:

Name Type Description Default
svg_data bytes

Raw SVG content.

required
width int

Desired output width in pixels.

required
height int

Desired output height in pixels.

required

Returns:

Type Description
bytes

Rasterised PNG image bytes.

Raises:

Type Description
RasterizeError

If rasterisation fails.

Source code in src/deckui/render/svg_rasterize.py
def rasterize(self, svg_data: bytes, width: int, height: int) -> bytes:
    """Convert raw SVG bytes to PNG bytes.

    Parameters
    ----------
    svg_data : bytes
        Raw SVG content.
    width : int
        Desired output width in pixels.
    height : int
        Desired output height in pixels.

    Returns
    -------
    bytes
        Rasterised PNG image bytes.

    Raises
    ------
    RasterizeError
        If rasterisation fails.
    """
    pass  # pragma: no cover

fetch_image

fetch_image(url: str) -> Image.Image

Fetch a remote image by URL and return it as a PIL Image.

The result is cached in-process; subsequent calls with the same URL return the cached :class:~PIL.Image.Image immediately. Negative lookups (network failure, invalid image data) are also cached so we do not retry broken URLs on every render.

Parameters:

Name Type Description Default
url str

Fully-qualified HTTP(S) URL pointing to an image resource.

required

Returns:

Type Description
Image

The decoded image.

Raises:

Type Description
ImageFetchError

If the URL is malformed, the network request fails, or the response cannot be decoded as a valid image.

Source code in src/deckui/render/image_fetch.py
def fetch_image(url: str) -> Image.Image:
    """Fetch a remote image by URL and return it as a PIL Image.

    The result is cached in-process; subsequent calls with the same URL
    return the cached :class:`~PIL.Image.Image` immediately.  Negative
    lookups (network failure, invalid image data) are also cached so we
    do not retry broken URLs on every render.

    Parameters
    ----------
    url : str
        Fully-qualified HTTP(S) URL pointing to an image resource.

    Returns
    -------
    PIL.Image.Image
        The decoded image.

    Raises
    ------
    ImageFetchError
        If the URL is malformed, the network request fails, or the
        response cannot be decoded as a valid image.
    """
    _validate_url(url)

    with _cache_lock:
        if url in _cache:
            cached = _cache[url]
            if cached is None:
                raise ImageFetchError(f"Image at '{url}' previously failed to load")
            return cached.copy()

    try:
        data = _http_get_bytes(url)
    except (urllib.error.URLError, OSError) as exc:
        with _cache_lock:
            _cache[url] = None
        raise ImageFetchError(f"Failed to fetch image from '{url}': {exc}") from exc

    try:
        img = Image.open(io.BytesIO(data))
        img.load()  # force full decode so errors surface now
    except (UnidentifiedImageError, OSError) as exc:
        with _cache_lock:
            _cache[url] = None
        raise ImageFetchError(
            f"Response from '{url}' is not a valid image: {exc}"
        ) from exc

    with _cache_lock:
        _cache[url] = img

    logger.debug("Fetched image from '%s' (%d bytes, %s)", url, len(data), img.size)
    return img.copy()

clear_image_cache

clear_image_cache() -> None

Drop all cached images. Primarily intended for tests.

Source code in src/deckui/render/image_fetch.py
def clear_cache() -> None:
    """Drop all cached images.  Primarily intended for tests."""
    with _cache_lock:
        _cache.clear()

render_blank_key

render_blank_key(key_size: tuple[int, int], image_format: str = 'JPEG') -> bytes

Render a blank key image at the given size.

Parameters:

Name Type Description Default
key_size tuple[int, int]

Key dimensions (width, height) in pixels.

required
image_format str

Image encoding format ("JPEG" or "BMP").

'JPEG'

Returns:

Type Description
bytes

Encoded blank-key image bytes.

Source code in src/deckui/render/key_renderer.py
def render_blank_key(
    key_size: tuple[int, int],
    image_format: str = "JPEG",
) -> bytes:
    """Render a blank key image at the given size.

    Parameters
    ----------
    key_size
        Key dimensions ``(width, height)`` in pixels.
    image_format
        Image encoding format (``"JPEG"`` or ``"BMP"``).

    Returns
    -------
    bytes
        Encoded blank-key image bytes.
    """
    return render_key_image(key_size=key_size, image_format=image_format)

render_key_image

render_key_image(key_size: tuple[int, int], icon: Image | None = None, background: str = 'black', image_format: str = 'JPEG') -> bytes

Render an image for a Stream Deck key.

The icon (if any) is resized to fill the entire key, edge-to-edge — the library does not impose margins or padding. Callers that want spacing should bake it into the source icon.

Parameters:

Name Type Description Default
key_size tuple[int, int]

Key dimensions (width, height) in pixels.

required
icon Image | None

Optional icon image to render on the key. Resized to key_size if it does not already match.

None
background str

Background colour name (used when icon is None or has alpha).

'black'
image_format str

Image encoding format ("JPEG" or "BMP").

'JPEG'

Returns:

Type Description
bytes

Encoded image bytes ready to send to the device.

Source code in src/deckui/render/key_renderer.py
def render_key_image(
    key_size: tuple[int, int],
    icon: Image.Image | None = None,
    background: str = "black",
    image_format: str = "JPEG",
) -> bytes:
    """Render an image for a Stream Deck key.

    The icon (if any) is resized to fill the entire key, edge-to-edge —
    the library does not impose margins or padding. Callers that want
    spacing should bake it into the source icon.

    Parameters
    ----------
    key_size
        Key dimensions ``(width, height)`` in pixels.
    icon
        Optional icon image to render on the key. Resized to ``key_size``
        if it does not already match.
    background
        Background colour name (used when *icon* is ``None`` or has alpha).
    image_format
        Image encoding format (``"JPEG"`` or ``"BMP"``).

    Returns
    -------
    bytes
        Encoded image bytes ready to send to the device.
    """
    img = Image.new("RGB", key_size, background)

    if icon is not None:
        if icon.size != key_size:
            icon = icon.resize(key_size, Image.Resampling.LANCZOS)

        if icon.mode == "RGBA":
            img.paste(icon, (0, 0), icon)
        else:
            img.paste(icon, (0, 0))

    return _encode_image(img, image_format)

render_info_screen

render_info_screen(image: Image | None, width: int, height: int, background: str = 'black', image_format: str = 'JPEG') -> bytes

Render an info screen image.

Parameters:

Name Type Description Default
image Image | None

Optional PIL Image to display. If None, a blank black screen is rendered.

required
width int

Screen width in pixels.

required
height int

Screen height in pixels.

required
background str

Background colour.

'black'
image_format str

Encoding format ("JPEG" or "BMP").

'JPEG'

Returns:

Type Description
bytes

Encoded image bytes.

Source code in src/deckui/render/screen_renderer.py
def render_info_screen(
    image: Image.Image | None,
    width: int,
    height: int,
    background: str = "black",
    image_format: str = "JPEG",
) -> bytes:
    """Render an info screen image.

    Parameters
    ----------
    image
        Optional PIL Image to display. If ``None``, a blank
        black screen is rendered.
    width
        Screen width in pixels.
    height
        Screen height in pixels.
    background
        Background colour.
    image_format
        Encoding format (``"JPEG"`` or ``"BMP"``).

    Returns
    -------
    bytes
        Encoded image bytes.
    """
    canvas = Image.new("RGB", (width, height), background)

    if image is not None:
        if image.size != (width, height):
            image = image.resize((width, height), Image.Resampling.LANCZOS)
        if image.mode == "RGBA":
            canvas.paste(image, (0, 0), image)
        else:
            canvas.paste(image, (0, 0))

    buf = io.BytesIO()
    fmt = image_format.upper()
    if fmt == "BMP":
        canvas.save(buf, format="BMP")
    else:
        canvas.save(buf, format="JPEG", quality=90)
    return buf.getvalue()

get_svg_backend

get_svg_backend() -> str

Return the name of the currently active SVG backend.

Returns:

Type Description
str

"auto" when no explicit backend has been chosen, otherwise the name passed to :func:set_svg_backend.

Source code in src/deckui/render/svg_rasterize.py
def get_svg_backend() -> str:
    """Return the name of the currently active SVG backend.

    Returns
    -------
    str
        ``"auto"`` when no explicit backend has been chosen, otherwise
        the name passed to :func:`set_svg_backend`.
    """
    return _active_backend or "auto"

list_svg_backends

list_svg_backends() -> list[str]

Return the names of all registered SVG backends.

Returns:

Type Description
list[str]

Sorted list of registered backend names.

Source code in src/deckui/render/svg_rasterize.py
def list_svg_backends() -> list[str]:
    """Return the names of all registered SVG backends.

    Returns
    -------
    list[str]
        Sorted list of registered backend names.
    """
    return sorted(_registry)

register_svg_backend

register_svg_backend(name: str, backend: SvgRasterizer) -> None

Register an SVG rasterisation backend.

Parameters:

Name Type Description Default
name str

Unique name for the backend (e.g. "cairo", "pyvips").

required
backend SvgRasterizer

An object implementing the :class:SvgRasterizer protocol.

required

Raises:

Type Description
TypeError

If backend does not implement :class:SvgRasterizer.

Source code in src/deckui/render/svg_rasterize.py
def register_svg_backend(name: str, backend: SvgRasterizer) -> None:
    """Register an SVG rasterisation backend.

    Parameters
    ----------
    name : str
        Unique name for the backend (e.g. ``"cairo"``, ``"pyvips"``).
    backend : SvgRasterizer
        An object implementing the :class:`SvgRasterizer` protocol.

    Raises
    ------
    TypeError
        If *backend* does not implement :class:`SvgRasterizer`.
    """
    if not isinstance(backend, SvgRasterizer):
        raise TypeError(
            f"backend must implement SvgRasterizer protocol, got {type(backend).__name__}"
        )
    _registry[name] = backend
    logger.debug("Registered SVG backend %r", name)

set_svg_backend

set_svg_backend(name: str, *, verify: bool = True) -> None

Select the active SVG rasterisation backend by name.

By default, performs a smoke test to verify the backend can rasterise a trivial SVG. Pass verify=False to skip.

Parameters:

Name Type Description Default
name str

Name of a previously registered backend, or "auto" to use automatic fallback ordering.

required
verify bool

When True, render a tiny SVG to confirm the backend works. Raises :class:RasterizeError immediately if it does not.

True

Raises:

Type Description
ValueError

If name is not "auto" and has not been registered.

RasterizeError

If verify is True and the backend cannot rasterise.

Source code in src/deckui/render/svg_rasterize.py
def set_svg_backend(name: str, *, verify: bool = True) -> None:
    """Select the active SVG rasterisation backend by name.

    By default, performs a smoke test to verify the backend can
    rasterise a trivial SVG.  Pass ``verify=False`` to skip.

    Parameters
    ----------
    name : str
        Name of a previously registered backend, or ``"auto"`` to use
        automatic fallback ordering.
    verify : bool, default=True
        When *True*, render a tiny SVG to confirm the backend works.
        Raises :class:`RasterizeError` immediately if it does not.

    Raises
    ------
    ValueError
        If *name* is not ``"auto"`` and has not been registered.
    RasterizeError
        If *verify* is True and the backend cannot rasterise.
    """
    global _active_backend  # noqa: PLW0603
    if name != "auto" and name not in _registry:
        raise ValueError(
            f"Unknown SVG backend {name!r}. "
            f"Registered backends: {', '.join(sorted(_registry)) or '(none)'}"
        )
    if verify and name != "auto":
        _test_svg = (
            b'<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">'
            b'<rect width="1" height="1"/></svg>'
        )
        _registry[name].rasterize(_test_svg, 1, 1)
    _active_backend = name
    logger.debug("Active SVG backend set to %r", name)

compose_touchstrip

compose_touchstrip(cards: list[Image | None], *, touchscreen_width: int, touchscreen_height: int, panel_count: int, panel_width: int, background: str = 'black', image_format: str = 'JPEG') -> bytes

Compose card images into a single touchscreen image.

Cards are tiled edge-to-edge across the touchscreen — the library imposes no margins or gaps. Card i starts at (i * panel_width, 0) and is expected to be panel_width wide and touchscreen_height tall. The background colour shows through wherever a slot is None or a card image leaves pixels uncovered.

Parameters:

Name Type Description Default
cards list[Image | None]

Card images (or None for blank slots).

required
touchscreen_width int

Total touchscreen width in pixels.

required
touchscreen_height int

Total touchscreen height in pixels.

required
panel_count int

Number of card zones (slots beyond this are silently dropped).

required
panel_width int

Width of each card panel in pixels.

required
background str

Fill colour for the canvas where no card is drawn.

'black'
image_format str

Image encoding format ("JPEG" or "BMP").

'JPEG'

Returns:

Type Description
bytes

Encoded touchscreen image bytes.

Source code in src/deckui/render/touch_renderer.py
def compose_touchstrip(
    cards: list[Image.Image | None],
    *,
    touchscreen_width: int,
    touchscreen_height: int,
    panel_count: int,
    panel_width: int,
    background: str = "black",
    image_format: str = "JPEG",
) -> bytes:
    """Compose card images into a single touchscreen image.

    Cards are tiled edge-to-edge across the touchscreen — the library
    imposes no margins or gaps. Card *i* starts at
    ``(i * panel_width, 0)`` and is expected to be ``panel_width`` wide
    and ``touchscreen_height`` tall. The *background* colour shows
    through wherever a slot is ``None`` or a card image leaves pixels
    uncovered.

    Parameters
    ----------
    cards
        Card images (or ``None`` for blank slots).
    touchscreen_width
        Total touchscreen width in pixels.
    touchscreen_height
        Total touchscreen height in pixels.
    panel_count
        Number of card zones (slots beyond this are silently dropped).
    panel_width
        Width of each card panel in pixels.
    background
        Fill colour for the canvas where no card is drawn.
    image_format
        Image encoding format (``"JPEG"`` or ``"BMP"``).

    Returns
    -------
    bytes
        Encoded touchscreen image bytes.
    """
    img = Image.new("RGB", (touchscreen_width, touchscreen_height), background)

    for index, card_image in enumerate(cards):
        if index >= panel_count:
            break
        if card_image is not None:
            img.paste(card_image, (index * panel_width, 0))

    return _encode_image(img, image_format)

render_blank_touchscreen

render_blank_touchscreen(*, touchscreen_width: int, touchscreen_height: int, panel_count: int, panel_width: int, background: str = 'black', image_format: str = 'JPEG') -> bytes

Render a blank touch-strip image.

Parameters:

Name Type Description Default
touchscreen_width int

Total touchscreen width in pixels.

required
touchscreen_height int

Total touchscreen height in pixels.

required
panel_count int

Number of card zones.

required
panel_width int

Width of each card panel in pixels.

required
background str

Fill colour for the canvas.

'black'
image_format str

Image encoding format ("JPEG" or "BMP").

'JPEG'

Returns:

Type Description
bytes

Encoded blank touchscreen image bytes.

Source code in src/deckui/render/touch_renderer.py
def render_blank_touchscreen(
    *,
    touchscreen_width: int,
    touchscreen_height: int,
    panel_count: int,
    panel_width: int,
    background: str = "black",
    image_format: str = "JPEG",
) -> bytes:
    """Render a blank touch-strip image.

    Parameters
    ----------
    touchscreen_width
        Total touchscreen width in pixels.
    touchscreen_height
        Total touchscreen height in pixels.
    panel_count
        Number of card zones.
    panel_width
        Width of each card panel in pixels.
    background
        Fill colour for the canvas.
    image_format
        Image encoding format (``"JPEG"`` or ``"BMP"``).

    Returns
    -------
    bytes
        Encoded blank touchscreen image bytes.
    """
    return compose_touchstrip(
        [None] * panel_count,
        touchscreen_width=touchscreen_width,
        touchscreen_height=touchscreen_height,
        panel_count=panel_count,
        panel_width=panel_width,
        background=background,
        image_format=image_format,
    )