Skip to content
led-ticker

image widget

The image widget displays a single still image at native physical resolution — each LED maps to one pixel in the source. Use it for logos, product photos, brand artwork, or any single-frame graphic. This widget is essentially gif with a single frame: it shares the gif widget’s full feature surface (fit modes, text overlay, two-row, animation, border) minus the per-frame mechanics — the only widget-specific knob is hold_time instead of play_count.

Phoenix png with scroll_over text overlay
Phoenix png with scroll_over text overlay

Minimal example

[[playlist.section.widget]]
type = "image"
path = "config/assets/my_logo.png"
hold_time = 4.0

Image source

Option Type Default Description
path string required Path to source file. Relative paths resolve against the config.toml directory. Single PNG / JPG / single-frame GIF are the primary use cases. For animated files, only frame 0 is decoded — use the gif widget for animation.
fit string "pillarbox" How the image fills the panel: "pillarbox" (scale by height, black bands on sides), "letterbox" (scale by width, black bands top/bottom), "stretch" (fill panel, distorts aspect ratio), "crop" (scale to cover, center-crop excess).
image_align string "center" Horizontal anchor for pillarbox: "left", "center", or "right". Ignored by the other three fit modes (they always fill panel width).

Display duration

Option Type Default Description
hold_time float 5.0 How long to display the image per visit. With text_loops > 0, becomes a duration floor: the section runs for max(hold_time, text_loops × traversal) so a long marquee always completes. Minimum 0.05.

Single-row text overlay

Option Type Default Description
text string "" Optional text rendered alongside the image. Inline :slug: emoji are rendered as pixel art. Leave empty for a silent image with no text.
text_align string "auto" Where text appears: "auto" picks the side opposite image_align so they don't overlap; "left" / "right" place text statically; "scroll" scrolls text behind a transparent image silhouette; "scroll_over" scrolls text in front of the image.
text_valign string "center" Vertical text anchor: "top", "center", or "bottom".
text_y_offset int 0 Logical-pixel shift on top of the text_valign baseline. Negative = up, positive = down.
text_x_offset int 0 Horizontal nudge for static text (text_align = "left" or "right"). Positive = right, negative = left. Rejected when used with scroll modes.
scroll_direction string "left" Direction the marquee travels: "left" or "right". Only applies when text_align is "scroll" or "scroll_over".
scroll_speed_ms int 50 Tick cadence in milliseconds when text scrolls. Minimum 20.
font string "6x12" BDF font alias (e.g. "5x8", "6x12") or hires font name (e.g. "Inter-Regular"). Default is the 6×12 BDF.
font_size int none Real-pixel font size. For BDF: snaps down to the nearest integer multiple of cell height, defaults to cell_h × section_scale (12 on the small sign, 48 on bigsign at scale=4). For hires fonts: required — raises at config-load without a hint if unset.
font_threshold int 0–255 128 Rasterization threshold for hires fonts. Lower = thicker glyphs; useful for thin-stroked fonts like Beloved Sans Regular at ~80. BDF fonts ignore this.
font_color RGB list / string / table [255, 255, 0] Text color. Constant [r,g,b], "rainbow", "color_cycle", "random", or {style="gradient", from=[...], to=[...]}.
text_loops int 0 Minimum number of full marquee traversals before the section transitions. Only meaningful with text_align = "scroll" or "scroll_over". 0 = no floor (one traversal is always guaranteed). In wrap mode (text_wrap = true), counts complete cycle traversals (one cycle = text + separator) instead.
text_wrap bool false Seamless wrap mode for the marquee. When true, the text repeats with a separator between copies and at least one full copy is on the panel at every tick. Requires text_align to be "scroll" or "scroll_over"; rejected in two-row mode.
text_separator string " • " (when text_wrap = true) Glyph(s) drawn between repeats in wrap mode. "" falls back to a two-space gap. Rendered in the widget's font — per-separator font override isn't supported in v1.
text_separator_color color spec inherit font_color Color for the separator in wrap mode. Whole-string provider (one hue per frame); accepts the same value types as font_color.

Two-row text overlay

Setting bottom_text to a non-empty string switches the widget to two-row mode: the image sits underneath, a held top row sits at the chosen alignment, and the bottom row scrolls when its text overflows the canvas width. Single-row text_align, text_valign, text_x_offset, and font_size are rejected in this mode — use the per-row knobs below.

