Skip to content

[Postponed] Resizer: capability-gated multi-frame resize for animated AVIF/WebP/GIF (blocked on backend sequence decoding) #61

Description

@parisek

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

  • Runtime capability probe per target format (Imagick + multi-image flag + libheif sequence support / cached round-trip self-test).
  • Animated source + capable backend → resized variants that retain animation (verified getNumberImages() > 1 on output).
  • Animated source + incapable backend → passthrough (original returned, never flattened).
  • Static sources unchanged.
  • Probe result memoised; no per-image backend spin-up beyond what fix(resizer)!: preserve animation for AVIF/WebP/GIF (passthrough on by default) #60 already does.
  • Tests: capable-backend (skip if unavailable in CI) animated round-trip; incapable-backend → passthrough; probe memoisation.

Related

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestpostponedDeferred — blocked on external/upstream prerequisites

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions