Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: Optimize frame buffer handling and WebP encoding #198

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))
}