Option Type Default Description
bottom_text string "" Setting this non-empty activates two-row mode. Text for the scrolling bottom row; auto-scrolls left when it overflows.
top_text string "" Text for the held top row. Falls back to text when unset (back-compat alias — set one or the other, not both).
top_color RGB list / string / table (same as font_color) Top-row text color. Same value types as font_color. Defaults to font_color when unset.
bottom_color RGB list / string / table (same as font_color) Bottom-row text color. Defaults to font_color when unset.
top_align string "center" Top-row horizontal alignment when text fits: "left", "center", or "right".
bottom_align string "center" Bottom-row alignment when text fits without scrolling. Ignored when the bottom row overflows.
top_font string (same as font) Per-row font override for the top row. Falls back to font when unset.
top_font_size int (BDF cell height) Real-pixel font size for hires top_font. Required when top_font is a hires font name.
top_font_threshold int 0–255 128 Rasterization threshold for hires top_font.
bottom_font string (same as font) Per-row font override for the bottom row. Falls back to font when unset.
bottom_font_size int (BDF cell height) Real-pixel font size for hires bottom_font. Required when bottom_font is a hires font name.
bottom_font_threshold int 0–255 128 Rasterization threshold for hires bottom_font.
top_text_y_offset int 0 Vertical nudge for the top row's text in logical rows. Negative = up.
bottom_text_y_offset int 0 Vertical nudge for the bottom row's text in logical rows.
top_emoji_y_offset int 0 Vertical nudge for the top row's emoji in logical rows. Set equal to top_text_y_offset to shift the whole row together.
bottom_emoji_y_offset int 0 Vertical nudge for the bottom row's emoji in logical rows.
top_row_height int none Give the top band exactly N logical rows; the bottom gets the remainder. Default None splits 50/50. Use when the top and bottom rows need different font sizes. Must be > 0 and < canvas.height.
bottom_text_wrap bool false Seamless wrap mode for the bottom row in two-row mode. When true, the bottom row repeats with a separator between copies and at least one full copy is on the panel at every tick. Requires bottom_text non-empty; top row never wraps. Only allowed in mode = "slideshow".
bottom_text_separator string " • " (when bottom_text_wrap = true) Glyph(s) drawn between bottom-row repeats in wrap mode. "" falls back to a two-space gap. Rendered in the bottom row's font.
bottom_text_separator_color color spec inherit bottom_color Color for the bottom separator in wrap mode. Whole-string provider (one hue per frame); accepts the same value types as bottom_color. Inherits bottom_color (NOT font_color) — separator is part of the bottom row.
text_loops int 0 Minimum number of full bottom-row wrap cycles before the section transitions in two-row mode (same field as single-row mode, reinterpreted as bottom-row cycle count when bottom_text_wrap = true). On the two_row widget the equivalent is bottom_text_loops.

Animation

Option Type Default Description
animation string none "typewriter" for character-by-character text reveal. Single-row only — raises if bottom_text is set or text_align is "scroll" / "scroll_over". See rule 14.

Border and background

Option Type Default Description
border "rainbow" | "color_cycle" | "lightbulbs" | [r,g,b] | {style="...", ...} none Perimeter border ring — five styles (rainbow chase, color cycle, constant, bands, lightbulbs); see /concepts/borders/. Paints at physical panel resolution (bypasses ScaledCanvas), so the border traces the real panel edge. Paints after the image and before text.
bg_color RGB list none Background fill for the full canvas before image and text. When set, pillarbox bands and alpha-transparent regions reveal this color instead of black. Use with a transparent-background PNG to blend the silhouette against a solid color.
transparent phoenix centered with `pillarbox` fit
transparent phoenix centered with `pillarbox` fit
[[playlist.section.widget]]
type = "image"
path = "config/assets/phoenix_transparent.png"
fit = "pillarbox"
image_align = "center"
hold_time = 4.0

Image with scrolling text overlay (scroll_over — text in front of the image)

Section titled “Image with scrolling text overlay (scroll_over — text in front of the image)”
phoenix pinned to the left, magenta text marquees over the right side
phoenix pinned to the left, magenta text marquees over the right side
[[playlist.section.widget]]
type = "image"
path = "config/assets/phoenix_transparent.png"
fit = "pillarbox"
image_align = "left"
text = "Follow us on Instagram!"
text_align = "scroll_over"
hold_time = 7.0
font_color = [200, 100, 255]

Scrolling text behind a transparent silhouette (scroll — text walks under the image; requires a PNG with transparency)

