ruby-vips
| Language | Ruby (MRI 2.x/3.x, JRuby) |
|---|---|
| License | MIT (libvips: LGPL-2.1-or-later) |
| Repository | github.com/libvips/ruby-vips |
| Documentation | rubydoc.info/gems/ruby-vips, libvips gifsave |
| Depends on | libvips 8.12+ (GIF saving uses the bundled cgif and libimagequant or quantizr) |
| Read / write | yes / yes; animation, delays, loop count, transparency, palette size, dithering |
libvips represents an animated image as a single tall image: frames are stacked top to bottom, each page-height pixels high. Metadata fields hold n-pages, delay (an array of milliseconds per frame) and loop (0 = forever). Because all frames are one image, any operation applies to the whole animation, and thumbnail knows to resize each page.
GIF loading composites frames onto full pages. Saving quantizes with libimagequant (or the compatible quantizr), reuses palettes between similar frames, and makes unchanged pixels transparent to shrink the file. It’s much faster than ImageMagick for resizing animated GIFs.
Installation#
Shell
# Debian / Ubuntu
sudo apt install libvips42
# macOS
brew install vips
gem install ruby-vips
Basic usage: load an animated GIF and read its metadata#
Ruby
require "vips"
# n: -1 loads all frames (by default only the first is loaded)
image = Vips::Image.new_from_file("anim.gif", n: -1)
page_height = image.get("page-height")
frames = image.height / page_height
delays = image.get_typeof("delay") != 0 ? image.get("delay") : []
loop_count = image.get_typeof("loop") != 0 ? image.get("loop") : 0
puts "#{image.width} x #{page_height}, #{frames} frames, loop #{loop_count}"
puts "delays (ms): #{delays.inspect}"
How to resize an animated GIF#
Ruby
# thumbnail resizes every frame; "[n=-1]" loads all frames
small = Vips::Image.thumbnail("anim.gif[n=-1]", 200)
small.gifsave("small.gif")
How to extract one frame as PNG#
Ruby
image = Vips::Image.new_from_file("anim.gif", n: -1)
h = image.get("page-height")
image.crop(0, 3 * h, image.width, h).write_to_file("frame3.png") # 4th frame
# or load only that frame
Vips::Image.new_from_file("anim.gif", page: 3).write_to_file("frame3.png")
How to create an animated GIF from images#
Join the frames vertically and set the animation fields:
Ruby
require "vips"
frames = %w[f1.png f2.png f3.png].map { |f| Vips::Image.new_from_file(f) }
anim = Vips::Image.arrayjoin(frames, across: 1)
anim = anim.mutate do |m|
m.set_type! GObject::GINT_TYPE, "page-height", frames.first.height
m.set_type! Vips::ARRAY_INT_TYPE, "delay", [200] * frames.size # ms per frame
m.set_type! GObject::GINT_TYPE, "loop", 0 # forever
end
anim.gifsave("anim.gif")
All frames must have the same width and height.
How to make a GIF smaller#
Ruby
image = Vips::Image.new_from_file("anim.gif", n: -1)
image.gifsave("smaller.gif",
bitdepth: 6, # up to 63 colors (one entry is reserved for transparency)
dither: 0.5, # 0 = none, 1 = full Floyd-Steinberg
effort: 10, # slower, better palette
interframe_maxerror: 8) # treat near-identical pixels as unchanged
Tips#
- Animated output works only with formats that support pages (GIF, WebP, AVIF/HEIF):
image.write_to_file("anim.webp")converts a GIF to animated WebP and keeps the delays. - Set
access: :sequentialwhen loading big animations you process once top to bottom. reuse: truekeeps the input palette instead of computing a new one, useful when you only change metadata.