浏览代码

lazy processing test

Viktor Sokolov 2 周之前
父节点
当前提交
4ac16fe355
共有 6 个文件被更改,包括 244 次插入53 次删除
  1. 24 13
      integration/load_test.go
  2. 58 38
      processing/processing_test.go
  3. 76 0
      testutil/image_hash.c
  4. 69 0
      testutil/image_hash.go
  5. 15 0
      testutil/image_hash.h
  6. 2 2
      vips/source.h

+ 24 - 13
integration/load_test.go

@@ -1,9 +1,7 @@
 package integration
 package integration
 
 
 import (
 import (
-	"bytes"
 	"fmt"
 	"fmt"
-	"image/png"
 	"io"
 	"io"
 	"net/http"
 	"net/http"
 	"os"
 	"os"
@@ -11,10 +9,12 @@ import (
 	"path/filepath"
 	"path/filepath"
 	"strings"
 	"strings"
 	"testing"
 	"testing"
+	"unsafe"
 
 
-	"github.com/corona10/goimagehash"
 	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/config"
+	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
+	"github.com/imgproxy/imgproxy/v3/testutil"
 	"github.com/imgproxy/imgproxy/v3/vips"
 	"github.com/imgproxy/imgproxy/v3/vips"
 	"github.com/stretchr/testify/suite"
 	"github.com/stretchr/testify/suite"
 )
 )
@@ -66,26 +66,34 @@ func (s *LoadTestSuite) testLoadFolder(folder string) {
 		// Construct the source URL for imgproxy (no processing)
 		// Construct the source URL for imgproxy (no processing)
 		sourceUrl := fmt.Sprintf("/insecure/plain/local:///%s/%s@png", folder, basePath)
 		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)
 		referenceFile, err := os.Open(referencePath)
 		s.Require().NoError(err)
 		s.Require().NoError(err)
 		defer referenceFile.Close()
 		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)
 		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)
 		s.Require().NoError(err)
 
 
 		distance, err := hash1.Distance(hash2)
 		distance, err := hash1.Distance(hash2)
 		s.Require().NoError(err)
 		s.Require().NoError(err)
 
 
+		imgproxyImageData.Close()
+		referenceImageData.Close()
+		imgproxyImage.Clear()
+		referenceImage.Clear()
+
 		s.Require().LessOrEqual(distance, similarityThreshold,
 		s.Require().LessOrEqual(distance, similarityThreshold,
 			"Image %s differs from reference image %s by %d, which is greater than the allowed threshold of %d",
 			"Image %s differs from reference image %s by %d, which is greater than the allowed threshold of %d",
 			basePath, referencePath, distance, similarityThreshold)
 			basePath, referencePath, distance, similarityThreshold)
@@ -97,7 +105,7 @@ func (s *LoadTestSuite) testLoadFolder(folder string) {
 }
 }
 
 
 // fetchImage fetches an image from the imgproxy server
 // 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)
 	resp := s.GET(path)
 	defer resp.Body.Close()
 	defer resp.Body.Close()
 
 