Section titled “Scrolling text behind a transparent silhouette (scroll — text walks under the image; requires a PNG with transparency)”
text scrolls under the phoenix — visible only through the transparent pillarbox area
text scrolls under the phoenix — visible only through the transparent pillarbox area
[[playlist.section.widget]]
type = "image"
path = "config/assets/phoenix_transparent.png"
fit = "pillarbox"
image_align = "left"
text = "Scrolling behind the phoenix!"
text_align = "scroll"
font_color = [200, 200, 255]

Wrap mode (seamless marquee with a separator between repeats)

Section titled “Wrap mode (seamless marquee with a separator between repeats)”
`text_wrap = true` — cyan `hi friend` repeats with a steady yellow `•` separator between copies. The message is always visible somewhere on the panel; the separator color is independent of `font_color`.
`text_wrap = true` — cyan `hi friend` repeats with a steady yellow `•` separator between copies. The message is always visible somewhere on the panel; the separator color is independent of `font_color`.

Setting text_wrap = true on a scroll or scroll_over marquee runs the text seamlessly — at every tick at least one full copy of the message is on the panel, with a configurable separator drawn between repeats. Use this for short slogans or repeating banners where the gap of an empty canvas between traversals breaks the rhythm.

[[playlist.section.widget]]
type = "image"
path = "config/assets/phoenix_transparent.png"
text = "hi friend"
text_wrap = true
text_align = "scroll_over"
font_color = [120, 230, 255] # cyan
text_separator_color = [255, 200, 80] # steady yellow separator
# default separator: " • " # (omit text_separator to use it)
FieldTypeDefaultMeaning
text_wrapboolfalseToggle seamless wrap. Requires text_align ∈ ("scroll", "scroll_over").
text_separatorstring" • " (when text_wrap = true)Glyph(s) between repeats. "" falls back to a two-space gap.
text_separator_colorcolor specinherit font_colorColor for the separator; whole-string provider (one hue per frame).

Notes:

  • text_loops in wrap mode means minimum number of cycle traversals (one cycle = text + separator), not the off-right → off-left definition the default marquee uses. On the two_row widget the equivalent field is bottom_text_loops (prefixed to match bottom_text_wrap and friends on that widget).
  • Two-row mode (bottom_text set) refuses text_wrap in v1.
  • The separator renders in the widget’s font; a per-separator font override isn’t part of v1.
still phoenix on the left, held magenta `BREAKING` up top, cyan `tap to subscribe` wrapping continuously on the bottom with a rainbow `*` separator
still phoenix on the left, held magenta `BREAKING` up top, cyan `tap to subscribe` wrapping continuously on the bottom with a rainbow `*` separator

Setting bottom_text_wrap = true in two-row mode (when bottom_text is set) runs the bottom row as a seamless marquee — chasing itself across the canvas with a separator between repeats. Top row stays held at top_align; only the bottom row wraps. Engages even when the bottom text fits — predictable.

[[playlist.section.widget]]
type = "image"
path = "config/assets/phoenix_transparent.png"
top_text = "BREAKING"
top_color = [225, 48, 108]
bottom_text = "tap to subscribe"
bottom_text_wrap = true
bottom_text_separator = " * " # default: " • "
bottom_text_separator_color = "rainbow" # default: inherit bottom_color
FieldTypeDefaultMeaning
bottom_text_wrapboolfalseToggle seamless wrap on the bottom row. Requires bottom_text non-empty.
bottom_text_separatorstring" • " (when bottom_text_wrap = true)Glyph(s) between bottom-row repeats. "" falls back to a two-space gap.
bottom_text_separator_colorcolor specinherit bottom_colorColor for the bottom separator; whole-string provider (one hue per frame).

Notes:

  • bottom_text_wrap always wraps when set, even if the bottom text fits the canvas.
  • Top row never wraps (refused by validation; no top_text_wrap field exists).
  • The separator color inherits bottom_color (NOT font_color) — separator is part of the bottom row.
  • v1’s text_wrap stays single-row only — in two-row mode use bottom_text_wrap.
  • bottom_text_wrap is only allowed in mode = "slideshow". In ticker / one_at_a_time modes, validation refuses it (those modes expect widgets to terminate naturally).
  • Loop count for the bottom-row wrap: use text_loops here on gif/image widgets (same field that controls single-row wrap loops). On the two_row widget — which is always two-row, not optionally — the equivalent field is named bottom_text_loops to match its bottom-prefix convention. The validator catches the cross-widget typo (rule 29) and points you at the right name.

Scroll-through mode (bottom row, one offscreen pass)

