diff --git a/README.md b/README.md
index 9fae8532..d11c8b75 100644
--- a/README.md
+++ b/README.md
@@ -129,7 +129,7 @@ curl -X POST http://your-pi-ip:5000/api/v3/plugins/install \
| Plugin | Description | Preview |
|--------|-------------|---------|
| [Music Player](./plugins/ledmatrix-music/) | Now playing with album art (Spotify & YouTube Music) | |
-| [Static Image Display](./plugins/static-image/) | Image display with scaling and transparency | |
+| [Static Image Display](./plugins/static-image/) | Image display with scaling and transparency |
|
### Content (2)
diff --git a/docs/assets/static-image/background-color.png b/docs/assets/static-image/background-color.png
new file mode 100644
index 00000000..fac023aa
Binary files /dev/null and b/docs/assets/static-image/background-color.png differ
diff --git a/docs/assets/static-image/fit-and-aspect.png b/docs/assets/static-image/fit-and-aspect.png
new file mode 100644
index 00000000..ff4373d3
Binary files /dev/null and b/docs/assets/static-image/fit-and-aspect.png differ
diff --git a/docs/assets/static-image/hero.png b/docs/assets/static-image/hero.png
new file mode 100644
index 00000000..d0d19430
Binary files /dev/null and b/docs/assets/static-image/hero.png differ
diff --git a/docs/assets/static-image/panel-sizes.png b/docs/assets/static-image/panel-sizes.png
new file mode 100644
index 00000000..4caa0f1f
Binary files /dev/null and b/docs/assets/static-image/panel-sizes.png differ
diff --git a/docs/assets/static-image/sample/mark-a.png b/docs/assets/static-image/sample/mark-a.png
new file mode 100644
index 00000000..cc970443
Binary files /dev/null and b/docs/assets/static-image/sample/mark-a.png differ
diff --git a/docs/assets/static-image/sample/mark-b.png b/docs/assets/static-image/sample/mark-b.png
new file mode 100644
index 00000000..413f77d6
Binary files /dev/null and b/docs/assets/static-image/sample/mark-b.png differ
diff --git a/docs/assets/static-image/sample/transparent.png b/docs/assets/static-image/sample/transparent.png
new file mode 100644
index 00000000..fd63adc2
Binary files /dev/null and b/docs/assets/static-image/sample/transparent.png differ
diff --git a/docs/assets/static-image/sample/wide.png b/docs/assets/static-image/sample/wide.png
new file mode 100644
index 00000000..a91fb5f0
Binary files /dev/null and b/docs/assets/static-image/sample/wide.png differ
diff --git a/docs/assets/static-image/shots.json b/docs/assets/static-image/shots.json
new file mode 100644
index 00000000..0621dffb
--- /dev/null
+++ b/docs/assets/static-image/shots.json
@@ -0,0 +1,310 @@
+{
+ "plugin": "static-image",
+ "defaults": {
+ "width": 128,
+ "height": 32,
+ "scale": 6,
+ "freeze_time": "2026-09-02T16:30:00+00:00"
+ },
+ "shots": [
+ {
+ "name": "hero",
+ "config": {
+ "enabled": true,
+ "images": [
+ {
+ "id": "mark-a",
+ "path": "docs/assets/static-image/sample/mark-a.png",
+ "display_order": 0
+ }
+ ]
+ }
+ },
+ {
+ "name": "fit-on",
+ "config": {
+ "enabled": true,
+ "images": [
+ {
+ "id": "wide",
+ "path": "docs/assets/static-image/sample/wide.png",
+ "display_order": 0
+ }
+ ],
+ "fit_to_display": true
+ },
+ "standalone": false
+ },
+ {
+ "name": "fit-off",
+ "config": {
+ "enabled": true,
+ "images": [
+ {
+ "id": "wide",
+ "path": "docs/assets/static-image/sample/wide.png",
+ "display_order": 0
+ }
+ ],
+ "fit_to_display": false
+ },
+ "standalone": false
+ },
+ {
+ "name": "aspect-on",
+ "config": {
+ "enabled": true,
+ "images": [
+ {
+ "id": "wide",
+ "path": "docs/assets/static-image/sample/wide.png",
+ "display_order": 0
+ }
+ ],
+ "fit_to_display": true,
+ "preserve_aspect_ratio": true
+ },
+ "standalone": false
+ },
+ {
+ "name": "aspect-off",
+ "config": {
+ "enabled": true,
+ "images": [
+ {
+ "id": "wide",
+ "path": "docs/assets/static-image/sample/wide.png",
+ "display_order": 0
+ }
+ ],
+ "fit_to_display": true,
+ "preserve_aspect_ratio": false
+ },
+ "standalone": false
+ },
+ {
+ "name": "bg-black",
+ "config": {
+ "enabled": true,
+ "images": [
+ {
+ "id": "transparent",
+ "path": "docs/assets/static-image/sample/transparent.png",
+ "display_order": 0
+ }
+ ],
+ "background_color": [
+ 0,
+ 0,
+ 0
+ ]
+ },
+ "standalone": false
+ },
+ {
+ "name": "bg-navy",
+ "config": {
+ "enabled": true,
+ "images": [
+ {
+ "id": "transparent",
+ "path": "docs/assets/static-image/sample/transparent.png",
+ "display_order": 0
+ }
+ ],
+ "background_color": [
+ 20,
+ 30,
+ 90
+ ]
+ },
+ "standalone": false
+ },
+ {
+ "name": "bg-red",
+ "config": {
+ "enabled": true,
+ "images": [
+ {
+ "id": "transparent",
+ "path": "docs/assets/static-image/sample/transparent.png",
+ "display_order": 0
+ }
+ ],
+ "background_color": [
+ 120,
+ 20,
+ 20
+ ]
+ },
+ "standalone": false
+ },
+ {
+ "name": "bg-grey",
+ "config": {
+ "enabled": true,
+ "images": [
+ {
+ "id": "transparent",
+ "path": "docs/assets/static-image/sample/transparent.png",
+ "display_order": 0
+ }
+ ],
+ "background_color": [
+ 60,
+ 60,
+ 60
+ ]
+ },
+ "standalone": false
+ },
+ {
+ "name": "size-64x32",
+ "config": {
+ "enabled": true,
+ "images": [
+ {
+ "id": "mark-a",
+ "path": "docs/assets/static-image/sample/mark-a.png",
+ "display_order": 0
+ }
+ ]
+ },
+ "width": 64,
+ "height": 32,
+ "scale": 8,
+ "standalone": false
+ },
+ {
+ "name": "size-128x32",
+ "config": {
+ "enabled": true,
+ "images": [
+ {
+ "id": "mark-a",
+ "path": "docs/assets/static-image/sample/mark-a.png",
+ "display_order": 0
+ }
+ ]
+ },
+ "width": 128,
+ "height": 32,
+ "scale": 8,
+ "standalone": false
+ },
+ {
+ "name": "size-128x64",
+ "config": {
+ "enabled": true,
+ "images": [
+ {
+ "id": "mark-a",
+ "path": "docs/assets/static-image/sample/mark-a.png",
+ "display_order": 0
+ }
+ ]
+ },
+ "width": 128,
+ "height": 64,
+ "scale": 8,
+ "standalone": false
+ },
+ {
+ "name": "size-256x32",
+ "config": {
+ "enabled": true,
+ "images": [
+ {
+ "id": "mark-a",
+ "path": "docs/assets/static-image/sample/mark-a.png",
+ "display_order": 0
+ }
+ ]
+ },
+ "width": 256,
+ "height": 32,
+ "scale": 4,
+ "standalone": false
+ }
+ ],
+ "composites": [
+ {
+ "name": "fit-and-aspect",
+ "columns": 2,
+ "cells": [
+ {
+ "shot": "fit-on",
+ "label": "fit_to_display: true",
+ "sublabel": "the default; the 192x64 image is scaled to the panel"
+ },
+ {
+ "shot": "fit-off",
+ "label": "fit_to_display: false",
+ "sublabel": "drawn at native size, so it overflows"
+ },
+ {
+ "shot": "aspect-on",
+ "label": "preserve_aspect_ratio: true",
+ "sublabel": "the default; proportions kept, letterboxed"
+ },
+ {
+ "shot": "aspect-off",
+ "label": "preserve_aspect_ratio: false",
+ "sublabel": "stretched to fill the panel exactly"
+ }
+ ]
+ },
+ {
+ "name": "background-color",
+ "columns": 2,
+ "cells": [
+ {
+ "shot": "bg-black",
+ "label": "[0, 0, 0]",
+ "sublabel": "the default; transparent areas stay unlit"
+ },
+ {
+ "shot": "bg-navy",
+ "label": "[20, 30, 90]",
+ "sublabel": "transparent areas fill with the colour"
+ },
+ {
+ "shot": "bg-red",
+ "label": "[120, 20, 20]",
+ "sublabel": ""
+ },
+ {
+ "shot": "bg-grey",
+ "label": "[60, 60, 60]",
+ "sublabel": "every pixel lit \u2014 brightest and most power"
+ }
+ ]
+ },
+ {
+ "name": "panel-sizes",
+ "columns": 1,
+ "cells": [
+ {
+ "shot": "size-64x32",
+ "label": "64 x 32",
+ "sublabel": "the square mark is capped by panel height"
+ },
+ {
+ "shot": "size-128x32",
+ "label": "128 x 32",
+ "sublabel": "the common two-panel chain"
+ },
+ {
+ "shot": "size-128x64",
+ "label": "128 x 64",
+ "sublabel": "twice the height, so twice the mark"
+ },
+ {
+ "shot": "size-256x32",
+ "label": "256 x 32",
+ "sublabel": "height still caps it; the image centres"
+ }
+ ]
+ }
+ ]
+}
diff --git a/plugins.json b/plugins.json
index f900d57c..9ec1cf31 100644
--- a/plugins.json
+++ b/plugins.json
@@ -780,10 +780,10 @@
"plugin_path": "plugins/static-image",
"stars": 0,
"downloads": 0,
- "last_updated": "2026-07-17",
+ "last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
- "latest_version": "1.0.5"
+ "latest_version": "1.0.6"
},
{
"id": "stock-news",
diff --git a/plugins/static-image/README.md b/plugins/static-image/README.md
index e75f2b84..2f57e998 100644
--- a/plugins/static-image/README.md
+++ b/plugins/static-image/README.md
@@ -1,220 +1,337 @@
------------------------------------------------------------------------------------
-### Connect with ChuckBuilds
+# Static Image
-- Show support on Youtube: https://www.youtube.com/@ChuckBuilds
-- Stay in touch on Instagram: https://www.instagram.com/ChuckBuilds/
-- Want to chat or need support? Reach out on the ChuckBuilds Discord: https://discord.com/invite/uW36dVAtcT
-- Feeling Generous? Support the project:
- - Github Sponsorship: https://github.com/sponsors/ChuckBuilds
- - Buy Me a Coffee: https://buymeacoffee.com/chuckbuilds
- - Ko-fi: https://ko-fi.com/chuckbuilds/
+Show your own images on the LED matrix — a logo, a photo, a pixel-art piece.
+One image, or a set that rotates, with optional per-image schedules so a
+picture only appears at the times you choose.
------------------------------------------------------------------------------------
+
-# Static Image Display Plugin
+*Every image in this README is real plugin output, rendered at the true panel
+size and then scaled up so the pixels stay pixels. The coloured marks are plain
+samples standing in for your own images.*
-Display static images on your LED matrix with automatic scaling, aspect ratio preservation, and transparency support.
+---
-## Features
+## Table of Contents
-- **Multiple Format Support**: PNG, JPG, BMP, GIF, and more
-- **Automatic Scaling**: Fit images to your display dimensions
-- **Aspect Ratio Preservation**: Keep images looking correct
-- **Transparency Support**: Handle PNG alpha channels
-- **Configurable Background**: Set custom background colors
-- **High-Quality Scaling**: LANCZOS resampling for best quality
+1. [What's On Screen](#whats-on-screen)
+2. [Installation](#installation)
+3. [Quick Start](#quick-start)
+4. [Adding Images](#adding-images)
+5. [Scaling and Background](#scaling-and-background)
+ - [fit_to_display and preserve_aspect_ratio](#fit_to_display-and-preserve_aspect_ratio)
+ - [background_color](#background_color)
+6. [Rotating Through Several Images](#rotating-through-several-images)
+ - [rotation_mode](#rotation_mode)
+ - [The two rotation intervals](#the-two-rotation-intervals)
+7. [Per-Image Schedules](#per-image-schedules)
+8. [Panel Sizes](#panel-sizes)
+9. [Troubleshooting](#troubleshooting)
+10. [Development](#development)
+11. [Support](#support)
-## Configuration
+---
-### Single image
+## What's On Screen
+
+One image at a time, centred on the panel. With several images configured the
+plugin cycles through them; with one, it simply shows that one.
+
+Transparent areas of a PNG are filled with `background_color`, which defaults to
+black — so on an LED panel a transparent background means those pixels are
+simply off.
+
+---
+
+## Installation
+
+**From the Plugin Store (recommended).** Open the LEDMatrix web interface at
+`http://:5000`, go to **Plugin Manager**, find **Static Image
+Display** in the **Plugin Store** section, and click **Install**. Images are
+uploaded from the plugin's own tab.
+
+**Manually.** Copy this directory into your LEDMatrix `plugin-repos/` and
+restart the display service.
+
+`enabled` defaults to **`false`**, so nothing appears until you switch the
+plugin on.
+
+---
+
+## Quick Start
```json
{
- "enabled": true,
- "images": [
- { "id": "logo", "path": "assets/static_images/my_logo.png" }
- ],
- "fit_to_display": true,
- "preserve_aspect_ratio": true,
- "background_color": [0, 0, 0],
- "display_duration": 10
+ "static-image": {
+ "enabled": true,
+ "images": [
+ { "id": "logo", "path": "assets/plugins/static-image/logo.png" }
+ ]
+ }
}
```
-### Multiple images with rotation
+Several images on a rotation:
```json
{
- "enabled": true,
- "images": [
- { "id": "logo_a", "path": "assets/static_images/logo_a.png" },
- { "id": "logo_b", "path": "assets/static_images/logo_b.png" },
- { "id": "logo_c", "path": "assets/static_images/logo_c.png" }
- ],
- "image_config": {
- "mode": "multiple",
- "rotation_mode": "sequential"
- },
- "rotation_settings": {
- "sequential_loop": true
- },
- "image_rotation_interval": 15,
- "fit_to_display": true,
- "preserve_aspect_ratio": true,
- "background_color": [0, 0, 0],
- "display_duration": 30
+ "static-image": {
+ "enabled": true,
+ "image_config": { "mode": "multiple", "rotation_mode": "sequential" },
+ "image_rotation_interval": 15,
+ "images": [
+ { "id": "logo", "path": "assets/plugins/static-image/logo.png", "display_order": 1 },
+ { "id": "badge", "path": "assets/plugins/static-image/badge.png", "display_order": 2 },
+ { "id": "banner", "path": "assets/plugins/static-image/banner.png", "display_order": 3 }
+ ]
+ }
}
```
-### Configuration Options
+---
-The full schema lives in
-[`config_schema.json`](config_schema.json) — the web UI form is generated
-from it. Key options:
+## Adding Images
-| Key | Default | Notes |
-|---|---|---|
-| `enabled` | `false` | Master switch |
-| `images` | `[]` | Array of image entries (each an object with a `path` and an optional per-image `schedule`), managed via the file-upload widget. See **Per-Image Scheduling** below. |
-| `image_config.mode` | `"single"` | How images are presented: `"single"` or `"multiple"` |
-| `image_config.rotation_mode` | `"sequential"` | Order when showing multiple images: `"sequential"`, `"random"`, `"time_based"` (advance on a wall-clock interval), or `"date_based"` (one image per day) |
-| `rotation_settings.sequential_loop` | `true` | Loop back to the first image after the last |
-| `rotation_settings.random_seed` | `null` | Optional fixed seed for reproducible random order |
-| `rotation_settings.time_intervals.enabled` | `false` | Tie image changes to wall-clock intervals |
-| `rotation_settings.time_intervals.interval_seconds` | `3600` | Wall-clock interval when enabled |
-| `image_rotation_interval` | `15` | Seconds between images during rotation |
-| `fit_to_display` | `true` | Scale image to display dimensions |
-| `preserve_aspect_ratio` | `true` | Don't stretch when scaling |
-| `background_color` | `[0, 0, 0]` | RGB fill behind transparent pixels |
-| `display_duration` | `10` | Seconds the plugin holds the screen each rotation |
+The `images` array is normally filled by the upload widget in the web UI, which
+writes the file and the entry for you. Each entry takes:
-### Per-Image Scheduling
+| Key | Type | Default | What it does |
+|-----|------|---------|--------------|
+| `id` | string | *auto* | Unique identifier for the entry |
+| `path` | string | — | File path, relative to the project root |
+| `display_order` | integer | `0` | Order for sequential rotation; lower goes first |
+| `uploaded_at` | string | *auto* | Upload timestamp, set by the UI |
+| `schedule` | object | `null` | Optional time window — see [Per-Image Schedules](#per-image-schedules) |
-Each entry in `images` can carry an optional `schedule` so an image only shows at
-certain times (e.g. a "good morning" board, or business-hours signage):
+PNG, JPEG and GIF all load; PNG is the sensible choice for anything with sharp
+edges or transparency. A still frame is taken from an animated GIF — this
+plugin does not animate.
-| Schedule key | Default | Notes |
-|---|---|---|
-| `schedule.enabled` | `false` | Turn scheduling on for this image |
-| `schedule.mode` | `"always"` | `"always"` (any time), `"time_range"` (same window every day), or `"per_day"` (a different window per weekday) |
-| `schedule.start_time` / `schedule.end_time` | `"08:00"` / `"18:00"` | Window bounds (HH:MM, 24-hour) for `time_range` mode |
-| `schedule.days.` | — | Per-weekday `enabled` + `start_time`/`end_time`, used when `mode` is `"per_day"` |
+---
-When several images are eligible at the same time, they rotate per
-`image_config.rotation_mode`.
+## Scaling and Background
-## Usage
+| Option | Type | Default | What it does |
+|--------|------|---------|--------------|
+| `fit_to_display` | boolean | `true` | Scale the image to the panel |
+| `preserve_aspect_ratio` | boolean | `true` | Keep proportions while scaling |
+| `background_color` | array | `[0, 0, 0]` | Fill for transparent areas, `[R, G, B]` |
-### Basic Setup
+### `fit_to_display` and `preserve_aspect_ratio`
-1. Place your image in a directory (e.g., `assets/static_images/`)
-2. Configure the plugin with the image path
-3. Enable the plugin
-4. The image will display automatically during rotation
+
-### Image Guidelines
+- **`fit_to_display: true`** (the default) scales the image to the panel. With
+ it off, the image is drawn at its native pixel size — a 192×64 source on a
+ 128×32 panel shows only the middle of itself.
+- **`preserve_aspect_ratio: true`** (the default) keeps proportions, so a wide
+ image is letterboxed rather than squashed. Turn it off only when you want the
+ image to fill the panel exactly and do not mind the distortion.
-**Recommended:**
-- Use PNG format for best quality and transparency support
-- Size images close to your display resolution for best performance
-- For 64x32 displays: 64x32 or 128x64 images work well
-- Use transparency to blend with background
+The two work together: `preserve_aspect_ratio` has no effect when
+`fit_to_display` is off, because nothing is being scaled.
-**Supported Formats:**
-- PNG (recommended for transparency)
-- JPG/JPEG
-- BMP
-- GIF
-- TIFF
+**Design images at your panel's aspect ratio** where you can. A 128×32 panel is
+4:1, which is unusually wide — a square logo will only ever use a third of it.
-### Tips for Best Results
+Choosing a format:
-1. **For Logos**: Use PNG with transparent background
-2. **For Photos**: Use JPG for smaller file size
-3. **For Pixel Art**: Use PNG at native resolution
-4. **For Icons**: Scale to exact display size
+| Content | Use | Why |
+|---------|-----|-----|
+| Logo or icon | PNG with transparency | Sharp edges stay sharp, and the background stays unlit |
+| Photograph | JPEG | Much smaller for the same result; transparency is not needed |
+| Pixel art | PNG at native resolution | Sized to the panel exactly, no resampling to soften it |
-## Advanced Usage
+### `background_color`
-### Dynamic Image Updates
+Transparent pixels in a PNG are filled with this colour.
-Change images programmatically via the Web UI or API:
+
-```python
-# Via plugin manager
-plugin.set_image_path("assets/static_images/new_image.png")
-plugin.reload_image()
-```
+Black (the default) leaves those pixels unlit, which is what you almost always
+want on an LED matrix. A non-black background lights **every** pixel on the
+panel — noticeably brighter in a dark room and a real increase in power draw.
-### Multiple Images
+---
-Put all the images you want to cycle through into the `images` array (see
-the multi-image example above) and set `image_config.mode` to
-`"multiple"`.
+## Rotating Through Several Images
-## Troubleshooting
+| Option | Type | Default | What it does |
+|--------|------|---------|--------------|
+| `image_config.mode` | string | `single` | `single` shows one image; `multiple` rotates |
+| `image_config.rotation_mode` | string | `sequential` | How the next image is chosen |
+| `rotation_settings.sequential_loop` | boolean | `true` | Return to the first image after the last |
+| `rotation_settings.random_seed` | integer / null | `null` | Fixes the random order; `null` uses the clock |
+| `image_rotation_interval` | number | `15` | Seconds each image is shown |
+| `display_duration` | number | `10` | Seconds the plugin holds the panel per turn |
+
+### `rotation_mode`
+
+| Value | What it does |
+|-------|--------------|
+| `sequential` | In `display_order`, wrapping at the end when `sequential_loop` is on |
+| `random` | A random pick each time. Set `random_seed` for a repeatable order |
+| `time_based` | Advances on its own timer — **also needs `rotation_settings.time_intervals.enabled: true`** |
+| `date_based` | **Not implemented.** The code is a stub that always returns the first image |
-**Image not displaying:**
-- Check that image path is correct
-- Verify image file exists
-- Check file permissions
-- Review logs for error messages
+Two of those deserve emphasis:
-**Image looks distorted:**
-- Enable `preserve_aspect_ratio`
-- Check image dimensions vs display size
-- Verify image isn't corrupted
+- **`time_based` needs a second switch.** Setting `rotation_mode: "time_based"`
+ alone does nothing; `rotation_settings.time_intervals.enabled` must also be
+ `true`, and `interval_seconds` (default `3600`) sets the pace. With the switch
+ off, the image never advances.
+- **`date_based` does nothing at all.** It is a placeholder in the source —
+ selecting it pins the display to the first available image. It is listed here
+ because the setting is offered in the UI and silently doing nothing is worse
+ than being told.
-**Image appears cropped:**
-- Enable `fit_to_display`
-- Check image size matches display
+With `sequential_loop: false` the rotation stops on the last image rather than
+wrapping, which is what you want for a sequence with an ending.
-**Transparency not working:**
-- Use PNG format with alpha channel
-- Verify background_color is set correctly
+### The two rotation intervals
-## Examples
+There are two different "how often" settings and they are not alternatives:
+
+| Setting | Default | Applies |
+|---------|---------|---------|
+| `image_rotation_interval` | `15` | Always. Seconds each image is shown before the next |
+| `rotation_settings.time_intervals.interval_seconds` | `3600` | **Only** in `time_based` mode, and only with `time_intervals.enabled` on |
+
+`image_rotation_interval` is the one most people want. It falls back to
+`display_duration` if unset, so leaving both alone gives a 10-second dwell.
+
+---
+
+## Per-Image Schedules
+
+Each image can carry a `schedule` object restricting when it is eligible.
+Images outside their window are skipped by the rotation entirely.
-### Logo Display
```json
{
- "enabled": true,
- "images": [
- { "id": "company_logo", "path": "assets/static_images/company_logo.png" }
- ],
- "fit_to_display": true,
- "preserve_aspect_ratio": true,
- "background_color": [0, 0, 0]
+ "id": "open-sign",
+ "path": "assets/plugins/static-image/open.png",
+ "schedule": {
+ "enabled": true,
+ "mode": "time_range",
+ "start_time": "08:00",
+ "end_time": "18:00"
+ }
}
```
-### Pixel Art
-```json
-{
- "enabled": true,
- "images": [
- { "id": "pixel_art", "path": "assets/static_images/pixel_art.png" }
- ],
- "fit_to_display": false,
- "preserve_aspect_ratio": true,
- "background_color": [0, 0, 50]
-}
+| Key | Default | What it does |
+|-----|---------|--------------|
+| `enabled` | `false` | Turn scheduling on for this image. **With it off the schedule is ignored entirely** |
+| `mode` | `always` | `always`, `time_range` (same window every day), or `per_day` (a different window per weekday) |
+| `start_time` | `"08:00"` | Window opens, `HH:MM` 24-hour |
+| `end_time` | `"18:00"` | Window closes, `HH:MM` 24-hour |
+| `days` | `null` | Per-weekday windows, used only when `mode` is `per_day` |
+
+The common mistake is setting `mode` and the times but leaving `enabled` at
+`false`, which leaves the image always visible.
+
+If every image is scheduled out at once there is nothing eligible to draw, so
+keep at least one image unscheduled as a fallback.
+
+---
+
+## Panel Sizes
+
+
+
+With `preserve_aspect_ratio` on, a square image is capped by the panel's
+*height*, so a longer chain does not make it bigger — it just centres it in
+more black. Doubling the height to 128×64 doubles the mark. If you want an
+image to use a wide panel, give it a wide source.
+
+---
+
+## Troubleshooting
+
+**Nothing appears.**
+`enabled` defaults to `false`. After that, check the `images` array is not
+empty and that at least one entry's `path` resolves.
+
+**The image is missing but others show.**
+Check the log for a load warning. `path` is relative to the LEDMatrix project
+root, not to the plugin directory.
+
+**Only part of the image is visible.**
+`fit_to_display` is off, so the image is drawn at native size and cropped by
+the panel. Turn it on, or resize the source.
+
+**The image looks squashed.**
+`preserve_aspect_ratio` is off. Turn it on to letterbox instead of stretch.
+
+**It never rotates.**
+Check `image_config.mode` is `multiple` — `single` never advances. If
+`rotation_mode` is `time_based`, also check
+`rotation_settings.time_intervals.enabled` is `true`. If it is `date_based`,
+that mode is a stub and will not rotate at all.
+
+**A scheduled image shows all the time.**
+`schedule.enabled` defaults to `false`; the window is ignored until you set it.
+
+**The whole panel is lit and it is too bright.**
+`background_color` is not black. Transparent areas are being filled with it.
+
+---
+
+## Development
+
+### Project structure
+
+```text
+static-image/
+├── manifest.json # Plugin metadata and version history
+├── manager.py # StaticImagePlugin
+├── config_schema.json # Settings schema; source of truth for defaults
+├── requirements.txt
+├── test/ # Harness config and golden images
+└── README.md
```
-### Full Screen Photo
-```json
-{
- "enabled": true,
- "images": [
- { "id": "photo", "path": "assets/static_images/photo.jpg" }
- ],
- "fit_to_display": true,
- "preserve_aspect_ratio": false,
- "background_color": [0, 0, 0]
-}
+### Changing the image programmatically
+
+The plugin exposes two methods for driving it from code or another plugin
+rather than through configuration:
+
+```python
+plugin.set_image_path("assets/plugins/static-image/alert.png")
+plugin.reload_image()
```
-## License
+Both return a boolean for whether they succeeded. This bypasses the `images`
+array and the rotation, so it suits a one-off takeover — an alert, say — rather
+than a permanent change.
+
+### Regenerating the images in this README
+
+```bash
+python scripts/render_docs_assets.py --plugin static-image
+```
+
+`--check` verifies the committed images still match what the plugin renders.
+The sample marks live in `docs/assets/static-image/sample/`.
+
+Note that rotation cannot be shown in a still: every `rotation_mode` starts on
+the first eligible image, and the differences only appear across frames. The
+rotation behaviour above is documented from the source rather than from a
+screenshot.
+
+---
+
+## Support
-GPL-3.0 License - see main LEDMatrix repository for details.
+- YouTube:
+- Instagram:
+- Discord:
+- Sponsor: [GitHub Sponsors](https://github.com/sponsors/ChuckBuilds) ·
+ [Buy Me a Coffee](https://buymeacoffee.com/chuckbuilds) ·
+ [Ko-fi](https://ko-fi.com/chuckbuilds/)
+Released under the GNU General Public License v3.0 — see [LICENSE](LICENSE).
diff --git a/plugins/static-image/manifest.json b/plugins/static-image/manifest.json
index ccba68e6..85f75abf 100644
--- a/plugins/static-image/manifest.json
+++ b/plugins/static-image/manifest.json
@@ -1,7 +1,7 @@
{
"id": "static-image",
"name": "Static Image Display",
- "version": "1.0.5",
+ "version": "1.0.6",
"author": "ChuckBuilds",
"description": "Display static images on your LED matrix with automatic scaling, aspect ratio preservation, and transparency support. Perfect for logos, artwork, or custom graphics.",
"entry_point": "manager.py",
@@ -18,6 +18,12 @@
"static_image"
],
"versions": [
+ {
+ "released": "2026-09-02",
+ "version": "1.0.6",
+ "notes": "Documentation only, no behaviour change. Rewrites the README around real rendered screenshots and documents all 23 settings including the per-image schedule object. Records two things a user would otherwise discover by staring at an unchanging panel: rotation_mode 'date_based' is a stub in the source that always returns the first image, and 'time_based' does nothing unless rotation_settings.time_intervals.enabled is also turned on. Separates the two rotation intervals that look interchangeable -- image_rotation_interval always applies, while time_intervals.interval_seconds is only read in time_based mode -- and notes that schedule.enabled defaults to false, so a configured window is ignored until it is switched on.",
+ "ledmatrix_min": "2.0.0"
+ },
{
"released": "2026-07-17",
"version": "1.0.5",
@@ -46,7 +52,7 @@
"ledmatrix_min_version": "2.0.0"
}
],
- "last_updated": "2026-07-17",
+ "last_updated": "2026-09-02",
"stars": 0,
"downloads": 0,
"verified": true,