lazy processing test

This commit is contained in:
Viktor Sokolov
2025-09-19 14:31:01 +02:00
parent 521df55ad5
commit 9de0ea362f
6 changed files with 244 additions and 54 deletions

View File

@@ -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,

View File

@@ -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
View 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
View 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
View 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

View File

@@ -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