Section titled “Scroll-through mode (bottom row, one offscreen pass)”
phoenix on the right, held magenta `@firebird.demo` on top, cream promo scrolling fully offscreen-to-offscreen on the bottom — keeps cycling within `hold_time`
phoenix on the right, held magenta `@firebird.demo` on top, cream promo scrolling fully offscreen-to-offscreen on the bottom — keeps cycling within `hold_time`

Setting bottom_text_scroll = "scroll_through" in two-row mode forces the bottom row to do an offscreen-to-offscreen scroll on every visit — text starts fully off the right edge and ends fully off the left edge — instead of the default “held when it fits, scroll only on overflow” behavior. bottom_align is ignored.

[[playlist.section.widget]]
type = "image"
path = "config/assets/phoenix_transparent.png"
fit = "pillarbox"
image_align = "right"
top_text = "@firebird.demo"
top_color = [225, 48, 108]
bottom_text = "Firebird Yoga — now booking spring sessions — your first class is free."
bottom_color = [255, 240, 200]
bottom_text_scroll = "scroll_through"
FieldTypeDefaultMeaning
bottom_text_scrollstring"marquee"Bottom-row scroll style. "marquee" preserves the held-on-fit / scroll-on-overflow default; "scroll_through" forces offscreen-to-offscreen passes on every visit.

Notes:

  • Mutually exclusive with bottom_text_wrap = true — pick one. Scroll-through is one pass; wrap is a seamless tile.
  • Requires non-empty bottom_text (two-row mode). Single-row text scroll lives under text_align = "scroll" / "scroll_over".
  • Only valid in mode = "slideshow". In ticker / one_at_a_time modes validation refuses it (rule 32).
  • hold_time is honored as a floor: at least one full pass per visit, or text_loops × (canvas_width + bottom_width) if larger, or whatever hold_time admits — whichever is greatest. The bottom row keeps cycling until that floor is met.

Two-row text overlay (set bottom_text to switch modes — image sits underneath)

Section titled “Two-row text overlay (set bottom_text to switch modes — image sits underneath)”
phoenix on the right, `@firebird.demo` on top, scrolling promo on the bottom
phoenix on the right, `@firebird.demo` on top, scrolling promo on the bottom
[[playlist.section.widget]]
type = "image"
path = "config/assets/phoenix_transparent.png"
fit = "pillarbox"
image_align = "right"
top_text = "@firebird.demo"
top_font = "5x8"
top_color = [225, 48, 108]
bottom_text = "Open daily 10am – 8pm"
bottom_font = "5x8"
bottom_color = [255, 240, 200]
hold_time = 7.0

Typewriter overlay (single-row only — letters type in next to the image)

Section titled “Typewriter overlay (single-row only — letters type in next to the image)”
phoenix on the left, `firebird` types in on the right with rainbow font_color
phoenix on the left, `firebird` types in on the right with rainbow font_color
[[playlist.section.widget]]
type = "image"
path = "config/assets/phoenix_transparent.png"
fit = "pillarbox"
image_align = "left"
text = "firebird"
text_align = "right"
font_color = "rainbow"
animation = "typewriter"
hold_time = 8.0

Typewriter + rainbow border (frame the panel as the text appears)

Section titled “Typewriter + rainbow border (frame the panel as the text appears)”
rainbow chase border around the perimeter, white text typing in beside the phoenix
rainbow chase border around the perimeter, white text typing in beside the phoenix
[[playlist.section.widget]]
type = "image"
path = "config/assets/phoenix_transparent.png"
fit = "pillarbox"
image_align = "left"
text = "firebird"
text_align = "right"
font_color = [255, 255, 255]
animation = "typewriter"
border = "rainbow"
hold_time = 8.0

animation = "typewriter" rejects three combinations: bottom_text set (two-row layout), text_align = "scroll" / "scroll_over" (cursor moves each tick), or empty text. Drop the conflicting setting; typewriter still composes with font_color = "rainbow" and border = "rainbow".

Stretch fills every pixel with image, leaving no transparent regions for the text to scroll through. Use text_align="scroll_over" for a fullscreen marquee, or pair scroll with a transparent PNG and fit="pillarbox".

Static offsets don’t compose with the per-tick scroll cursor. Set text_x_offset = 0 or change text_align to "left" or "right".

hold_time is in seconds, not milliseconds. Anything under 50 ms is rejected — nothing shorter than one engine tick can actually display.

The image widget decodes only the first frame of .gif / .webp files. Use the gif widget for animation — it shares the entire image text-overlay surface plus per-frame mechanics.