Skip to content

Animated images (GIF and WebP) and the toilet-roll frame model

Copy page

libvips does not use a separate 3D frame-array object for animations. Instead, when loading Animated GIF or Animated WebP files with n=-1 (NumPages: -1 or animated: true), libvips stacks every frame vertically along the Y axis into a single tall 2D strip — known in libvips documentation as a “toilet-roll” image.

1. Dimensions and metadata of a toilet-roll image

Section titled “1. Dimensions and metadata of a toilet-roll image”

Suppose an animated GIF has a single-frame canvas of 400 × 300 and contains 12 frames:

  • Default load (n=1): decodes only frame 0 (Width = 400, Height = 300).
  • All-frame load (n=-1): stacks all 12 frames vertically:
    • Width = 400
    • Height = 3600 (300 × 12)
    • PageHeight = 300 (stored in page-height metadata to record the height of a single frame)
    • Pages = 12 (total frame count in the source file)
    • PageDelay (per-frame duration array in milliseconds) and Loop (0 means infinite looping)
Operation or property govips (v2.20.0) sharp (0.35.5) pyvips (3.2.0) vips CLI (8.17.3)
Load all animation frames ImportParams.NumPages.Set(-1) sharp(input, { animated: true }) new_from_file("in.gif", n=-1) 'in.gif[n=-1]'
Read single-frame height image.PageHeight() meta.pageHeight image.get("page-height") vipsheader -f page-height
Read/write frame delays image.PageDelay() / SetPageDelay meta.delay / webp({ delay }) image.get("delay") —
Split and rejoin frames Built into multi-page-aware methods Automatic via pageHeight image.pagesplit() / Image.pagejoin(frames) —

2. Resizing and transcoding Animated GIF to Animated WebP

Section titled “2. Resizing and transcoding Animated GIF to Animated WebP”

Animated WebP supports 24-bit RGB plus an 8-bit alpha channel, typically reducing file size by 50%–80% compared to 256-colour GIFs while eliminating dithering grain.

In govips v2.20.0, ExtractArea, Resize/ResizeWithVScale, Embed, Zoom, Flip, and 90/180/270-degree Rotate preserve frame order and page-height on loaded multi-page strips. Version 2.20 fixes frame-geometry failures in Rotate, Flip, Zoom, and scaling to non-integer per-frame heights. This does not make Similarity, SmartCrop, or Gravity frame-safe. In pyvips 3.2.0, use pagesplit() and pagejoin() when an operation is not explicitly multi-page-aware:

package main
import (
"fmt"
"os"
"github.com/davidbyttow/govips/v2/vips"
)
func main() {
if err := vips.Startup(nil); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer vips.Shutdown()
// 1. Set NumPages = -1 to load all animation frames
importParams := vips.NewImportParams()
importParams.NumPages.Set(-1)
anim, err := vips.LoadImageFromFile("animated.gif", importParams)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer anim.Close()
fmt.Printf("Total strip: %dx%d, single frame PageHeight=%d, Pages=%d\n",
anim.Width(), anim.Height(), anim.PageHeight(), anim.Pages())
// 2. Resize by 50%: govips Resize is multi-page-aware and updates PageHeight
if err := anim.Resize(0.5, vips.KernelLanczos3); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// 3. Export as Animated WebP (preserving frame delays and loop count)
webpParams := vips.NewWebpExportParams()
webpParams.Quality = 80
webpParams.ReductionEffort = 4
data, _, err := anim.ExportWebp(webpParams)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if err := os.WriteFile("animated.webp", data, 0o644); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
  1. Calling raw crop on a multi-frame strip without updating page-height:
    • In pyvips or the vips CLI, calling image.crop(left, top, width, height) directly on an n=-1 strip slices only the top region of the strip and leaves page-height inconsistent (often failing on export with Image height is not a multiple of page-height). Use pagesplit() and pagejoin() in pyvips, or rely on govips v2.20.0’s multi-page-aware ExtractArea.
  2. Confusing total strip Height() with single-frame PageHeight():
    • A 500 × 500 animation with 100 frames has Height() == 50,000 when loaded with n=-1. Dimension guards that check Height() after loading will falsely reject normal animations, whereas forgetting to budget Width × PageHeight × Pages exposes your service to thousand-frame memory bombs (see Handling untrusted inputs).
  3. CPU cost of exporting back to GIF:
    • Exporting truecolour frames to .gif (ExportGIF / .gif() / gifsave) invokes palette quantization via cgif, which costs significantly more CPU than exporting Animated WebP.