Multipage and Animated Images

This page adapts the upstream libvips guide for ol.vips. The original reference is Multipage and animated images.

libvips represents animated and multipage images as one tall image plus metadata describing how that strip should be interpreted. In ol.vips, the main metadata fields are v/pages, v/page-height, v/page-delays, and v/loop-count.

This model works best when every page or frame has the same dimensions. That is common for animated GIF and WebP, and also for some TIFF or PDF inputs. If page sizes vary, read and process them one at a time instead of treating the file as one uniform strip.

Reading multipage images

By default, libvips usually reads only the first page or frame. Pass {:n -1} to load every page, or use {:page …​ :n …​} to load a single page or a range.

(require '[ol.vips :as v]
         '[ol.vips.operations :as ops])

(with-open [image (ops/gifload "test/fixtures/cogs.gif" {:n -1})]
  (select-keys (v/metadata image)
               [:width :height :pages :page-height :loop :delay]))
;; => {:width 85
;;     :height 385
;;     :pages 5
;;     :page-height 77
;;     :loop 32761
;;     :delay [0 50 50 50 50]}

Points to note:

  • ops/gifload with {:n -1} loads every frame in the animation.

  • :page-height is the height of one frame within the strip.

  • :pages * :page-height == :height for uniform-height images.

  • :loop is the loop count for animated formats.

  • :delay is a vector of frame delays in milliseconds.

You can also load a subset of frames:

(with-open [two-frames (ops/gifload "test/fixtures/cogs.gif" {:page 2 :n 2})]
  (select-keys (v/metadata two-frames) [:height :pages :page-height]))

Be careful interpreting metadata on partial loads. The loaded strip height reflects the selected range, but some loaders can still preserve source metadata such as the original total n-pages. For frame slicing logic, rely on the loaded image dimensions together with page-height, not only on the reported source page count.

The same loader options apply to other multipage-capable loaders such as ops/webpload and ops/tiffload, or to the generic v/from-file path when you want format autodetection.

For PDF input, remember that ol.vips blocks libvips operations marked as untrusted during initialization. If you have a trusted PDF input path and need to load it, opt in first with v/set-block-untrusted-operations! set to false.

Writing multipage images

If an image already has the right strip layout and metadata, writing an animated or multipage image is just a normal save. For example, you can load a GIF animation and write it back as animated WebP:

(with-open [image (ops/gifload "test/fixtures/cogs.gif" {:n -1})]
  (v/write-to-file image "cogs.webp"))

That works because the loaded image already carries the n-pages, page-height, loop, and delay metadata that the saver needs.

The same pattern applies to multipage TIFF output with ops/tiffsave or to generic saves through v/write-to-file when libvips can infer the target format from the filename.

Building an animation from frames

The upstream libvips examples build a vertical strip and then set metadata by hand. In ol.vips, prefer v/assemble-pages when you already have a sequence of equal-sized frames. It joins the frames into one strip and sets the page metadata for you.

(require '[ol.vips :as v]
         '[ol.vips.operations :as ops])

(with-open [base     (v/from-file "test/fixtures/puppies.jpg")
            frame-a  (ops/extract-area base 0 0 40 30)
            frame-b  (ops/extract-area base 10 10 40 30)
            frame-c  (ops/extract-area base 20 20 40 30)
            animated (v/assemble-pages [frame-a frame-b frame-c]
                                       {:loop  2
                                        :delay [80 120 160]})]
  (v/write-to-file animated "puppies.gif")
  (select-keys (v/metadata animated)
               [:width :height :pages :page-height :loop :delay]))

Here, frame-a, frame-b, and frame-c are just ordinary image handles. v/assemble-pages does not require a special frame type. The frames argument is simply a non-empty collection of same-sized images, with each image becoming one animation frame in the output.

In practice, those images can come from any image-producing operation, not only from ops/extract-area. You can resize, crop, composite, render text, or load separate files first, then pass the resulting image handles to v/assemble-pages.

If you already have a tall strip and need to annotate or adjust it, use v/assoc-pages, v/assoc-page-height, v/assoc-page-delays, and v/assoc-loop-count.

Applying operations page by page

For uniform-height multipage images, the page-aware helpers apply ordinary operations frame by frame and keep the metadata consistent.

(with-open [image   (ops/gifload "test/fixtures/cogs.gif" {:n -1})
            cropped (v/extract-area-pages image 10 7 50 50)
            turned  (v/rot-pages cropped :d90)
            looped  (v/assoc-loop-count turned 2)]
  (v/write-to-file looped "cogs-turned.gif")
  (select-keys (v/metadata looped)
               [:width :height :pages :page-height :loop :delay]))

The main helpers are:

For single-page inputs, these helpers fall back to the ordinary single-image behavior. For true multipage work, they assume that the image can be divided cleanly into equal-height pages.