Uniform vs Positional Transforms on a Stitched Strip
Whether a transform can run on a whole libvips page-strip comes down to one question: does it look at coordinates? Uniform ops are safe, positional ops go frame by frame.
A GIF in libvips is one tall stacked strip (a page-strip): 5 frames of 100px make one 500px image. Whether a transform can run on that strip comes down to one question: does it care where a pixel is?
- Uniform means the same rule for every pixel, position independent, like resize/scale. Shrink the whole strip and every frame shrinks equally, boundaries stay aligned, and the animation survives. It never has to know frames exist.
- Positional means it acts on a specific coordinate or region, like crop and fill/pad. On a strip that coordinate points into the pile of frames, not “the same spot in each frame,” so a naive box slices across frame boundaries and produces garbage.
Fix for positional ops: load with [n=-1], split the strip by page-height, apply the operation identically to each frame, vips_arrayjoin(across=1), then reset page-height, n-pages, and delay.
Rule of thumb: scales or recolors everything the same → uniform → safe on the whole strip. Targets a coordinate or region → positional → go frame by frame. The service I worked on splits exactly this way: a bare resize runs on the whole strip, while crop and fill run per frame.
Not language-specific: the vips_* names are the libvips C API, but the geometry rule holds in any binding (govips, pyvips, sharp). It survives a rewrite off the host language.
Whether it looks at coordinates decides if you can run on the whole strip or must go frame by frame.
References:
- https://www.libvips.org/API/current/multipage-and-animated-images.html
- https://github.com/libvips/libvips/issues/2668
Related: This closes the loop from how libvips stores an animation as one tall stacked strip and one load path for stills and animation — or go back to the crop, resize, fill overview.