跳转到内容

动画图像(Animated GIF 与 WebP)与多帧垂直条带模型

复制本页

libvips 没有专门的三维“视频帧数组”结构。它在处理 Animated GIF 或 Animated WebP 时,采用了一种极具特色的设计——垂直长卷模型(Toilet-roll image):当你在加载时指定 n=-1(或 NumPages: -1 / animated: true),libvips 会把所有动画帧沿垂直方向(Y 轴)拼接成一张超高的二维图像。

1. 理解垂直长卷(Toilet-roll)的尺寸与元数据

Section titled “1. 理解垂直长卷(Toilet-roll)的尺寸与元数据”

假设一个 GIF 动图的单帧分辨率为 400 × 300,共包含 12 帧:

  • 默认加载(n=1):只解码第 1 帧,Width = 400,Height = 300。
  • 全帧加载(n=-1):全部 12 帧沿 Y 轴拼接:
    • Width = 400
    • Height = 3600(即 300 × 12)
    • PageHeight = 300(存储在图像元数据 page-height 中,标识单帧高度)
    • Pages = 12(原文件的总帧数)
    • PageDelay(每帧停留时长数组,单位毫秒 ms)与 Loop(循环次数,0 表示无限循环)
操作或属性 govips (v2.20.0) sharp (0.35.5) pyvips (3.2.0) vips CLI (8.17.3)
加载全部动画帧 ImportParams.NumPages.Set(-1) sharp(input, { animated: true }) new_from_file("in.gif", n=-1) 'in.gif[n=-1]'
获取单帧高度 image.PageHeight() meta.pageHeight image.get("page-height") vipsheader -f page-height
获取/设置帧延迟 image.PageDelay() / SetPageDelay meta.delay / webp({ delay }) image.get("delay") —
拆分为帧列表与重组 多页感知方法(自动拆合) 自动按 pageHeight 处理 image.pagesplit() / Image.pagejoin(frames) —

2. 将 Animated GIF 缩放并转换为 Animated WebP

Section titled “2. 将 Animated GIF 缩放并转换为 Animated WebP”

Animated WebP 支持 24-bit RGB + 8-bit 半透明 Alpha,通常比 256 色的 GIF 体积小 50% ~ 80% 且没有颜色抖动噪点。

在 govips v2.20.0 中,ExtractArea、Resize / ResizeWithVScale、Embed、Zoom、Flip 与 90/180/270 度 Rotate 会保持多页长卷的帧序和 page-height;2.20 修复了 Rotate、Flip、Zoom 与非整数单帧缩放可能破坏帧几何的问题。Similarity、SmartCrop 与 Gravity 仍不能据此视为逐帧安全。在 pyvips 中,可以使用 pagesplit() 与 pagejoin() 显式逐帧处理:

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. 设置 NumPages = -1 加载全部动画帧
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. 缩放到 50%:govips 的 Resize 会自动感知多页并同步更新 PageHeight
if err := anim.Resize(0.5, vips.KernelLanczos3); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// 3. 导出为 Animated WebP(保留原有的帧延迟与循环次数)
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)
}
}

3. 动图处理的三大高频事故与防御

Section titled “3. 动图处理的三大高频事故与防御”
  1. 直接对整条长卷调底层 crop 或 resize 导致帧错位:
    • 在 pyvips 或 vips CLI 中,如果你对 n=-1 的图像直接调用 image.crop(left, top, width, height),它只会从第 1 帧切出一块并丢失其余帧(或者忘记更新 page-height 导致导出时报错 Image height is not a multiple of page-height)。在 pyvips 中请使用 pagesplit() 逐帧操作后再 pagejoin();在 govips v2.20.0 中,ExtractArea 已内置逐页裁剪支持。
  2. 用总高度 Height() 判断单帧尺寸超限:
    • 一个 500 × 500、共 100 帧的动图,加载后 Height() 高达 50,000 像素!如果你的网关在加载后用 Height() > 10000 拦截大图,所有稍长的 GIF 都会被误杀;反之,如果不限制总像素数(Width × PageHeight × Pages),恶意构造的千帧 GIF 会瞬间耗尽内存(详见不可信输入防护)。
  3. GIF 导出颜色量化开销:
    • 将真彩色图像或 WebP 动图导出回 .gif(ExportGIF / .gif() / gifsave)需要调用 cgif 与调色板量化,CPU 与内存开销显著高于导出 Animated WebP。