fix(processing): downscale large images on low-RAM boards before encode - #3235
Conversation
The image-normalisation pipeline had no low-RAM pixel cap. The 1080p `_LOW_RAM_MAX_PIXELS` guard (`_exceeds_low_ram_pixel_cap`) was wired only into the video path; `_convert_image_to_webp` decoded, RGBA-converted, and lossless-WebP-encoded an image at full resolution regardless of board RAM. A many-MP photo (HEIC / AVIF / TIFF) drives peak RSS to ~770 MB — measured on x86 — while a Pi 2/3 normalisation container is capped around 540 MiB, so the encode exhausts RAM+swap. On a Pi 3B+ this OOM'd hard enough to reboot the board, and `method=6` lossless is minutes-slow on that CPU regardless. Downscale a > 1080p image to the pixel budget (aspect-preserving, `thumbnail`) before the RGBA convert + encode on low-RAM boards. A 1080p board can't display more detail, so this is lossless for signage. Unlike the video gate (which rejects over-cap uploads — a 4K clip can't be re-encoded on-device), an image downscales cleanly, so it stays a successful upload rather than a "Failed" pill. Validated on the Pi 3B+ testbed (container capped ~543 MiB): - Stock: a real 24 MP photo drove peak ~770 MB and rebooted the board. - Patched, real pipeline: the same 24 MP HEIC normalised to a 1175x1763 WebP, celery peaked ~195 MiB, board stayed up (restarts=0, OOMKilled=false); JPEG and HEIC micro-benchmarks under the container limit peaked 144 / 189 MiB in 27 / 38 s. Found while testing the EXIF-orientation fix (issue 3232) on constrained hardware; independent of that change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJ3ucEkn62cbgPoisAZ5LQ
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3235 +/- ##
=========================================
Coverage ? 89.44%
=========================================
Files ? 76
Lines ? 8343
Branches ? 892
=========================================
Hits ? 7462
Misses ? 666
Partials ? 215 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Self-review follow-up: max(1, ...) on the thumbnail target so a crafted sub-5px-tall image (still under the 50 MP bomb cap) can't floor a side to 0 and make thumbnail() raise. Add a test that an already-under-cap image is left untouched on a low-RAM board. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJ3ucEkn62cbgPoisAZ5LQ
|
Testbed validationReproduced the crash and verified the fix on a real Raspberry Pi 3B+ (1 GB; celery normalisation container capped ~543 MiB RAM + ~543 MiB swap), plus an x86 baseline. The crash (stock code)A real 24 MP photo through the normalisation path drove peak RSS to ~770 MB — well over the 543 MiB cap. On the Pi 3B+ this exhausted RAM+swap and rebooted the board ( The fix (patched) — real pipelineUploaded the same 24 MP HEIC through the actual
Micro-benchmarks under the exact container limit (
|
| Fixture (24 MP) | Result | Peak RSS | Time |
|---|---|---|---|
| JPEG, patched | downscaled 1762×1175 | 144 MiB | 27 s |
| HEIC, patched | downscaled 1175×1763 | 189 MiB | 38 s |
| (stock, either) | — | ~770 MB | crashed / rebooted |
The downscale also removes the other low-RAM pain point: method=6 lossless on a full 24 MP image was minutes-slow on the Pi 3 CPU; on the downscaled image it is seconds.
Testbed restored to pristine afterwards (patched processing.py reverted to the shipped image, test asset deleted, fixtures cleaned).



Downscales oversized images to the 1080p budget on low-RAM boards (Pi 2/3) before the memory-heavy WebP encode, so a large photo can no longer OOM/crash the normalisation worker.
Problem
The image-normalisation pipeline has no low-RAM pixel cap. The 1080p
_LOW_RAM_MAX_PIXELSguard (_exceeds_low_ram_pixel_cap) is wired only into the video path;_convert_image_to_webpdecodes, RGBA-converts, and lossless-WebP-encodes an image at full resolution regardless of board RAM (the only image guard is the 50 MP decompression-bomb cap).A many-MP photo (HEIC / AVIF / TIFF — e.g. an iPhone upload) drives peak RSS to ~770 MB (measured on x86), while a Pi 2/3 normalisation container is capped around 540 MiB. The encode exhausts RAM+swap — on a Pi 3B+ this OOMd hard enough to reboot the board — and
method=6lossless is minutes-slow on that CPU regardless.Fix
On low-RAM boards, downscale a
> 1080pimage to the pixel budget (aspect-preserving, viaImage.thumbnail) before the RGBA convert + encode. A 1080p board cant display more detail, so this is lossless for signage.thumbnailshrinks in place and, for JPEG, uses libjpeg draft decoding so even the decode of the oversized source stays cheap.Reuses the existing
_exceeds_low_ram_pixel_caphelper. Note the deliberate asymmetry with video: the video gate rejects an over-cap upload (a 4K clip cant be re-encoded on-device), whereas an image downscales cleanly and stays a successful upload.Validation
Unit tests (downscale on low-RAM, full-res on normal-RAM) plus the full
test_processing.pysuite (124 passed); ruff check + format clean.On the Pi 3B+ testbed (celery container capped ~543 MiB):
1175x1763WebP; celery peaked ~195 MiB, board stayed up (restarts=0,OOMKilled=false).Context
Found while testing the EXIF-orientation fix (issue 3232) on constrained hardware. This is an independent, pre-existing bug — the two changes touch adjacent lines in
_convert_image_to_webpbut are otherwise unrelated.🤖 Generated with Claude Code
https://claude.ai/code/session_01HJ3ucEkn62cbgPoisAZ5LQ