diff --git a/lilliput.go b/lilliput.go index 8f3bfa7f..a7835d68 100644 --- a/lilliput.go +++ b/lilliput.go @@ -10,12 +10,13 @@ import ( ) var ( - ErrInvalidImage = errors.New("unrecognized image format") - ErrDecodingFailed = errors.New("failed to decode image") - ErrBufTooSmall = errors.New("buffer too small to hold image") - ErrFrameBufNoPixels = errors.New("Framebuffer contains no pixels") - ErrSkipNotSupported = errors.New("skip operation not supported by this decoder") - ErrEncodeTimeout = errors.New("encode timed out") + ErrInvalidImage = errors.New("unrecognized image format") + ErrDecodingFailed = errors.New("failed to decode image") + ErrBufTooSmall = errors.New("buffer too small to hold image") + ErrFrameBufNoPixels = errors.New("Framebuffer contains no pixels") + ErrSkipNotSupported = errors.New("skip operation not supported by this decoder") + ErrEncodeTimeout = errors.New("encode timed out") + ErrInvalidCropCoordinates = errors.New("invalid crop coordinates") gif87Magic = []byte("GIF87a") gif89Magic = []byte("GIF89a") diff --git a/opencv.cpp b/opencv.cpp index 04d6179f..375ebe74 100644 --- a/opencv.cpp +++ b/opencv.cpp @@ -312,4 +312,10 @@ int opencv_decoder_get_png_icc(void* src, size_t src_len, void* dest, size_t des png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); return 0; +} + +void opencv_mat_copy_to(const opencv_mat src, opencv_mat dst) { + auto srcMat = static_cast(src); + auto dstMat = static_cast(dst); + srcMat->copyTo(*dstMat); } \ No newline at end of file diff --git a/opencv.go b/opencv.go index 84e4f8d4..219b95ef 100644 --- a/opencv.go +++ b/opencv.go @@ -277,6 +277,26 @@ func (f *Framebuffer) Fit(width, height int, dst *Framebuffer) error { return nil } +// CropTo performs a cropping transform on the Framebuffer +func (f *Framebuffer) CropTo(x, y, width, height int, dst *Framebuffer) error { + if f.mat == nil { + return ErrFrameBufNoPixels + } + + croppedMat := C.opencv_mat_crop(f.mat, C.int(x), C.int(y), C.int(width), C.int(height)) + if croppedMat == nil { + return ErrInvalidCropCoordinates + } + defer C.opencv_mat_release(croppedMat) + + err := dst.resizeMat(width, height, f.pixelType) + if err != nil { + return err + } + C.opencv_mat_copy_to(croppedMat, dst.mat) + return nil +} + // Width returns the width of the contained pixel data in number of pixels. This may // differ from the capacity of the framebuffer. func (f *Framebuffer) Width() int { diff --git a/opencv.hpp b/opencv.hpp index 75e69332..26e88eb4 100644 --- a/opencv.hpp +++ b/opencv.hpp @@ -69,6 +69,7 @@ void opencv_encoder_release(opencv_encoder e); bool opencv_encoder_write(opencv_encoder e, const opencv_mat src, const int* opt, size_t opt_len); int opencv_decoder_get_jpeg_icc(void* src, size_t src_len, void* dest, size_t dest_len); int opencv_decoder_get_png_icc(void* src, size_t src_len, void* dest, size_t dest_len); +void opencv_mat_copy_to(const opencv_mat src, opencv_mat dst); #ifdef __cplusplus } diff --git a/opencv_test.go b/opencv_test.go index d18b7f09..d9a2a208 100644 --- a/opencv_test.go +++ b/opencv_test.go @@ -288,3 +288,61 @@ func TestICC(t *testing.T) { }) } } + +func TestCropTo(t *testing.T) { + // Read a test image + imgData, err := ioutil.ReadFile("testdata/ferry_sunset.jpg") + if err != nil { + t.Fatalf("Failed to read image file: %v", err) + } + + // Create a new decoder for the image + decoder, err := newOpenCVDecoder(imgData) + if err != nil { + t.Fatalf("Failed to create decoder: %v", err) + } + defer decoder.Close() + + // Get the image header + header, err := decoder.Header() + if err != nil { + t.Fatalf("Failed to get the header: %v", err) + } + + // Create a new framebuffer and decode the image into it + framebuffer := NewFramebuffer(header.width, header.height) + if err = decoder.DecodeTo(framebuffer); err != nil { + t.Fatalf("DecodeTo failed unexpectedly: %v", err) + } + + // Define the crop rectangle + cropX, cropY, cropWidth, cropHeight := 10, 10, 100, 100 + + // Perform the crop operation + err = framebuffer.CropTo(cropX, cropY, cropWidth, cropHeight) + if err != nil { + t.Fatalf("CropTo failed unexpectedly: %v", err) + } + + // Check the dimensions of the cropped image + if framebuffer.Width() != cropWidth || framebuffer.Height() != cropHeight { + t.Fatalf("Cropped dimensions are incorrect, got width = %d, height = %d, expected width = %d, height = %d", + framebuffer.Width(), framebuffer.Height(), cropWidth, cropHeight) + } + + // Optional: Encode the cropped image to verify the integrity + dstBuf := make([]byte, 1024*1024) + encoder, err := newOpenCVEncoder(".jpg", decoder, dstBuf) + if err != nil { + t.Fatalf("Failed to create encoder: %v", err) + } + defer encoder.Close() + + encodedData, err := encoder.Encode(framebuffer, nil) + if err != nil { + t.Fatalf("Encode failed unexpectedly: %v", err) + } + if len(encodedData) == 0 { + t.Fatalf("Encoded data is empty, but it should not be") + } +} diff --git a/ops.go b/ops.go index 6a929fab..db280911 100644 --- a/ops.go +++ b/ops.go @@ -46,6 +46,17 @@ type ImageOptions struct { // This is a best effort timeout when encoding multiple frames EncodeTimeout time.Duration + + // CropCoordinates specify the rectangle of the original image to be cropped before resizing + CropCoordinates *CropCoordinates +} + +// CropCoordinates specifies a rectangle for cropping +type CropCoordinates struct { + X int + Y int + Width int + Height int } // ImageOps is a reusable object that can resize and encode images. @@ -119,6 +130,17 @@ func (o *ImageOps) resize(d Decoder, width, height int) (bool, error) { return true, nil } +func (o *ImageOps) crop(d Decoder, x, y, width, height int) (bool, error) { + active := o.active() + secondary := o.secondary() + err := active.CropTo(x, y, width, height, secondary) + if err != nil { + return false, err + } + o.swap() + return true, nil +} + func (o *ImageOps) normalizeOrientation(orientation ImageOrientation) { active := o.active() active.OrientationTransform(orientation) @@ -188,6 +210,18 @@ func (o *ImageOps) Transform(d Decoder, opt *ImageOptions, dst []byte) ([]byte, o.normalizeOrientation(h.Orientation()) + var swappedCrop bool + if opt.CropCoordinates != nil { + if opt.CropCoordinates.X+opt.CropCoordinates.Width > h.Width() || opt.CropCoordinates.Y+opt.CropCoordinates.Height > h.Height() { + return nil, ErrInvalidCropCoordinates + } + + swappedCrop, err = o.crop(d, opt.CropCoordinates.X, opt.CropCoordinates.Y, opt.CropCoordinates.Width, opt.CropCoordinates.Height) + if err != nil { + return nil, err + } + } + var swapped bool if opt.ResizeMethod == ImageOpsFit { swapped, err = o.fit(d, opt.Width, opt.Height) @@ -197,6 +231,10 @@ func (o *ImageOps) Transform(d Decoder, opt *ImageOptions, dst []byte) ([]byte, swapped, err = false, nil } + if swappedCrop { + swapped = !swapped + } + if err != nil { return nil, err } @@ -232,7 +270,7 @@ func (o *ImageOps) Transform(d Decoder, opt *ImageOptions, dst []byte) ([]byte, // content == nil and err == nil -- this is encoder telling us to do another frame - // for mulitple frames/gifs we need the decoded frame to be active again + // for multiple frames/gifs we need the decoded frame to be active again if swapped { o.swap() }