mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-09-21 14:11:22 +02:00
lazy processing test
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -11,10 +9,12 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/corona10/goimagehash"
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||
"github.com/imgproxy/imgproxy/v3/testutil"
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@@ -66,26 +66,34 @@ func (s *LoadTestSuite) testLoadFolder(folder string) {
|
||||
// Construct the source URL for imgproxy (no processing)
|
||||
sourceUrl := fmt.Sprintf("/insecure/plain/local:///%s/%s@png", folder, basePath)
|
||||
|
||||
imgproxyImageBytes := s.fetchImage(sourceUrl)
|
||||
imgproxyImage, err := png.Decode(bytes.NewReader(imgproxyImageBytes))
|
||||
s.Require().NoError(err, "Failed to decode PNG image from imgproxy for %s", basePath)
|
||||
imgproxyImageData := s.fetchImage(sourceUrl)
|
||||
var imgproxyImage vips.Image
|
||||
s.Require().NoError(imgproxyImage.Load(imgproxyImageData, 1, 1.0, 1))
|
||||
|
||||
hash1, err := testutil.ImageHash(unsafe.Pointer(imgproxyImage.VipsImage))
|
||||
s.Require().NoError(err)
|
||||
|
||||
referenceFile, err := os.Open(referencePath)
|
||||
s.Require().NoError(err)
|
||||
defer referenceFile.Close()
|
||||
|
||||
referenceImage, err := png.Decode(referenceFile)
|
||||
s.Require().NoError(err, "Failed to decode PNG reference image for %s", referencePath)
|
||||
|
||||
hash1, err := goimagehash.DifferenceHash(imgproxyImage)
|
||||
referenceImageData, err := s.Imgproxy().ImageDataFactory().NewFromPath(referencePath)
|
||||
s.Require().NoError(err)
|
||||
|
||||
hash2, err := goimagehash.DifferenceHash(referenceImage)
|
||||
var referenceImage vips.Image
|
||||
s.Require().NoError(referenceImage.Load(referenceImageData, 1, 1.0, 1))
|
||||
|
||||
hash2, err := testutil.ImageHash(unsafe.Pointer(referenceImage.VipsImage))
|
||||
s.Require().NoError(err)
|
||||
|
||||
distance, err := hash1.Distance(hash2)
|
||||
s.Require().NoError(err)
|
||||
|
||||
imgproxyImageData.Close()
|
||||
referenceImageData.Close()
|
||||
imgproxyImage.Clear()
|
||||
referenceImage.Clear()
|
||||
|
||||
s.Require().LessOrEqual(distance, similarityThreshold,
|
||||
"Image %s differs from reference image %s by %d, which is greater than the allowed threshold of %d",
|
||||
basePath, referencePath, distance, similarityThreshold)
|
||||
@@ -97,7 +105,7 @@ func (s *LoadTestSuite) testLoadFolder(folder string) {
|
||||
}
|
||||
|
||||
// fetchImage fetches an image from the imgproxy server
|
||||
func (s *LoadTestSuite) fetchImage(path string) []byte {
|
||||
func (s *LoadTestSuite) fetchImage(path string) imagedata.ImageData {
|
||||
resp := s.GET(path)
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -106,7 +114,10 @@ func (s *LoadTestSuite) fetchImage(path string) []byte {
|
||||
bytes, err := io.ReadAll(resp.Body)
|
||||
s.Require().NoError(err, "Failed to read response body from %s", path)
|
||||
|
||||
return bytes
|
||||
d, err := s.Imgproxy().ImageDataFactory().NewFromBytes(bytes)
|
||||
s.Require().NoError(err, "Failed to load image from bytes for %s", path)
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// TestLoadSaveToPng ensures that our load pipeline works,
|
||||
|
@@ -7,54 +7,67 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/fetcher"
|
||||
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||
"github.com/imgproxy/imgproxy/v3/options"
|
||||
"github.com/imgproxy/imgproxy/v3/security"
|
||||
"github.com/imgproxy/imgproxy/v3/testutil"
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
)
|
||||
|
||||
type ProcessingTestSuite struct {
|
||||
suite.Suite
|
||||
idf *imagedata.Factory
|
||||
pof *options.Factory
|
||||
testutil.LazySuite
|
||||
|
||||
imageDataFactory testutil.LazyObj[*imagedata.Factory]
|
||||
securityConfig testutil.LazyObj[*security.Config]
|
||||
security testutil.LazyObj[*security.Checker]
|
||||
poConfig testutil.LazyObj[*options.Config]
|
||||
po testutil.LazyObj[*options.Factory]
|
||||
}
|
||||
|
||||
func (s *ProcessingTestSuite) SetupSuite() {
|
||||
config.Reset()
|
||||
|
||||
config.MaxSrcResolution = 10 * 1024 * 1024
|
||||
config.MaxSrcFileSize = 10 * 1024 * 1024
|
||||
config.MaxAnimationFrames = 100
|
||||
config.MaxAnimationFrameResolution = 10 * 1024 * 1024
|
||||
|
||||
s.Require().NoError(vips.Init())
|
||||
|
||||
logrus.SetOutput(io.Discard)
|
||||
|
||||
fc := fetcher.NewDefaultConfig()
|
||||
f, err := fetcher.New(&fc)
|
||||
s.Require().NoError(err)
|
||||
s.imageDataFactory, _ = testutil.NewLazySuiteObj(s, func() (*imagedata.Factory, error) {
|
||||
c := fetcher.NewDefaultConfig()
|
||||
f, err := fetcher.New(&c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.idf = imagedata.NewFactory(f)
|
||||
return imagedata.NewFactory(f), nil
|
||||
})
|
||||
|
||||
scfg, err := security.LoadConfigFromEnv(nil)
|
||||
s.Require().NoError(err)
|
||||
s.securityConfig, _ = testutil.NewLazySuiteObj(s, func() (*security.Config, error) {
|
||||
c := security.NewDefaultConfig()
|
||||
|
||||
security, err := security.New(scfg)
|
||||
s.Require().NoError(err)
|
||||
c.DefaultOptions.MaxSrcResolution = 10 * 1024 * 1024
|
||||
c.DefaultOptions.MaxSrcFileSize = 10 * 1024 * 1024
|
||||
c.DefaultOptions.MaxAnimationFrames = 100
|
||||
c.DefaultOptions.MaxAnimationFrameResolution = 10 * 1024 * 1024
|
||||
|
||||
cfg, err := options.LoadConfigFromEnv(nil)
|
||||
s.Require().NoError(err)
|
||||
return &c, nil
|
||||
})
|
||||
|
||||
s.pof, err = options.NewFactory(cfg, security)
|
||||
s.Require().NoError(err)
|
||||
s.security, _ = testutil.NewLazySuiteObj(s, func() (*security.Checker, error) {
|
||||
return security.New(s.securityConfig())
|
||||
})
|
||||
|
||||
s.poConfig, _ = testutil.NewLazySuiteObj(s, func() (*options.Config, error) {
|
||||
c := options.NewDefaultConfig()
|
||||
return &c, nil
|
||||
})
|
||||
|
||||
s.po, _ = testutil.NewLazySuiteObj(s, func() (*options.Factory, error) {
|
||||
return options.NewFactory(s.poConfig(), s.security())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ProcessingTestSuite) openFile(name string) imagedata.ImageData {
|
||||
@@ -62,7 +75,7 @@ func (s *ProcessingTestSuite) openFile(name string) imagedata.ImageData {
|
||||
s.Require().NoError(err)
|
||||
path := filepath.Join(wd, "..", "testdata", name)
|
||||
|
||||
imagedata, err := s.idf.NewFromPath(path)
|
||||
imagedata, err := s.imageDataFactory().NewFromPath(path)
|
||||
s.Require().NoError(err)
|
||||
|
||||
return imagedata
|
||||
@@ -77,7 +90,7 @@ func (s *ProcessingTestSuite) checkSize(r *Result, width, height int) {
|
||||
func (s *ProcessingTestSuite) TestResizeToFit() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po := s.po().NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFit
|
||||
|
||||
testCases := []struct {
|
||||
@@ -115,7 +128,7 @@ func (s *ProcessingTestSuite) TestResizeToFit() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po := s.po().NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFit
|
||||
po.Enlarge = true
|
||||
|
||||
@@ -154,7 +167,7 @@ func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFitExtend() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po := s.po().NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFit
|
||||
po.Extend = options.ExtendOptions{
|
||||
Enabled: true,
|
||||
@@ -198,7 +211,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtend() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po := s.po().NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFit
|
||||
po.ExtendAspectRatio = options.ExtendOptions{
|
||||
Enabled: true,
|
||||
@@ -235,6 +248,12 @@ func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
|
||||
s.Require().NotNil(result)
|
||||
|
||||
s.checkSize(result, tc.outWidth, tc.outHeight)
|
||||
|
||||
var i vips.Image
|
||||
s.Require().NoError(i.Load(result.OutData, 1, 1.0, 1))
|
||||
h, err := testutil.ImageHash(unsafe.Pointer(i.VipsImage))
|
||||
s.Require().NoError(err)
|
||||
fmt.Println(h.ToString())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -242,7 +261,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFill() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po := s.po().NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFill
|
||||
|
||||
testCases := []struct {
|
||||
@@ -280,7 +299,7 @@ func (s *ProcessingTestSuite) TestResizeToFill() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po := s.po().NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFill
|
||||
po.Enlarge = true
|
||||
|
||||
@@ -319,7 +338,7 @@ func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFillExtend() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po := s.po().NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFill
|
||||
po.Extend = options.ExtendOptions{
|
||||
Enabled: true,
|
||||
@@ -365,7 +384,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtend() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po := s.po().NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFill
|
||||
po.ExtendAspectRatio = options.ExtendOptions{
|
||||
Enabled: true,
|
||||
@@ -411,7 +430,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFillDown() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po := s.po().NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFillDown
|
||||
|
||||
testCases := []struct {
|
||||
@@ -449,7 +468,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDown() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po := s.po().NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFillDown
|
||||
po.Enlarge = true
|
||||
|
||||
@@ -488,7 +507,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po := s.po().NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFillDown
|
||||
po.Extend = options.ExtendOptions{
|
||||
Enabled: true,
|
||||
@@ -534,7 +553,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po := s.po().NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFillDown
|
||||
po.ExtendAspectRatio = options.ExtendOptions{
|
||||
Enabled: true,
|
||||
@@ -578,7 +597,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
|
||||
func (s *ProcessingTestSuite) TestResultSizeLimit() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po := s.po().NewProcessingOptions()
|
||||
|
||||
testCases := []struct {
|
||||
limit int
|
||||
@@ -1006,7 +1025,7 @@ func (s *ProcessingTestSuite) TestResultSizeLimit() {
|
||||
}
|
||||
|
||||
func (s *ProcessingTestSuite) TestImageResolutionTooLarge() {
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po := s.po().NewProcessingOptions()
|
||||
po.SecurityOptions.MaxSrcResolution = 1
|
||||
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
76
testutil/image_hash.c
Normal file
76
testutil/image_hash.c
Normal file
@@ -0,0 +1,76 @@
|
||||
#include "image_hash.h"
|
||||
|
||||
/**
|
||||
* vips_image_read_to_memory: converts VipsImage to RGBA format and reads into memory buffer
|
||||
* @in: VipsImage to convert and read
|
||||
* @buf: pointer to buffer pointer (will be allocated)
|
||||
* @size: pointer to size_t to store the buffer size
|
||||
*
|
||||
* Converts the VipsImage to RGBA format using VIPS operations and reads the raw pixel data.
|
||||
* The caller is responsible for freeing the buffer using vips_memory_buffer_free().
|
||||
*
|
||||
* Returns: 0 on success, -1 on error
|
||||
*/
|
||||
int
|
||||
vips_image_read_to_memory(VipsImage *in, void **buf, size_t *size)
|
||||
{
|
||||
VipsImage *rgba_image = NULL;
|
||||
|
||||
if (!in || !buf || !size) {
|
||||
vips_error("vips_image_read_to_memory", "invalid arguments");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Initialize output parameters
|
||||
*buf = NULL;
|
||||
*size = 0;
|
||||
|
||||
// Convert to sRGB colorspace first if needed
|
||||
if (vips_colourspace(in, &rgba_image, VIPS_INTERPRETATION_sRGB, NULL) != 0) {
|
||||
vips_error("vips_image_read_to_memory", "failed to convert to sRGB");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Add alpha channel if not present (convert to RGBA)
|
||||
VipsImage *with_alpha = NULL;
|
||||
if (vips_image_hasalpha(rgba_image)) {
|
||||
// Already has alpha, just reference it
|
||||
with_alpha = rgba_image;
|
||||
g_object_ref(with_alpha);
|
||||
}
|
||||
else {
|
||||
// Add alpha channel
|
||||
if (vips_addalpha(rgba_image, &with_alpha, NULL) != 0) {
|
||||
g_object_unref(rgba_image);
|
||||
vips_error("vips_image_read_to_memory", "failed to add alpha channel");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
g_object_unref(rgba_image);
|
||||
|
||||
// Get raw pixel data
|
||||
*buf = vips_image_write_to_memory(with_alpha, size);
|
||||
g_object_unref(with_alpha);
|
||||
|
||||
if (*buf == NULL) {
|
||||
vips_error("vips_image_read_to_memory", "failed to write image to memory");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* vips_memory_buffer_free: frees memory buffer allocated by vips_image_write_to_memory
|
||||
* @buf: buffer pointer to free
|
||||
*
|
||||
* Frees the memory buffer allocated by vips_image_write_to_memory.
|
||||
* Safe to call with NULL pointer.
|
||||
*/
|
||||
void
|
||||
vips_memory_buffer_free(void *buf)
|
||||
{
|
||||
if (buf) {
|
||||
g_free(buf);
|
||||
}
|
||||
}
|
69
testutil/image_hash.go
Normal file
69
testutil/image_hash.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package testutil
|
||||
|
||||
/*
|
||||
#cgo pkg-config: vips
|
||||
#cgo CFLAGS: -O3
|
||||
#cgo LDFLAGS: -lm
|
||||
#include <vips/vips.h>
|
||||
#include "image_hash.h"
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"unsafe"
|
||||
|
||||
"github.com/corona10/goimagehash"
|
||||
)
|
||||
|
||||
// ImageHash calculates a hash of the VipsImage
|
||||
func ImageHash(vipsImgPtr unsafe.Pointer) (*goimagehash.ImageHash, error) {
|
||||
vipsImg := (*C.VipsImage)(vipsImgPtr)
|
||||
|
||||
// Convert to RGBA and read into memory using VIPS
|
||||
var data unsafe.Pointer
|
||||
var size C.size_t
|
||||
|
||||
// no one knows why this triggers linter
|
||||
//nolint:gocritic
|
||||
loadErr := C.vips_image_read_to_memory(vipsImg, &data, &size)
|
||||
if loadErr != 0 {
|
||||
return nil, fmt.Errorf("failed to convert VipsImage to RGBA memory")
|
||||
}
|
||||
defer C.vips_memory_buffer_free(data)
|
||||
|
||||
// Convert raw RGBA pixel data to Go image.Image
|
||||
goImg, err := createRGBAFromRGBAPixels(vipsImg, data, size)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert RGBA pixel data to image.Image: %v", err)
|
||||
}
|
||||
|
||||
hash, err := goimagehash.DifferenceHash(goImg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return hash, err
|
||||
}
|
||||
|
||||
// createRGBAFromRGBAPixels creates a Go image.Image from raw RGBA VIPS pixel data
|
||||
func createRGBAFromRGBAPixels(vipsImg *C.VipsImage, data unsafe.Pointer, size C.size_t) (*image.RGBA, error) {
|
||||
width := int(vipsImg.Xsize)
|
||||
height := int(vipsImg.Ysize)
|
||||
|
||||
// RGBA should have 4 bands
|
||||
expectedSize := width * height * 4
|
||||
if int(size) != expectedSize {
|
||||
return nil, fmt.Errorf("size mismatch: expected %d bytes for RGBA, got %d", expectedSize, int(size))
|
||||
}
|
||||
|
||||
pixels := unsafe.Slice((*byte)(data), int(size))
|
||||
|
||||
// Create RGBA image
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
// Copy RGBA pixel data directly
|
||||
copy(img.Pix, pixels)
|
||||
|
||||
return img, nil
|
||||
}
|
15
testutil/image_hash.h
Normal file
15
testutil/image_hash.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h> // uintptr_t
|
||||
|
||||
#include <vips/vips.h>
|
||||
|
||||
#ifndef TESTUTIL_IMAGE_HASH_H
|
||||
#define TESTUTIL_IMAGE_HASH_H
|
||||
|
||||
// Function to read VipsImage as RGBA into memory buffer
|
||||
int vips_image_read_to_memory(VipsImage *in, void **buf, size_t *size);
|
||||
|
||||
// Function to free/discard the memory buffer
|
||||
void vips_memory_buffer_free(void *buf);
|
||||
|
||||
#endif
|
@@ -21,7 +21,7 @@ typedef struct _VipsImgproxySourceClass {
|
||||
// creates new vips async source from a reader handle
|
||||
VipsImgproxySource *vips_new_imgproxy_source(uintptr_t readerHandle);
|
||||
|
||||
#endif
|
||||
|
||||
// unreferences the source, which leads to reader close
|
||||
void unref_imgproxy_source(VipsImgproxySource *source);
|
||||
|
||||
#endif
|
||||
|
Reference in New Issue
Block a user