mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-09-26 11:38:01 +02:00
lazy processing test
This commit is contained in:
@@ -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)
|
imgproxyImageData := s.fetchImage(sourceUrl)
|
||||||
imgproxyImage, err := png.Decode(bytes.NewReader(imgproxyImageBytes))
|
var imgproxyImage vips.Image
|
||||||
s.Require().NoError(err, "Failed to decode PNG image from imgproxy for %s", basePath)
|
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)
|
referenceImageData, err := s.Imgproxy().ImageDataFactory().NewFromPath(referencePath)
|
||||||
s.Require().NoError(err, "Failed to decode PNG reference image for %s", referencePath)
|
|
||||||
|
|
||||||
hash1, err := goimagehash.DifferenceHash(imgproxyImage)
|
|
||||||
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,
|
||||||
|
@@ -7,54 +7,67 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"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/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
|
testutil.LazySuite
|
||||||
idf *imagedata.Factory
|
|
||||||
pof *options.Factory
|
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())
|
||||||
|
|
||||||
logrus.SetOutput(io.Discard)
|
logrus.SetOutput(io.Discard)
|
||||||
|
|
||||||
fc := fetcher.NewDefaultConfig()
|
s.imageDataFactory, _ = testutil.NewLazySuiteObj(s, func() (*imagedata.Factory, error) {
|
||||||
f, err := fetcher.New(&fc)
|
c := fetcher.NewDefaultConfig()
|
||||||
s.Require().NoError(err)
|
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.securityConfig, _ = testutil.NewLazySuiteObj(s, func() (*security.Config, error) {
|
||||||
s.Require().NoError(err)
|
c := security.NewDefaultConfig()
|
||||||
|
|
||||||
security, err := security.New(scfg)
|
c.DefaultOptions.MaxSrcResolution = 10 * 1024 * 1024
|
||||||
s.Require().NoError(err)
|
c.DefaultOptions.MaxSrcFileSize = 10 * 1024 * 1024
|
||||||
|
c.DefaultOptions.MaxAnimationFrames = 100
|
||||||
|
c.DefaultOptions.MaxAnimationFrameResolution = 10 * 1024 * 1024
|
||||||
|
|
||||||
cfg, err := options.LoadConfigFromEnv(nil)
|
return &c, nil
|
||||||
s.Require().NoError(err)
|
})
|
||||||
|
|
||||||
s.pof, err = options.NewFactory(cfg, security)
|
s.security, _ = testutil.NewLazySuiteObj(s, func() (*security.Checker, error) {
|
||||||
s.Require().NoError(err)
|
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 {
|
func (s *ProcessingTestSuite) openFile(name string) imagedata.ImageData {
|
||||||
@@ -62,7 +75,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
|
||||||
@@ -77,7 +90,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 {
|
||||||
@@ -115,7 +128,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
|
||||||
|
|
||||||
@@ -154,7 +167,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,
|
||||||
@@ -198,7 +211,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,
|
||||||
@@ -235,6 +248,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())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,7 +261,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 {
|
||||||
@@ -280,7 +299,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
|
||||||
|
|
||||||
@@ -319,7 +338,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,
|
||||||
@@ -365,7 +384,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,
|
||||||
@@ -411,7 +430,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 {
|
||||||
@@ -449,7 +468,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
|
||||||
|
|
||||||
@@ -488,7 +507,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,
|
||||||
@@ -534,7 +553,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,
|
||||||
@@ -578,7 +597,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
|
||||||
@@ -1006,7 +1025,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
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
|
// 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
|
||||||
|
Reference in New Issue
Block a user