Images¶
image show puts a PNG, a baseline JPEG or a GIF from /data in the middle of the
round panel, where it stays until something else takes the screen. A still has no task and no
timing behind it: it is drawn once and then simply left, exactly as a
colour is. An animated GIF plays — see Animated GIFs.
For clips, see Video clips.
Preparing a file¶
The panel is 240×240, and an image has to fit it — anything larger is refused rather than scaled or cropped, so the framing stays yours. Resize on the host:
sips -z 240 240 <input> --out r2.png # macOS, built in
ffmpeg -i <input> -vf scale=240:240 r2.png # anywhere
Smaller images are centred, with the rest of the panel black.
A JPEG has to be baseline and 4:2:0
The decoder reads baseline JPEG at 4:2:0 chroma subsampling. Two things it will not read, and what each reports:
| The file is | image show says |
|---|---|
| Progressive | only baseline JPEG is supported |
| 4:4:4 | unsupported colour sampling; re-encode as 4:2:0 |
| 4:2:2 | bad JPEG data: corrupt, or colour sampling the decoder cannot read |
This catches people out because ffmpeg picks 4:4:4 by default when the input is a still RGB image — unlike a clip, where the input is already 4:2:0. Ask for it explicitly:
ffmpeg -i <input> -c:v mjpeg -q:v 3 -pix_fmt yuvj420p r2.jpg
PNG is far more forgiving: palette, greyscale, grey+alpha, RGB and RGBA, at 1, 2, 4, 8 or 16 bits, interlaced or not, all decode. Unless you have a reason to send a JPEG, send a PNG.
Transparency is composited over black, which is what the panel wakes to — so a transparent background comes out the same as a black one, not white.
A file that was truncated in transfer is refused rather than half-drawn, with libpng's own reason:
image: /data/r2.png: Read Error.
Put the file on the board with fs_xfer.py put, then show it.
Showing¶
image show r2.png
image show leia.jpg
Each reports what it did:
image: showing /data/r2.png (240x240 PNG, 16-line blocks, 118.4 ms)
The image stays until a clip, a colour, the calibration pattern or another image replaces it.
screen reports it while it is up, and screen clear takes it away and puts the panel back to
sleep.
screen
showing /data/r2.png (240x240, backlight 100%)
Nothing that fails ever disturbs the screen. The file, its format and its size are all checked
before the panel is touched, so a rejected image show leaves whatever was already showing in
place.
Animated GIFs¶
A GIF with more than one frame plays, on its own frame delays, the same way a clip plays on its own timing:
image show r2-wave.gif
image: playing /data/r2-wave.gif (240x240 GIF, 24 frames, loops forever)
It loops the way the file says, which is how it looks in a browser: forever, a set number of
times, or once if the file does not say. When a GIF that stops runs out of passes, its last
frame stays up — it was shown as an image, and an image stays until something replaces it.
screen says playing while it moves and showing once it has stopped.
It plays on the same task as clips, so everything that works on a clip works on a GIF:
video stop stops it and puts the panel to sleep, video status reports its frame times, and
another image, clip or colour replaces it without the panel blinking. A one-frame GIF is a
still like any other.
Making one¶
From a clip or any video, at 240 pixels across and a frame rate the panel can keep up with:
ffmpeg -i <input> -vf "fps=15,scale=240:-1:flags=lanczos,split[a][b];[a]palettegen[p];[b][p]paletteuse" r2-wave.gif
The palette pair builds one 256-colour palette for the whole animation instead of a generic one; the difference is obvious. ffmpeg also writes each frame as only the rectangle that changed, which is what makes a GIF cheap to play here — see the table below.
Very short frame delays play at 100 ms
A frame with no delay, or a delay of 10 ms or less, stays up 100 ms. That is what Chrome and Firefox do, and files that ask for no delay were made expecting it — so a GIF runs at the speed it has in a browser, not faster.
What it costs¶
Measured on the board with video status. What matters is how much of the picture changes
from frame to frame, not how big the picture is: only the changed area is decoded and painted.
| GIF | Frame budget | Decode + draw per frame | Late |
|---|---|---|---|
| ffmpeg, 240×240, 20 fps (a test pattern with moving parts) | 50 ms | 5.9 ms | none |
| ffmpeg, 120×120, 20 fps | 50 ms | 1.9 ms | none |
| Every pixel changing, flat colours, 240×240 | — | 30.7 ms | — |
| Every pixel changing, random noise, 240×240, 25 fps | 40 ms | 44.9 ms | all |
The last row is the ceiling: a 240×240 GIF that repaints the whole panel with incompressible
content every frame manages about 22 fps. Real animations rarely come near it. If one
stutters, video status will show frames running late; drop its frame rate, or its size.
Looking at a file¶
image info r2.png
reports what the file contains without decoding it or touching the panel:
/data/r2.png: PNG 200x150, 8-bit RGBA, non-interlaced, 14233 bytes
parsed in 3.2 ms
For a GIF it counts the frames and adds up their delays, by the same rule it plays them:
/data/r2-wave.gif: GIF 240x240, 24 frames, loops forever, 1.60 s a pass, 412306 bytes
As with video info, it is the quickest way to find out whether a
file survived its upload intact. A GIF cut short in transfer is refused outright —
truncated: it ends before the GIF trailer — rather than played with its last frame half
garbage.
How it is decoded¶
PNG and JPEG end up in the same place: 16 lines at a time into one of two small DMA buffers, each block sent to the panel while the next is prepared. It is the mechanism the clip player uses, because a still has the same problem — a 240×240 frame is 115 KB, more than the largest free block of internal RAM.
Two things fall back to decoding the whole image first, and image show names which:
| Case | Mode reported |
|---|---|
| The usual one | 16-line blocks |
| A JPEG whose width or height is not a multiple of 8 | whole image |
| An interlaced PNG | whole image, in PSRAM |
Adam7 interlacing cannot be read row by row: each of its seven passes writes only its own pixels into rows the later passes fill in, so every row has to be resident at once. That is 173 KB, which goes to PSRAM. A still has no frame to miss, so the slower path costs nothing but the wait.
A GIF is different, because its frames are rarely whole pictures: most are a rectangle drawn
over the ones before. So it is composited onto a 240×240 canvas held in PSRAM, and then only
what changed is sent, in the same 16-line blocks. video status calls this
changed area only, from a PSRAM canvas.
What a still costs¶
Measured on this board, from the command to the last pixel on the panel — decode, convert and
paint together, which is what image show reports:
| File | Mode | Time |
|---|---|---|
| 240×240 PNG, RGB | 16-line blocks | 101 ms |
| 240×240 PNG, RGBA | 16-line blocks | 107 ms |
| 240×240 PNG, interlaced | whole image, in PSRAM | 175 ms |
| 120×120 PNG, RGB | 16-line blocks | 40 ms |
| 240×240 JPEG, 4:2:0 | 16-line blocks | 27 ms |
| 120×120 JPEG, 4:2:0 | 16-line blocks | 17 ms |
JPEG is three to four times quicker than PNG at the same size, which is the difference between a
decoder written for this chip and a general-purpose one doing zlib on the CPU. Neither matters
much for something shown once — but the very first image show after a restart takes about twice
as long as the table says, while libpng and zlib claim their buffers for the first time.
Why it is built this way is in Video internals.