Skip to content

Commit

Permalink
perf: Optimize frame buffer handling and WebP encoding
Browse files Browse the repository at this point in the history
  • Loading branch information
skidder committed Nov 6, 2024
1 parent 9ccf8e8 commit ea13adb
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 56 deletions.
33 changes: 18 additions & 15 deletions ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func (o *ImageOps) fit(d Decoder, inputCanvasWidth, inputCanvasHeight, outputCan
return false, err
}

// blend transparent pixels of the active frame with corresponding pixels of the previous canvas, creating a composite
// blend transparent pixels of the active frame with corresponding pixels of the previous canvas
if err := o.applyBlendMethod(d); err != nil {
return false, err
}
Expand All @@ -163,7 +163,7 @@ func (o *ImageOps) fit(d Decoder, inputCanvasWidth, inputCanvasHeight, outputCan
return true, nil
}

// If the image is not animated, we can fit it directly.
// If the image is not animated, we can fit it directly
if err := o.active().Fit(newWidth, newHeight, o.secondary()); err != nil {
return false, err
}
Expand All @@ -172,9 +172,9 @@ func (o *ImageOps) fit(d Decoder, inputCanvasWidth, inputCanvasHeight, outputCan
}

// resize resizes the active frame to the specified output canvas size.
func (o *ImageOps) resize(d Decoder, inputCanvasWidth, inputCanvasHeight, outputCanvasWidth, outputCanvasHeight, frameCount int, isAnimated, hasAlpha bool) (bool, error) {
func (o *ImageOps) resize(d Decoder, inputCanvasWidth, inputCanvasHeight, outputCanvasWidth, outputCanvasHeight int, isAnimated, hasAlpha bool) (bool, error) {
// If the image is animated, we need to resize the frame to the input canvas size
// and then copy the previous frame's data to the working buffer.
// and then copy the previous frame's data to the working buffer
if isAnimated {
if err := o.setupAnimatedFrameBuffers(d, inputCanvasWidth, inputCanvasHeight, hasAlpha); err != nil {
return false, err
Expand All @@ -200,7 +200,6 @@ func (o *ImageOps) resize(d Decoder, inputCanvasWidth, inputCanvasHeight, output
return false, err
}
o.copyFramePropertiesAndSwap()

return true, nil
}

Expand All @@ -212,10 +211,9 @@ func calculateExpectedSize(origWidth, origHeight, reqWidth, reqHeight int) (int,
} else if reqWidth > origWidth && reqHeight > origHeight && reqWidth != reqHeight {
// Both dimensions larger than original and not square
return origWidth, origHeight
} else {
// All other cases
return reqWidth, reqHeight
}
// All other cases
return reqWidth, reqHeight
}

func min(a, b int) int {
Expand Down Expand Up @@ -308,7 +306,7 @@ func (o *ImageOps) Transform(d Decoder, opt *ImageOptions, dst []byte) ([]byte,
// transform the frame, resizing if necessary
var swapped bool
if !emptyFrame {
swapped, err = o.transformCurrentFrame(d, opt, inputHeader, frameCount)
swapped, err = o.transformCurrentFrame(d, opt, inputHeader)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -355,7 +353,7 @@ func (o *ImageOps) Transform(d Decoder, opt *ImageOptions, dst []byte) ([]byte,
// transformCurrentFrame transforms the current frame using the decoder specified by d.
// It returns true if the frame was resized and false if it was not.
// It returns an error if the frame could not be resized.
func (o *ImageOps) transformCurrentFrame(d Decoder, opt *ImageOptions, inputHeader *ImageHeader, frameCount int) (bool, error) {
func (o *ImageOps) transformCurrentFrame(d Decoder, opt *ImageOptions, inputHeader *ImageHeader) (bool, error) {
if opt.ResizeMethod == ImageOpsNoResize && !inputHeader.IsAnimated() {
return false, nil
}
Expand All @@ -367,9 +365,11 @@ func (o *ImageOps) transformCurrentFrame(d Decoder, opt *ImageOptions, inputHead

switch opt.ResizeMethod {
case ImageOpsFit, ImageOpsNoResize:
return o.fit(d, inputHeader.Width(), inputHeader.Height(), outputWidth, outputHeight, inputHeader.IsAnimated(), inputHeader.HasAlpha())
return o.fit(d, inputHeader.Width(), inputHeader.Height(), outputWidth, outputHeight,
inputHeader.IsAnimated(), inputHeader.HasAlpha())
case ImageOpsResize:
return o.resize(d, inputHeader.Width(), inputHeader.Height(), outputWidth, outputHeight, frameCount, inputHeader.IsAnimated(), inputHeader.HasAlpha())
return o.resize(d, inputHeader.Width(), inputHeader.Height(), outputWidth, outputHeight,
inputHeader.IsAnimated(), inputHeader.HasAlpha())
default:
return false, fmt.Errorf("unknown resize method: %v", opt.ResizeMethod)
}
Expand All @@ -395,10 +395,13 @@ func (o *ImageOps) applyDisposeMethod(d Decoder) error {
active := o.active()
switch active.dispose {
case DisposeToBackgroundColor:
rect := image.Rect(active.xOffset, active.yOffset, active.xOffset+active.Width(), active.yOffset+active.Height())
return o.animatedCompositeBuffer.ClearToTransparent(rect)
if o.animatedCompositeBuffer != nil {
rect := image.Rect(active.xOffset, active.yOffset,
active.xOffset+active.Width(), active.yOffset+active.Height())
return o.animatedCompositeBuffer.ClearToTransparent(rect)
}
case NoDispose:
// Do nothing
return nil
}
return nil
}
Expand Down
95 changes: 54 additions & 41 deletions webp.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package lilliput
import "C"

import (
"fmt"
"io"
"time"
"unsafe"
Expand Down Expand Up @@ -76,31 +77,9 @@ func (d *webpDecoder) IsStreamable() bool {
return false
}

// hasReachedEndOfFrames checks if the decoder has reached the end of all frames.
func (d *webpDecoder) hasReachedEndOfFrames() bool {
return C.webp_decoder_has_more_frames(d.decoder) == 0
}

// advanceFrameIndex advances the internal frame index for the next decoding call.
func (d *webpDecoder) advanceFrameIndex() {
// Advance the frame index within the C++ decoder
C.webp_decoder_advance_frame(d.decoder)
}

func (d *webpDecoder) ICC() []byte {
iccDst := make([]byte, 8192)
iccLength := C.webp_decoder_get_icc(d.decoder, unsafe.Pointer(&iccDst[0]), C.size_t(cap(iccDst)))
return iccDst[:iccLength]
}

func (d *webpDecoder) BackgroundColor() uint32 {
return uint32(C.webp_decoder_get_bg_color(d.decoder))
}

func (d *webpDecoder) LoopCount() int {
return int(C.webp_decoder_get_loop_count(d.decoder))
}

// DecodeTo decodes the current frame into the provided Framebuffer.
// Returns io.EOF when there are no more frames to decode.
// Returns ErrDecodingFailed if decoding fails for any other reason.
func (d *webpDecoder) DecodeTo(f *Framebuffer) error {
if f == nil {
return io.EOF
Expand All @@ -109,35 +88,31 @@ func (d *webpDecoder) DecodeTo(f *Framebuffer) error {
// Get image header information
h, err := d.Header()
if err != nil {
return err
return fmt.Errorf("failed to get header: %w", err)
}

// Resize the framebuffer matrix to fit the image dimensions and pixel type
err = f.resizeMat(h.Width(), h.Height(), h.PixelType())
if err != nil {
return err
return fmt.Errorf("failed to resize framebuffer: %w", err)
}

// Decode the current frame into the framebuffer
ret := C.webp_decoder_decode(d.decoder, f.mat)
if !ret {
// Check if the decoder has reached the end of the frames
if !C.webp_decoder_decode(d.decoder, f.mat) {
if d.hasReachedEndOfFrames() {
return io.EOF
}
return ErrDecodingFailed
return fmt.Errorf("%w: frame decode failed", ErrDecodingFailed)
}

// Set the frame properties
// Set frame properties
f.duration = time.Duration(C.webp_decoder_get_prev_frame_delay(d.decoder)) * time.Millisecond
f.xOffset = int(C.webp_decoder_get_prev_frame_x_offset(d.decoder))
f.yOffset = int(C.webp_decoder_get_prev_frame_y_offset(d.decoder))
f.dispose = DisposeMethod(C.webp_decoder_get_prev_frame_dispose(d.decoder))
f.blend = BlendMethod(C.webp_decoder_get_prev_frame_blend(d.decoder))

// Advance to the next frame
d.advanceFrameIndex()

return nil
}

Expand Down Expand Up @@ -168,44 +143,82 @@ func newWebpEncoder(decodedBy Decoder, dstBuf []byte) (*webpEncoder, error) {
}, nil
}

// Encode encodes a frame into WebP format using the provided options.
// If f is nil, it finalizes the WebP animation and returns the encoded bytes.
// Returns io.EOF if encoding has already been finalized.
// Returns ErrInvalidImage if encoding fails.
func (e *webpEncoder) Encode(f *Framebuffer, opt map[int]int) ([]byte, error) {
if e.hasFlushed {
return nil, io.EOF
}

// Finalize animation if frame is nil
if f == nil {
// Finalize the WebP animation
length := C.webp_encoder_flush(e.encoder)
if length == 0 {
return nil, ErrInvalidImage
return nil, fmt.Errorf("%w: failed to flush encoder", ErrInvalidImage)
}

e.hasFlushed = true
return e.dstBuf[:length], nil
}

// Convert options to C array
var optList []C.int
var firstOpt *C.int
for k, v := range opt {
optList = append(optList, C.int(k))
optList = append(optList, C.int(v))
}
if len(optList) > 0 {
firstOpt = (*C.int)(unsafe.Pointer(&optList[0]))
firstOpt = &optList[0]
}

// Encode the current frame
frameDelay := int(f.duration.Milliseconds())
length := C.webp_encoder_write(e.encoder, f.mat, firstOpt, C.size_t(len(optList)), C.int(frameDelay), C.int(f.blend), C.int(f.dispose), 0, 0)
length := C.webp_encoder_write(
e.encoder,
f.mat,
firstOpt,
C.size_t(len(optList)),
C.int(frameDelay),
C.int(f.blend),
C.int(f.dispose),
C.int(f.xOffset),
C.int(f.yOffset),
)

if length == 0 {
return nil, ErrInvalidImage
return nil, fmt.Errorf("%w: failed to encode frame %d", ErrInvalidImage, e.frameIndex)
}

e.frameIndex++

return nil, nil
}

func (e *webpEncoder) Close() {
C.webp_encoder_release(e.encoder)
}

// hasReachedEndOfFrames checks if the decoder has reached the end of all frames.
func (d *webpDecoder) hasReachedEndOfFrames() bool {
return C.webp_decoder_has_more_frames(d.decoder) == 0
}

// advanceFrameIndex advances the internal frame index for the next decoding call.
func (d *webpDecoder) advanceFrameIndex() {
C.webp_decoder_advance_frame(d.decoder)
}

func (d *webpDecoder) ICC() []byte {
iccDst := make([]byte, 8192)
iccLength := C.webp_decoder_get_icc(d.decoder, unsafe.Pointer(&iccDst[0]), C.size_t(cap(iccDst)))
return iccDst[:iccLength]
}

func (d *webpDecoder) BackgroundColor() uint32 {
return uint32(C.webp_decoder_get_bg_color(d.decoder))
}

func (d *webpDecoder) LoopCount() int {
return int(C.webp_decoder_get_loop_count(d.decoder))
}

0 comments on commit ea13adb

Please sign in to comment.