Follow-up to #60.
Background
#60 fixes the regression where the resizer flattened animated AVIF / WebP / GIF to a single frame: it now detects an animated source and passes the original through untouched (timber_kit_resizer_skip_animated, default true). That's the correct, portable default — but the animated source is then served at full size, unresized.
This issue tracks the next step: actually resize/crop animated sources when — and only when — the active backend can re-encode them with animation preserved, falling back to passthrough otherwise.
The hard requirement: decide at RUNTIME, never assume
The original bug came from a static assumption (an allow-list that silently sent AVIF into a single-frame pipeline). We must not reintroduce that shape. The multi-frame path must be gated on a runtime capability probe, so on a backend that can't write animated output we fall back to passthrough instead of flattening.
Concretely, before attempting an animated re-encode, probe the active backend for the target format:
- Imagick present?
extension_loaded('imagick').
- Format multi-image read+write?
Imagick::queryFormats() + the format's multi-image (+) capability. (magick -list format shows e.g. AVIF rw+, WEBP* rw+, GIF* rw+ when the coder supports multiple images.)
- libheif new enough for AVIF/HEIC sequences? Gate AVIF/HEIC on a libheif version probe, or — most robust — a tiny round-trip self-test done once and cached: encode a 2-frame in-memory image to the target format with
writeImages(..., true), read it back, assert getNumberImages() > 1. If the round-trip fails, treat the backend as "cannot animate this format" and pass through.
- Cache the probe result per (format) per request/process so it isn't repeated per image.
Decision table per source:
| Source animated? |
Backend can re-encode animated <fmt>? |
Action |
| no |
— |
resize normally (current path) |
| yes |
yes |
multi-frame resize (this issue) |
| yes |
no |
passthrough (the #60 behaviour — never flatten) |
Implementation notes
- Spatie\Image cannot express multi-frame. The animated branch must be a dedicated raw Imagick path:
new Imagick($src) → coalesceImages() → per-frame cropImage() / scaleImage() → setImageFormat() / quality → writeImages($target, true) (note: writeImages, plural, with $adjoin = true). Reuse the existing crop-position / variant normalisation.
- Wire it into the existing gate. Today
timber_kit_resizer_skip_animated => false just reverts to the flattening pipeline (no multi-frame write exists), so the escape hatch is currently a no-op-toward-animation. This issue makes false meaningful: attempt the capability-gated animated resize; still fall back to passthrough when the probe says the backend can't.
- Scope: AVIF, WebP, GIF. GIF multi-frame write via Imagick is reliable; WebP via libwebp is reliable; AVIF via libheif is the fragile one — hence the round-trip self-test gate.
- Quality/size: animated re-encodes can be large; consider a per-format max-frames / max-dimension guard and
log()/filter to opt out per project.
Production reality (motivating data)
On the wearemullet production box (Cloudways): PHP 8.3.31, ImageMagick 7.1.2-8, libheif 1.20.2, Imagick loaded, AVIF/HEIC/WEBP/GIF all rw+, GD AVIF+WebP+GIF. So on capable servers like this, animated AVIF resize is genuinely achievable — the runtime probe lets those servers benefit while older/leaner backends keep the safe passthrough.
Acceptance criteria
Related
Follow-up to #60.
Background
#60 fixes the regression where the resizer flattened animated AVIF / WebP / GIF to a single frame: it now detects an animated source and passes the original through untouched (
timber_kit_resizer_skip_animated, defaulttrue). That's the correct, portable default — but the animated source is then served at full size, unresized.This issue tracks the next step: actually resize/crop animated sources when — and only when — the active backend can re-encode them with animation preserved, falling back to passthrough otherwise.
The hard requirement: decide at RUNTIME, never assume
The original bug came from a static assumption (an allow-list that silently sent AVIF into a single-frame pipeline). We must not reintroduce that shape. The multi-frame path must be gated on a runtime capability probe, so on a backend that can't write animated output we fall back to passthrough instead of flattening.
Concretely, before attempting an animated re-encode, probe the active backend for the target format:
extension_loaded('imagick').Imagick::queryFormats()+ the format's multi-image (+) capability. (magick -list formatshows e.g.AVIF rw+,WEBP* rw+,GIF* rw+when the coder supports multiple images.)writeImages(..., true), read it back, assertgetNumberImages() > 1. If the round-trip fails, treat the backend as "cannot animate this format" and pass through.Decision table per source:
<fmt>?Implementation notes
new Imagick($src)→coalesceImages()→ per-framecropImage()/scaleImage()→setImageFormat()/ quality →writeImages($target, true)(note:writeImages, plural, with$adjoin = true). Reuse the existing crop-position / variant normalisation.timber_kit_resizer_skip_animated => falsejust reverts to the flattening pipeline (no multi-frame write exists), so the escape hatch is currently a no-op-toward-animation. This issue makesfalsemeaningful: attempt the capability-gated animated resize; still fall back to passthrough when the probe says the backend can't.log()/filter to opt out per project.Production reality (motivating data)
On the
wearemulletproduction box (Cloudways): PHP 8.3.31, ImageMagick 7.1.2-8, libheif 1.20.2, Imagick loaded,AVIF/HEIC/WEBP/GIFallrw+, GD AVIF+WebP+GIF. So on capable servers like this, animated AVIF resize is genuinely achievable — the runtime probe lets those servers benefit while older/leaner backends keep the safe passthrough.Acceptance criteria
getNumberImages() > 1on output).Related