From f76ad210784b26f7b2cfcf506177c5d75556711b Mon Sep 17 00:00:00 2001 From: Hillay Koon Date: Wed, 9 Jul 2025 01:56:17 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(webp):=20=E6=B7=BB=E5=8A=A0=E5=8A=A8?= =?UTF-8?q?=E7=94=BBWebP=E8=A7=A3=E7=A0=81=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现动画WebP文件的解码功能,包括检查是否为动画、获取动画信息、解码第一帧和所有帧 --- .gitignore | 2 + anim.go | 304 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 306 insertions(+) create mode 100644 anim.go diff --git a/.gitignore b/.gitignore index d668d01..5f343c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /output.webp .idea .DS_Store +examples/ +testdata/ diff --git a/anim.go b/anim.go new file mode 100644 index 0000000..3be5987 --- /dev/null +++ b/anim.go @@ -0,0 +1,304 @@ +// Copyright 2014 . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build cgo +// +build cgo + +package webp + +/* +#cgo CFLAGS: -I./internal/libwebp-1.4.0/ +#cgo CFLAGS: -I./internal/libwebp-1.4.0/src/ +#cgo CFLAGS: -I./internal/include/ +#cgo CFLAGS: -Wno-pointer-sign -DWEBP_USE_THREAD +#cgo !windows LDFLAGS: -lm + +#include +#include +#include +#include +#include + +// 检查是否为动画WebP +int webpIsAnimated(const uint8_t* data, size_t data_size) { + WebPData webp_data = {data, data_size}; + WebPDemuxer* demux = WebPDemux(&webp_data); + if (!demux) return 0; + + uint32_t frame_count = WebPDemuxGetI(demux, WEBP_FF_FRAME_COUNT); + int is_animated = frame_count > 1; + + WebPDemuxDelete(demux); + return is_animated; +} + +// 获取动画信息 +int webpGetAnimInfo(const uint8_t* data, size_t data_size, + int* canvas_width, int* canvas_height, + int* frame_count, int* loop_count) { + WebPData webp_data = {data, data_size}; + WebPDemuxer* demux = WebPDemux(&webp_data); + if (!demux) return 0; + + *canvas_width = WebPDemuxGetI(demux, WEBP_FF_CANVAS_WIDTH); + *canvas_height = WebPDemuxGetI(demux, WEBP_FF_CANVAS_HEIGHT); + *frame_count = WebPDemuxGetI(demux, WEBP_FF_FRAME_COUNT); + *loop_count = WebPDemuxGetI(demux, WEBP_FF_LOOP_COUNT); + + WebPDemuxDelete(demux); + return 1; +} + +// 解码动画WebP的第一帧 +uint8_t* webpDecodeAnimFirstFrame(const uint8_t* data, size_t data_size, + int* width, int* height) { + WebPData webp_data = {data, data_size}; + WebPDemuxer* demux = WebPDemux(&webp_data); + if (!demux) return NULL; + + WebPIterator iter; + if (!WebPDemuxGetFrame(demux, 1, &iter)) { + WebPDemuxDelete(demux); + return NULL; + } + + // 解码第一帧 + uint8_t* rgba = WebPDecodeRGBA(iter.fragment.bytes, iter.fragment.size, width, height); + + WebPDemuxReleaseIterator(&iter); + WebPDemuxDelete(demux); + + return rgba; +} + +// 解码动画WebP的所有帧 +int webpDecodeAnimFrames(const uint8_t* data, size_t data_size, + uint8_t*** frames, int** timestamps, int** durations, + int* frame_count, int* width, int* height) { + WebPData webp_data = {data, data_size}; + WebPDemuxer* demux = WebPDemux(&webp_data); + if (!demux) return 0; + + *frame_count = WebPDemuxGetI(demux, WEBP_FF_FRAME_COUNT); + *width = WebPDemuxGetI(demux, WEBP_FF_CANVAS_WIDTH); + *height = WebPDemuxGetI(demux, WEBP_FF_CANVAS_HEIGHT); + + if (*frame_count <= 0) { + WebPDemuxDelete(demux); + return 0; + } + + // 分配内存 + *frames = (uint8_t**)malloc(*frame_count * sizeof(uint8_t*)); + *timestamps = (int*)malloc(*frame_count * sizeof(int)); + *durations = (int*)malloc(*frame_count * sizeof(int)); + + if (!*frames || !*timestamps || !*durations) { + if (*frames) free(*frames); + if (*timestamps) free(*timestamps); + if (*durations) free(*durations); + WebPDemuxDelete(demux); + return 0; + } + + WebPIterator iter; + int frame_idx = 0; + + if (WebPDemuxGetFrame(demux, 1, &iter)) { + do { + if (frame_idx >= *frame_count) break; + + int frame_width, frame_height; + uint8_t* rgba = WebPDecodeRGBA(iter.fragment.bytes, iter.fragment.size, + &frame_width, &frame_height); + + if (rgba) { + (*frames)[frame_idx] = rgba; + (*timestamps)[frame_idx] = frame_idx * 100; // 简单的时间戳计算 + (*durations)[frame_idx] = iter.duration; + frame_idx++; + } + } while (WebPDemuxNextFrame(&iter)); + + WebPDemuxReleaseIterator(&iter); + } + + WebPDemuxDelete(demux); + return frame_idx; // 返回实际解码的帧数 +} + +// 释放帧数据 +void webpFreeFrames(uint8_t** frames, int* timestamps, int* durations, int frame_count) { + if (frames) { + for (int i = 0; i < frame_count; i++) { + if (frames[i]) free(frames[i]); + } + free(frames); + } + if (timestamps) free(timestamps); + if (durations) free(durations); +} +*/ +import "C" +import ( + "errors" + "image" + "unsafe" +) + +// AnimInfo 包含动画WebP的基本信息 +type AnimInfo struct { + CanvasWidth int // 画布宽度 + CanvasHeight int // 画布高度 + FrameCount int // 帧数 + LoopCount int // 循环次数 +} + +// Frame 表示动画中的一帧 +type Frame struct { + Image *image.RGBA // 帧图像 + Timestamp int // 时间戳(毫秒) + Duration int // 持续时间(毫秒) +} + +// IsAnimated 检查WebP数据是否为动画 +func IsAnimated(data []byte) (bool, error) { + if len(data) == 0 { + return false, errors.New("IsAnimated: data is empty") + } + + result := C.webpIsAnimated((*C.uint8_t)(unsafe.Pointer(&data[0])), C.size_t(len(data))) + return result != 0, nil +} + +// GetAnimInfo 获取动画WebP的基本信息 +func GetAnimInfo(data []byte) (*AnimInfo, error) { + if len(data) == 0 { + return nil, errors.New("GetAnimInfo: data is empty") + } + + var canvasWidth, canvasHeight, frameCount, loopCount C.int + result := C.webpGetAnimInfo( + (*C.uint8_t)(unsafe.Pointer(&data[0])), C.size_t(len(data)), + &canvasWidth, &canvasHeight, &frameCount, &loopCount, + ) + + if result == 0 { + return nil, errors.New("GetAnimInfo: failed to get animation info") + } + + return &AnimInfo{ + CanvasWidth: int(canvasWidth), + CanvasHeight: int(canvasHeight), + FrameCount: int(frameCount), + LoopCount: int(loopCount), + }, nil +} + +// DecodeAnimFirstFrame 解码动画WebP的第一帧 +func DecodeAnimFirstFrame(data []byte) (*image.RGBA, error) { + if len(data) == 0 { + return nil, errors.New("DecodeAnimFirstFrame: data is empty") + } + + var width, height C.int + cptr := C.webpDecodeAnimFirstFrame( + (*C.uint8_t)(unsafe.Pointer(&data[0])), C.size_t(len(data)), + &width, &height, + ) + + if cptr == nil { + return nil, errors.New("DecodeAnimFirstFrame: failed to decode first frame") + } + defer C.free(unsafe.Pointer(cptr)) + + w, h := int(width), int(height) + pixelCount := w * h * 4 + pixData := make([]byte, pixelCount) + copy(pixData, (*[1 << 30]byte)(unsafe.Pointer(cptr))[:pixelCount:pixelCount]) + + return &image.RGBA{ + Pix: pixData, + Stride: w * 4, + Rect: image.Rect(0, 0, w, h), + }, nil +} + +// DecodeAnimFrames 解码动画WebP的所有帧 +func DecodeAnimFrames(data []byte) ([]*Frame, error) { + if len(data) == 0 { + return nil, errors.New("DecodeAnimFrames: data is empty") + } + + var frames **C.uint8_t + var timestamps, durations *C.int + var frameCount, width, height C.int + + result := C.webpDecodeAnimFrames( + (*C.uint8_t)(unsafe.Pointer(&data[0])), C.size_t(len(data)), + &frames, ×tamps, &durations, + &frameCount, &width, &height, + ) + + if result == 0 { + return nil, errors.New("DecodeAnimFrames: failed to decode frames") + } + + defer C.webpFreeFrames(frames, timestamps, durations, frameCount) + + w, h := int(width), int(height) + pixelCount := w * h * 4 + goFrames := make([]*Frame, int(result)) + + // 转换C数组到Go切片 + frameSlice := (*[1 << 20]*C.uint8_t)(unsafe.Pointer(frames))[:int(result):int(result)] + timestampSlice := (*[1 << 20]C.int)(unsafe.Pointer(timestamps))[:int(result):int(result)] + durationSlice := (*[1 << 20]C.int)(unsafe.Pointer(durations))[:int(result):int(result)] + + for i := 0; i < int(result); i++ { + if frameSlice[i] != nil { + pixData := make([]byte, pixelCount) + copy(pixData, (*[1 << 30]byte)(unsafe.Pointer(frameSlice[i]))[:pixelCount:pixelCount]) + + goFrames[i] = &Frame{ + Image: &image.RGBA{ + Pix: pixData, + Stride: w * 4, + Rect: image.Rect(0, 0, w, h), + }, + Timestamp: int(timestampSlice[i]), + Duration: int(durationSlice[i]), + } + } + } + + return goFrames, nil +} + +// ConvertAnimToStatic 将动画WebP转换为静态WebP(提取第一帧) +func ConvertAnimToStatic(data []byte, quality float32) ([]byte, error) { + // 首先检查是否为动画 + isAnim, err := IsAnimated(data) + if err != nil { + return nil, err + } + + if !isAnim { + // 如果不是动画,直接重新编码以应用质量设置 + img, err := DecodeRGBA(data) + if err != nil { + return nil, err + } + return EncodeRGBA(img, quality) + } + + // 解码第一帧 + firstFrame, err := DecodeAnimFirstFrame(data) + if err != nil { + return nil, err + } + + // 编码为静态WebP + return EncodeRGBA(firstFrame, quality) +} \ No newline at end of file From f72eb4c8c9abd42185f136ea2b0a3b8ffd6b22f0 Mon Sep 17 00:00:00 2001 From: Hillay Koon Date: Wed, 9 Jul 2025 10:54:24 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(webp):=20=E6=B7=BB=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E5=8A=A8=E7=94=BBWebP=E7=9A=84=E6=94=AF=E6=8C=81=E5=B9=B6?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E7=9B=B8=E5=85=B3=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将动画WebP相关功能从anim.go迁移到webp.go和capi.go 新增AnimInfo和Frame类型用于处理动画信息 实现动画检测、信息获取、帧解码等功能 --- anim.go | 304 -------------------------------------------------------- capi.go | 243 ++++++++++++++++++++++++++++++++++++++++++++ webp.go | 47 +++++++++ 3 files changed, 290 insertions(+), 304 deletions(-) delete mode 100644 anim.go diff --git a/anim.go b/anim.go deleted file mode 100644 index 3be5987..0000000 --- a/anim.go +++ /dev/null @@ -1,304 +0,0 @@ -// Copyright 2014 . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build cgo -// +build cgo - -package webp - -/* -#cgo CFLAGS: -I./internal/libwebp-1.4.0/ -#cgo CFLAGS: -I./internal/libwebp-1.4.0/src/ -#cgo CFLAGS: -I./internal/include/ -#cgo CFLAGS: -Wno-pointer-sign -DWEBP_USE_THREAD -#cgo !windows LDFLAGS: -lm - -#include -#include -#include -#include -#include - -// 检查是否为动画WebP -int webpIsAnimated(const uint8_t* data, size_t data_size) { - WebPData webp_data = {data, data_size}; - WebPDemuxer* demux = WebPDemux(&webp_data); - if (!demux) return 0; - - uint32_t frame_count = WebPDemuxGetI(demux, WEBP_FF_FRAME_COUNT); - int is_animated = frame_count > 1; - - WebPDemuxDelete(demux); - return is_animated; -} - -// 获取动画信息 -int webpGetAnimInfo(const uint8_t* data, size_t data_size, - int* canvas_width, int* canvas_height, - int* frame_count, int* loop_count) { - WebPData webp_data = {data, data_size}; - WebPDemuxer* demux = WebPDemux(&webp_data); - if (!demux) return 0; - - *canvas_width = WebPDemuxGetI(demux, WEBP_FF_CANVAS_WIDTH); - *canvas_height = WebPDemuxGetI(demux, WEBP_FF_CANVAS_HEIGHT); - *frame_count = WebPDemuxGetI(demux, WEBP_FF_FRAME_COUNT); - *loop_count = WebPDemuxGetI(demux, WEBP_FF_LOOP_COUNT); - - WebPDemuxDelete(demux); - return 1; -} - -// 解码动画WebP的第一帧 -uint8_t* webpDecodeAnimFirstFrame(const uint8_t* data, size_t data_size, - int* width, int* height) { - WebPData webp_data = {data, data_size}; - WebPDemuxer* demux = WebPDemux(&webp_data); - if (!demux) return NULL; - - WebPIterator iter; - if (!WebPDemuxGetFrame(demux, 1, &iter)) { - WebPDemuxDelete(demux); - return NULL; - } - - // 解码第一帧 - uint8_t* rgba = WebPDecodeRGBA(iter.fragment.bytes, iter.fragment.size, width, height); - - WebPDemuxReleaseIterator(&iter); - WebPDemuxDelete(demux); - - return rgba; -} - -// 解码动画WebP的所有帧 -int webpDecodeAnimFrames(const uint8_t* data, size_t data_size, - uint8_t*** frames, int** timestamps, int** durations, - int* frame_count, int* width, int* height) { - WebPData webp_data = {data, data_size}; - WebPDemuxer* demux = WebPDemux(&webp_data); - if (!demux) return 0; - - *frame_count = WebPDemuxGetI(demux, WEBP_FF_FRAME_COUNT); - *width = WebPDemuxGetI(demux, WEBP_FF_CANVAS_WIDTH); - *height = WebPDemuxGetI(demux, WEBP_FF_CANVAS_HEIGHT); - - if (*frame_count <= 0) { - WebPDemuxDelete(demux); - return 0; - } - - // 分配内存 - *frames = (uint8_t**)malloc(*frame_count * sizeof(uint8_t*)); - *timestamps = (int*)malloc(*frame_count * sizeof(int)); - *durations = (int*)malloc(*frame_count * sizeof(int)); - - if (!*frames || !*timestamps || !*durations) { - if (*frames) free(*frames); - if (*timestamps) free(*timestamps); - if (*durations) free(*durations); - WebPDemuxDelete(demux); - return 0; - } - - WebPIterator iter; - int frame_idx = 0; - - if (WebPDemuxGetFrame(demux, 1, &iter)) { - do { - if (frame_idx >= *frame_count) break; - - int frame_width, frame_height; - uint8_t* rgba = WebPDecodeRGBA(iter.fragment.bytes, iter.fragment.size, - &frame_width, &frame_height); - - if (rgba) { - (*frames)[frame_idx] = rgba; - (*timestamps)[frame_idx] = frame_idx * 100; // 简单的时间戳计算 - (*durations)[frame_idx] = iter.duration; - frame_idx++; - } - } while (WebPDemuxNextFrame(&iter)); - - WebPDemuxReleaseIterator(&iter); - } - - WebPDemuxDelete(demux); - return frame_idx; // 返回实际解码的帧数 -} - -// 释放帧数据 -void webpFreeFrames(uint8_t** frames, int* timestamps, int* durations, int frame_count) { - if (frames) { - for (int i = 0; i < frame_count; i++) { - if (frames[i]) free(frames[i]); - } - free(frames); - } - if (timestamps) free(timestamps); - if (durations) free(durations); -} -*/ -import "C" -import ( - "errors" - "image" - "unsafe" -) - -// AnimInfo 包含动画WebP的基本信息 -type AnimInfo struct { - CanvasWidth int // 画布宽度 - CanvasHeight int // 画布高度 - FrameCount int // 帧数 - LoopCount int // 循环次数 -} - -// Frame 表示动画中的一帧 -type Frame struct { - Image *image.RGBA // 帧图像 - Timestamp int // 时间戳(毫秒) - Duration int // 持续时间(毫秒) -} - -// IsAnimated 检查WebP数据是否为动画 -func IsAnimated(data []byte) (bool, error) { - if len(data) == 0 { - return false, errors.New("IsAnimated: data is empty") - } - - result := C.webpIsAnimated((*C.uint8_t)(unsafe.Pointer(&data[0])), C.size_t(len(data))) - return result != 0, nil -} - -// GetAnimInfo 获取动画WebP的基本信息 -func GetAnimInfo(data []byte) (*AnimInfo, error) { - if len(data) == 0 { - return nil, errors.New("GetAnimInfo: data is empty") - } - - var canvasWidth, canvasHeight, frameCount, loopCount C.int - result := C.webpGetAnimInfo( - (*C.uint8_t)(unsafe.Pointer(&data[0])), C.size_t(len(data)), - &canvasWidth, &canvasHeight, &frameCount, &loopCount, - ) - - if result == 0 { - return nil, errors.New("GetAnimInfo: failed to get animation info") - } - - return &AnimInfo{ - CanvasWidth: int(canvasWidth), - CanvasHeight: int(canvasHeight), - FrameCount: int(frameCount), - LoopCount: int(loopCount), - }, nil -} - -// DecodeAnimFirstFrame 解码动画WebP的第一帧 -func DecodeAnimFirstFrame(data []byte) (*image.RGBA, error) { - if len(data) == 0 { - return nil, errors.New("DecodeAnimFirstFrame: data is empty") - } - - var width, height C.int - cptr := C.webpDecodeAnimFirstFrame( - (*C.uint8_t)(unsafe.Pointer(&data[0])), C.size_t(len(data)), - &width, &height, - ) - - if cptr == nil { - return nil, errors.New("DecodeAnimFirstFrame: failed to decode first frame") - } - defer C.free(unsafe.Pointer(cptr)) - - w, h := int(width), int(height) - pixelCount := w * h * 4 - pixData := make([]byte, pixelCount) - copy(pixData, (*[1 << 30]byte)(unsafe.Pointer(cptr))[:pixelCount:pixelCount]) - - return &image.RGBA{ - Pix: pixData, - Stride: w * 4, - Rect: image.Rect(0, 0, w, h), - }, nil -} - -// DecodeAnimFrames 解码动画WebP的所有帧 -func DecodeAnimFrames(data []byte) ([]*Frame, error) { - if len(data) == 0 { - return nil, errors.New("DecodeAnimFrames: data is empty") - } - - var frames **C.uint8_t - var timestamps, durations *C.int - var frameCount, width, height C.int - - result := C.webpDecodeAnimFrames( - (*C.uint8_t)(unsafe.Pointer(&data[0])), C.size_t(len(data)), - &frames, ×tamps, &durations, - &frameCount, &width, &height, - ) - - if result == 0 { - return nil, errors.New("DecodeAnimFrames: failed to decode frames") - } - - defer C.webpFreeFrames(frames, timestamps, durations, frameCount) - - w, h := int(width), int(height) - pixelCount := w * h * 4 - goFrames := make([]*Frame, int(result)) - - // 转换C数组到Go切片 - frameSlice := (*[1 << 20]*C.uint8_t)(unsafe.Pointer(frames))[:int(result):int(result)] - timestampSlice := (*[1 << 20]C.int)(unsafe.Pointer(timestamps))[:int(result):int(result)] - durationSlice := (*[1 << 20]C.int)(unsafe.Pointer(durations))[:int(result):int(result)] - - for i := 0; i < int(result); i++ { - if frameSlice[i] != nil { - pixData := make([]byte, pixelCount) - copy(pixData, (*[1 << 30]byte)(unsafe.Pointer(frameSlice[i]))[:pixelCount:pixelCount]) - - goFrames[i] = &Frame{ - Image: &image.RGBA{ - Pix: pixData, - Stride: w * 4, - Rect: image.Rect(0, 0, w, h), - }, - Timestamp: int(timestampSlice[i]), - Duration: int(durationSlice[i]), - } - } - } - - return goFrames, nil -} - -// ConvertAnimToStatic 将动画WebP转换为静态WebP(提取第一帧) -func ConvertAnimToStatic(data []byte, quality float32) ([]byte, error) { - // 首先检查是否为动画 - isAnim, err := IsAnimated(data) - if err != nil { - return nil, err - } - - if !isAnim { - // 如果不是动画,直接重新编码以应用质量设置 - img, err := DecodeRGBA(data) - if err != nil { - return nil, err - } - return EncodeRGBA(img, quality) - } - - // 解码第一帧 - firstFrame, err := DecodeAnimFirstFrame(data) - if err != nil { - return nil, err - } - - // 编码为静态WebP - return EncodeRGBA(firstFrame, quality) -} \ No newline at end of file diff --git a/capi.go b/capi.go index d36162c..b2cd790 100644 --- a/capi.go +++ b/capi.go @@ -24,12 +24,140 @@ package webp #include "webp.h" #include +#include +#include +#include +#include +#include #include +#include +#include + +// 检查是否为动画WebP +int webpIsAnimated(const uint8_t* data, size_t data_size) { + WebPData webp_data = {data, data_size}; + WebPDemuxer* demux = WebPDemux(&webp_data); + if (!demux) return 0; + + uint32_t frame_count = WebPDemuxGetI(demux, WEBP_FF_FRAME_COUNT); + int is_animated = frame_count > 1; + + WebPDemuxDelete(demux); + return is_animated; +} + +// 获取动画信息 +int webpGetAnimInfo(const uint8_t* data, size_t data_size, + int* canvas_width, int* canvas_height, + int* frame_count, int* loop_count) { + WebPData webp_data = {data, data_size}; + WebPDemuxer* demux = WebPDemux(&webp_data); + if (!demux) return 0; + + *canvas_width = WebPDemuxGetI(demux, WEBP_FF_CANVAS_WIDTH); + *canvas_height = WebPDemuxGetI(demux, WEBP_FF_CANVAS_HEIGHT); + *frame_count = WebPDemuxGetI(demux, WEBP_FF_FRAME_COUNT); + *loop_count = WebPDemuxGetI(demux, WEBP_FF_LOOP_COUNT); + + WebPDemuxDelete(demux); + return 1; +} + +// 解码动画WebP的第一帧 +uint8_t* webpDecodeAnimFirstFrame(const uint8_t* data, size_t data_size, + int* width, int* height) { + WebPData webp_data = {data, data_size}; + WebPDemuxer* demux = WebPDemux(&webp_data); + if (!demux) return NULL; + + WebPIterator iter; + if (!WebPDemuxGetFrame(demux, 1, &iter)) { + WebPDemuxDelete(demux); + return NULL; + } + + // 解码第一帧 + uint8_t* rgba = WebPDecodeRGBA(iter.fragment.bytes, iter.fragment.size, width, height); + + WebPDemuxReleaseIterator(&iter); + WebPDemuxDelete(demux); + + return rgba; +} + +// 解码动画WebP的所有帧 +int webpDecodeAnimFrames(const uint8_t* data, size_t data_size, + uint8_t*** frames, int** timestamps, int** durations, + int* frame_count, int* width, int* height) { + WebPData webp_data = {data, data_size}; + WebPDemuxer* demux = WebPDemux(&webp_data); + if (!demux) return 0; + + *frame_count = WebPDemuxGetI(demux, WEBP_FF_FRAME_COUNT); + *width = WebPDemuxGetI(demux, WEBP_FF_CANVAS_WIDTH); + *height = WebPDemuxGetI(demux, WEBP_FF_CANVAS_HEIGHT); + + if (*frame_count <= 0) { + WebPDemuxDelete(demux); + return 0; + } + + // 分配内存 + *frames = (uint8_t**)malloc(*frame_count * sizeof(uint8_t*)); + *timestamps = (int*)malloc(*frame_count * sizeof(int)); + *durations = (int*)malloc(*frame_count * sizeof(int)); + + if (!*frames || !*timestamps || !*durations) { + if (*frames) free(*frames); + if (*timestamps) free(*timestamps); + if (*durations) free(*durations); + WebPDemuxDelete(demux); + return 0; + } + + WebPIterator iter; + int frame_idx = 0; + + if (WebPDemuxGetFrame(demux, 1, &iter)) { + do { + if (frame_idx >= *frame_count) break; + + int frame_width, frame_height; + uint8_t* rgba = WebPDecodeRGBA(iter.fragment.bytes, iter.fragment.size, + &frame_width, &frame_height); + + if (rgba) { + (*frames)[frame_idx] = rgba; + (*timestamps)[frame_idx] = frame_idx * 100; // 简单的时间戳计算 + (*durations)[frame_idx] = iter.duration; + frame_idx++; + } + } while (WebPDemuxNextFrame(&iter)); + + WebPDemuxReleaseIterator(&iter); + } + + WebPDemuxDelete(demux); + return frame_idx; // 返回实际解码的帧数 +} + +// 释放帧数据 +void webpFreeFrames(uint8_t** frames, int* timestamps, int* durations, int frame_count) { + if (frames) { + for (int i = 0; i < frame_count; i++) { + if (frames[i]) free(frames[i]); + } + free(frames); + } + if (timestamps) free(timestamps); + if (durations) free(durations); +} */ import "C" import ( "errors" + "image" "unsafe" ) @@ -537,3 +665,118 @@ func webpDelXMP(data []byte) (newData []byte, err error) { copy(newData, ((*[1 << 30]byte)(unsafe.Pointer(cptr)))[0:len(newData):len(newData)]) return } + +// 动画WebP相关函数 + +func webpIsAnimated(data []byte) bool { + if len(data) == 0 { + return false + } + + result := C.webpIsAnimated((*C.uint8_t)(unsafe.Pointer(&data[0])), C.size_t(len(data))) + return result != 0 +} + +func webpGetAnimInfo(data []byte) (*AnimInfo, error) { + if len(data) == 0 { + return nil, errors.New("webpGetAnimInfo: data is empty") + } + + var cCanvasWidth, cCanvasHeight, cFrameCount, cLoopCount C.int + result := C.webpGetAnimInfo( + (*C.uint8_t)(unsafe.Pointer(&data[0])), C.size_t(len(data)), + &cCanvasWidth, &cCanvasHeight, &cFrameCount, &cLoopCount, + ) + + if result == 0 { + return nil, errors.New("webpGetAnimInfo: failed to get animation info") + } + + return &AnimInfo{ + CanvasWidth: int(cCanvasWidth), + CanvasHeight: int(cCanvasHeight), + FrameCount: int(cFrameCount), + LoopCount: int(cLoopCount), + }, nil +} + +func webpDecodeAnimFirstFrame(data []byte) (*image.RGBA, error) { + if len(data) == 0 { + return nil, errors.New("webpDecodeAnimFirstFrame: data is empty") + } + + var cWidth, cHeight C.int + cptr := C.webpDecodeAnimFirstFrame( + (*C.uint8_t)(unsafe.Pointer(&data[0])), C.size_t(len(data)), + &cWidth, &cHeight, + ) + + if cptr == nil { + return nil, errors.New("webpDecodeAnimFirstFrame: failed to decode first frame") + } + defer C.free(unsafe.Pointer(cptr)) + + width := int(cWidth) + height := int(cHeight) + pix := make([]byte, width*height*4) + copy(pix, ((*[1 << 30]byte)(unsafe.Pointer(cptr)))[0:len(pix):len(pix)]) + + return &image.RGBA{ + Pix: pix, + Stride: 4 * width, + Rect: image.Rect(0, 0, width, height), + }, nil +} + +func webpDecodeAnimFrames(data []byte) ([]*Frame, error) { + if len(data) == 0 { + return nil, errors.New("webpDecodeAnimFrames: data is empty") + } + + var cFrames **C.uint8_t + var cTimestamps, cDurations *C.int + var cFrameCount, cWidth, cHeight C.int + + result := C.webpDecodeAnimFrames( + (*C.uint8_t)(unsafe.Pointer(&data[0])), C.size_t(len(data)), + &cFrames, &cTimestamps, &cDurations, + &cFrameCount, &cWidth, &cHeight, + ) + + if result == 0 { + return nil, errors.New("webpDecodeAnimFrames: failed to decode frames") + } + + defer C.webpFreeFrames(cFrames, cTimestamps, cDurations, cFrameCount) + + frameCount := int(cFrameCount) + width := int(cWidth) + height := int(cHeight) + pixelCount := width * height * 4 + + // 转换C数组到Go切片 + frames := make([]*Frame, frameCount) + + cFramesSlice := (*[1 << 20]*C.uint8_t)(unsafe.Pointer(cFrames))[:frameCount:frameCount] + cTimestampsSlice := (*[1 << 20]C.int)(unsafe.Pointer(cTimestamps))[:frameCount:frameCount] + cDurationsSlice := (*[1 << 20]C.int)(unsafe.Pointer(cDurations))[:frameCount:frameCount] + + for i := 0; i < frameCount; i++ { + if cFramesSlice[i] != nil { + pix := make([]byte, pixelCount) + copy(pix, ((*[1 << 30]byte)(unsafe.Pointer(cFramesSlice[i])))[0:pixelCount:pixelCount]) + + frames[i] = &Frame{ + Image: &image.RGBA{ + Pix: pix, + Stride: 4 * width, + Rect: image.Rect(0, 0, width, height), + }, + Timestamp: int(cTimestampsSlice[i]), + Duration: int(cDurationsSlice[i]), + } + } + } + + return frames, nil +} diff --git a/webp.go b/webp.go index 1205695..9dc8df1 100644 --- a/webp.go +++ b/webp.go @@ -161,3 +161,50 @@ func GetMetadata(data []byte, format string) (metadata []byte, err error) { func SetMetadata(data, metadata []byte, format string) (newData []byte, err error) { return webpSetMetadata(data, metadata, format) } + +// AnimInfo contains animation information +type AnimInfo struct { + CanvasWidth int + CanvasHeight int + FrameCount int + LoopCount int +} + +// Frame represents a single frame in an animation +type Frame struct { + Image *image.RGBA + Timestamp int + Duration int +} + +// IsAnimated checks if the WebP data contains an animation +func IsAnimated(data []byte) bool { + return webpIsAnimated(data) +} + +// GetAnimInfo returns animation information +func GetAnimInfo(data []byte) (*AnimInfo, error) { + return webpGetAnimInfo(data) +} + +// DecodeAnimFirstFrame decodes the first frame of an animated WebP +func DecodeAnimFirstFrame(data []byte) (*image.RGBA, error) { + return webpDecodeAnimFirstFrame(data) +} + +// DecodeAnimFrames decodes all frames of an animated WebP +func DecodeAnimFrames(data []byte) ([]*Frame, error) { + return webpDecodeAnimFrames(data) +} + +// ConvertAnimToStatic converts an animated WebP to a static WebP (first frame) +func ConvertAnimToStatic(data []byte) ([]byte, error) { + // 解码第一帧 + firstFrame, err := DecodeAnimFirstFrame(data) + if err != nil { + return nil, err + } + + // 编码为静态 WebP + return EncodeRGBA(firstFrame, 80.0) +}