@@ -106,7 +114,10 @@ func (s *LoadTestSuite) fetchImage(path string) []byte {
 	bytes, err := io.ReadAll(resp.Body)
 	bytes, err := io.ReadAll(resp.Body)
 	s.Require().NoError(err, "Failed to read response body from %s", path)
 	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,
 // TestLoadSaveToPng ensures that our load pipeline works,

+ 58 - 38
processing/processing_test.go

@@ -6,54 +6,68 @@ import (
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
 	"testing"
 	"testing"
+	"unsafe"
 
 
 	"github.com/stretchr/testify/suite"
 	"github.com/stretchr/testify/suite"
 
 
-	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/fetcher"
 	"github.com/imgproxy/imgproxy/v3/fetcher"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/logger"
 	"github.com/imgproxy/imgproxy/v3/logger"
 	"github.com/imgproxy/imgproxy/v3/options"
 	"github.com/imgproxy/imgproxy/v3/options"
 	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/security"
+	"github.com/imgproxy/imgproxy/v3/testutil"
 	"github.com/imgproxy/imgproxy/v3/vips"
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 )
 
 
 type ProcessingTestSuite struct {
 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() {
 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())
 	s.Require().NoError(vips.Init())
 
 
 	logger.Mute()
 	logger.Mute()
 
 
-	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) TearDownSuite() {
 func (s *ProcessingTestSuite) TearDownSuite() {
@@ -65,7 +79,7 @@ func (s *ProcessingTestSuite) openFile(name string) imagedata.ImageData {
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 	path := filepath.Join(wd, "..", "testdata", name)
 	path := filepath.Join(wd, "..", "testdata", name)
 
 
-	imagedata, err := s.idf.NewFromPath(path)
+	imagedata, err := s.imageDataFactory().NewFromPath(path)
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
 	return imagedata
 	return imagedata
@@ -80,7 +94,7 @@ func (s *ProcessingTestSuite) checkSize(r *Result, width, height int) {
 func (s *ProcessingTestSuite) TestResizeToFit() {
 func (s *ProcessingTestSuite) TestResizeToFit() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := s.pof.NewProcessingOptions()
+	po := s.po().NewProcessingOptions()
 	po.ResizingType = options.ResizeFit
 	po.ResizingType = options.ResizeFit
 
 
 	testCases := []struct {
 	testCases := []struct {
@@ -118,7 +132,7 @@ func (s *ProcessingTestSuite) TestResizeToFit() {
 func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
 func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := s.pof.NewProcessingOptions()
+	po := s.po().NewProcessingOptions()
 	po.ResizingType = options.ResizeFit
 	po.ResizingType = options.ResizeFit
 	po.Enlarge = true
 	po.Enlarge = true
 
 
@@ -157,7 +171,7 @@ func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
 func (s *ProcessingTestSuite) TestResizeToFitExtend() {
 func (s *ProcessingTestSuite) TestResizeToFitExtend() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := s.pof.NewProcessingOptions()
+	po := s.po().NewProcessingOptions()
 	po.ResizingType = options.ResizeFit
 	po.ResizingType = options.ResizeFit
 	po.Extend = options.ExtendOptions{
 	po.Extend = options.ExtendOptions{
 		Enabled: true,
 		Enabled: true,
@@ -201,7 +215,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtend() {
 func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
 func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := s.pof.NewProcessingOptions()
+	po := s.po().NewProcessingOptions()
 	po.ResizingType = options.ResizeFit
 	po.ResizingType = options.ResizeFit
 	po.ExtendAspectRatio = options.ExtendOptions{
 	po.ExtendAspectRatio = options.ExtendOptions{
 		Enabled: true,
 		Enabled: true,
@@ -238,6 +252,12 @@ func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
 			s.Require().NotNil(result)
 			s.Require().NotNil(result)
 
 
 			s.checkSize(result, tc.outWidth, tc.outHeight)
 			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())
 		})
 		})
 	}
 	}
 }
 }
@@ -245,7 +265,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
 func (s *ProcessingTestSuite) TestResizeToFill() {
 func (s *ProcessingTestSuite) TestResizeToFill() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := s.pof.NewProcessingOptions()
+	po := s.po().NewProcessingOptions()
 	po.ResizingType = options.ResizeFill
 	po.ResizingType = options.ResizeFill
 
 
 	testCases := []struct {
 	testCases := []struct {
@@ -283,7 +303,7 @@ func (s *ProcessingTestSuite) TestResizeToFill() {
 func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
 func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := s.pof.NewProcessingOptions()
+	po := s.po().NewProcessingOptions()
 	po.ResizingType = options.ResizeFill
 	po.ResizingType = options.ResizeFill
 	po.Enlarge = true
 	po.Enlarge = true
 
 
@@ -322,7 +342,7 @@ func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
 func (s *ProcessingTestSuite) TestResizeToFillExtend() {
 func (s *ProcessingTestSuite) TestResizeToFillExtend() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := s.pof.NewProcessingOptions()
+	po := s.po().NewProcessingOptions()
 	po.ResizingType = options.ResizeFill
 	po.ResizingType = options.ResizeFill
 	po.Extend = options.ExtendOptions{
 	po.Extend = options.ExtendOptions{
 		Enabled: true,
 		Enabled: true,
@@ -368,7 +388,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtend() {
 func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
 func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := s.pof.NewProcessingOptions()
+	po := s.po().NewProcessingOptions()
 	po.ResizingType = options.ResizeFill
 	po.ResizingType = options.ResizeFill
 	po.ExtendAspectRatio = options.ExtendOptions{
 	po.ExtendAspectRatio = options.ExtendOptions{
 		Enabled: true,
 		Enabled: true,
@@ -414,7 +434,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
 func (s *ProcessingTestSuite) TestResizeToFillDown() {
 func (s *ProcessingTestSuite) TestResizeToFillDown() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := s.pof.NewProcessingOptions()
+	po := s.po().NewProcessingOptions()
 	po.ResizingType = options.ResizeFillDown
 	po.ResizingType = options.ResizeFillDown
 
 
 	testCases := []struct {
 	testCases := []struct {
@@ -452,7 +472,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDown() {
 func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
 func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := s.pof.NewProcessingOptions()
+	po := s.po().NewProcessingOptions()
 	po.ResizingType = options.ResizeFillDown
 	po.ResizingType = options.ResizeFillDown
 	po.Enlarge = true
 	po.Enlarge = true
 
 
@@ -491,7 +511,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
 func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
 func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := s.pof.NewProcessingOptions()
+	po := s.po().NewProcessingOptions()
 	po.ResizingType = options.ResizeFillDown
 	po.ResizingType = options.ResizeFillDown
 	po.Extend = options.ExtendOptions{
 	po.Extend = options.ExtendOptions{
 		Enabled: true,
 		Enabled: true,
@@ -537,7 +557,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
 func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
 func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := s.pof.NewProcessingOptions()
+	po := s.po().NewProcessingOptions()
 	po.ResizingType = options.ResizeFillDown
 	po.ResizingType = options.ResizeFillDown
 	po.ExtendAspectRatio = options.ExtendOptions{
 	po.ExtendAspectRatio = options.ExtendOptions{
 		Enabled: true,
 		Enabled: true,
@@ -581,7 +601,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
 func (s *ProcessingTestSuite) TestResultSizeLimit() {
 func (s *ProcessingTestSuite) TestResultSizeLimit() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := s.pof.NewProcessingOptions()
+	po := s.po().NewProcessingOptions()
 
 
 	testCases := []struct {
 	testCases := []struct {
 		limit        int
 		limit        int
@@ -1009,7 +1029,7 @@ func (s *ProcessingTestSuite) TestResultSizeLimit() {
 }
 }
 
 
 func (s *ProcessingTestSuite) TestImageResolutionTooLarge() {
 func (s *ProcessingTestSuite) TestImageResolutionTooLarge() {
-	po := s.pof.NewProcessingOptions()
+	po := s.po().NewProcessingOptions()
 	po.SecurityOptions.MaxSrcResolution = 1
 	po.SecurityOptions.MaxSrcResolution = 1
 
 
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")

+ 76 - 0
testutil/image_hash.c

@@ -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 - 0
testutil/image_hash.go

@@ -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 - 0
testutil/image_hash.h

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

+ 2 - 2
vips/source.h

@@ -21,7 +21,7 @@ typedef struct _VipsImgproxySourceClass {
 // creates new vips async source from a reader handle
 // creates new vips async source from a reader handle
 VipsImgproxySource *vips_new_imgproxy_source(uintptr_t readerHandle);
 VipsImgproxySource *vips_new_imgproxy_source(uintptr_t readerHandle);
 
 
-#endif
-
 // unreferences the source, which leads to reader close
 // unreferences the source, which leads to reader close
 void unref_imgproxy_source(VipsImgproxySource *source);
 void unref_imgproxy_source(VipsImgproxySource *source);
+
+#endif