mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-10-10 20:22:31 +02:00
IMG-59: processing_test.go became lazy, added test image hash calculation (#1532)
* lazy processing test * VHS * ImageHashMatcher
This commit is contained in:
@@ -39,6 +39,9 @@ RUN go install github.com/air-verse/air@latest
|
|||||||
# Install lefthook
|
# Install lefthook
|
||||||
RUN go install github.com/evilmartians/lefthook@latest
|
RUN go install github.com/evilmartians/lefthook@latest
|
||||||
|
|
||||||
|
# Install gotestsum
|
||||||
|
RUN go install gotest.tools/gotestsum@latest
|
||||||
|
|
||||||
# Install golangci-lint
|
# Install golangci-lint
|
||||||
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.1.6
|
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.1.6
|
||||||
|
|
||||||
|
@@ -1,40 +1,38 @@
|
|||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/png"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/corona10/goimagehash"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/config"
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
similarityThreshold = 5 // Distance between images to be considered similar
|
maxDistance = 0 // maximum image distance
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoadTestSuite struct {
|
type LoadTestSuite struct {
|
||||||
Suite
|
Suite
|
||||||
|
|
||||||
|
matcher *testutil.ImageHashMatcher
|
||||||
testImagesPath string
|
testImagesPath string
|
||||||
|
saveTmpImagesPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LoadTestSuite) SetupTest() {
|
func (s *LoadTestSuite) SetupTest() {
|
||||||
s.testImagesPath = s.TestData.Path("test-images")
|
s.testImagesPath = s.TestData.Path("test-images")
|
||||||
|
s.saveTmpImagesPath = os.Getenv("TEST_SAVE_TMP_IMAGES")
|
||||||
|
s.matcher = testutil.NewImageHashMatcher(s.TestData)
|
||||||
|
|
||||||
config.MaxAnimationFrames = 999
|
s.Config().Security.DefaultOptions.MaxAnimationFrames = 999
|
||||||
config.DevelopmentErrorsMode = true
|
s.Config().Server.DevelopmentErrorsMode = true
|
||||||
|
|
||||||
s.Config().Fetcher.Transport.Local.Root = s.testImagesPath
|
s.Config().Fetcher.Transport.Local.Root = s.testImagesPath
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,40 +53,19 @@ func (s *LoadTestSuite) testLoadFolder(folder string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get the base name of the file (8-bpp.png)
|
// get the base name of the file (8-bpp.png)
|
||||||
basePath := filepath.Base(path)
|
baseName := filepath.Base(path)
|
||||||
|
|
||||||
// Replace the extension with .png
|
|
||||||
referencePath := strings.TrimSuffix(basePath, filepath.Ext(basePath)) + ".png"
|
|
||||||
|
|
||||||
// Construct the full path to the reference image (integration/ folder)
|
|
||||||
referencePath = filepath.Join(s.testImagesPath, "integration", folder, referencePath)
|
|
||||||
|
|
||||||
// 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@bmp", folder, baseName)
|
||||||
|
|
||||||
imgproxyImageBytes := s.fetchImage(sourceUrl)
|
// Read source image from imgproxy
|
||||||
imgproxyImage, err := png.Decode(bytes.NewReader(imgproxyImageBytes))
|
resp := s.GET(sourceUrl)
|
||||||
s.Require().NoError(err, "Failed to decode PNG image from imgproxy for %s", basePath)
|
defer resp.Body.Close()
|
||||||
|
|
||||||
referenceFile, err := os.Open(referencePath)
|
s.Require().Equal(http.StatusOK, resp.StatusCode, "expected status code 200 OK, got %d, path: %s", resp.StatusCode, path)
|
||||||
s.Require().NoError(err)
|
|
||||||
defer referenceFile.Close()
|
|
||||||
|
|
||||||
referenceImage, err := png.Decode(referenceFile)
|
// Match image to precalculated hash
|
||||||
s.Require().NoError(err, "Failed to decode PNG reference image for %s", referencePath)
|
s.matcher.ImageMatches(s.T(), resp.Body, baseName, maxDistance)
|
||||||
|
|
||||||
hash1, err := goimagehash.DifferenceHash(imgproxyImage)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
hash2, err := goimagehash.DifferenceHash(referenceImage)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
distance, err := hash1.Distance(hash2)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -96,19 +73,6 @@ func (s *LoadTestSuite) testLoadFolder(folder string) {
|
|||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchImage fetches an image from the imgproxy server
|
|
||||||
func (s *LoadTestSuite) fetchImage(path string) []byte {
|
|
||||||
resp := s.GET(path)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
s.Require().Equal(http.StatusOK, resp.StatusCode, "Expected status code 200 OK, got %d, path: %s", resp.StatusCode, path)
|
|
||||||
|
|
||||||
bytes, err := io.ReadAll(resp.Body)
|
|
||||||
s.Require().NoError(err, "Failed to read response body from %s", path)
|
|
||||||
|
|
||||||
return bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLoadSaveToPng ensures that our load pipeline works,
|
// TestLoadSaveToPng ensures that our load pipeline works,
|
||||||
// including standard and custom loaders. For each source image
|
// including standard and custom loaders. For each source image
|
||||||
// in the folder, it does the passthrough request through imgproxy:
|
// in the folder, it does the passthrough request through imgproxy:
|
||||||
|
@@ -9,51 +9,64 @@ import (
|
|||||||
|
|
||||||
"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
|
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())
|
||||||
|
|
||||||
logger.Mute()
|
logger.Mute()
|
||||||
|
|
||||||
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) TearDownSuite() {
|
func (s *ProcessingTestSuite) TearDownSuite() {
|
||||||
@@ -65,7 +78,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 +93,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 +131,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 +170,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 +214,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,
|
||||||
@@ -245,7 +258,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 +296,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 +335,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 +381,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 +427,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 +465,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 +504,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 +550,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 +594,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 +1022,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")
|
||||||
|
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/1-bpp.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/1-bpp.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/16-bpp.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/16-bpp.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/24-bpp-no-alpha-mask.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/24-bpp-no-alpha-mask.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/24-bpp.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/24-bpp.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/32-bpp-with-alpha-self-gen.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/32-bpp-with-alpha-self-gen.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/32-bpp-with-alpha.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/32-bpp-with-alpha.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/4-bpp.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/4-bpp.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/8-bpp-rle-move-to-x.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/8-bpp-rle-move-to-x.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/8-bpp-rle-single-color.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/8-bpp-rle-single-color.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/8-bpp-rle-small.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/8-bpp-rle-small.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/8-bpp-rle.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/8-bpp-rle.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/8-bpp.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/8-bpp.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/gif.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/gif.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/heif.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/heif.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/jpg.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/jpg.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/jxl.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/jxl.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/multi-bmp.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/multi-bmp.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/multi-png.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/multi-png.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/png-256x256.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/png-256x256.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/single-bmp.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/single-bmp.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/svg.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/svg.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/tiff.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/tiff.hash
vendored
Normal file
Binary file not shown.
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/webp.hash
vendored
Normal file
BIN
testdata/test-hashes/TestIntegration/TestLoadSaveToPng/webp.hash
vendored
Normal file
Binary file not shown.
82
testutil/image_hash_matcher.c
Normal file
82
testutil/image_hash_matcher.c
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#include "image_hash_matcher.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_from_to_memory(void *in, size_t in_size, void **out, size_t *out_size, int *out_width, int *out_height)
|
||||||
|
{
|
||||||
|
if (!in || !out || !out_size || !out_width || !out_height) {
|
||||||
|
vips_error("vips_image_read_from_to_memory", "invalid arguments");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
VipsImage *base = vips_image_new_from_buffer(in, in_size, "", NULL);
|
||||||
|
if (base == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 2);
|
||||||
|
|
||||||
|
// Initialize output parameters
|
||||||
|
*out = NULL;
|
||||||
|
*out_size = 0;
|
||||||
|
|
||||||
|
// Convert to sRGB colorspace first if needed
|
||||||
|
if (vips_colourspace(base, &t[0], VIPS_INTERPRETATION_sRGB, NULL) != 0) {
|
||||||
|
VIPS_UNREF(base);
|
||||||
|
vips_error("vips_image_read_from_to_memory", "failed to convert to sRGB");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
in = t[0];
|
||||||
|
|
||||||
|
// Add alpha channel if not present (convert to RGBA)
|
||||||
|
if (!vips_image_hasalpha(base)) {
|
||||||
|
// Add alpha channel
|
||||||
|
if (vips_addalpha(base, &t[1], NULL) != 0) {
|
||||||
|
VIPS_UNREF(base);
|
||||||
|
vips_error("vips_image_read_from_to_memory", "failed to add alpha channel");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
in = t[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get raw pixel data, width and height
|
||||||
|
*out = vips_image_write_to_memory(in, out_size);
|
||||||
|
*out_width = base->Xsize;
|
||||||
|
*out_height = base->Ysize;
|
||||||
|
|
||||||
|
// Dispose the image regardless of the result
|
||||||
|
VIPS_UNREF(base);
|
||||||
|
|
||||||
|
if (*out == NULL) {
|
||||||
|
vips_error("vips_image_read_from_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);
|
||||||
|
}
|
||||||
|
}
|
189
testutil/image_hash_matcher.go
Normal file
189
testutil/image_hash_matcher.go
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo pkg-config: vips
|
||||||
|
#cgo CFLAGS: -O3
|
||||||
|
#cgo LDFLAGS: -lm
|
||||||
|
#include <vips/vips.h>
|
||||||
|
#include "image_hash_matcher.h"
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/corona10/goimagehash"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// hashPath is a path to hash data in testdata folder
|
||||||
|
hashPath = "test-hashes"
|
||||||
|
|
||||||
|
// If TEST_CREATE_MISSING_HASHES is set, matcher would create missing hash files
|
||||||
|
createMissingHashesEnv = "TEST_CREATE_MISSING_HASHES"
|
||||||
|
|
||||||
|
// If this is set, the images are saved to this folder before hash is calculated
|
||||||
|
saveTmpImagesPathEnv = "TEST_SAVE_TMP_IMAGES_PATH"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImageHashMatcher is a helper struct for image hash comparison in tests
|
||||||
|
type ImageHashMatcher struct {
|
||||||
|
hashesPath string
|
||||||
|
createMissingHashes bool
|
||||||
|
saveTmpImagesPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewImageHashMatcher creates a new ImageHashMatcher instance
|
||||||
|
func NewImageHashMatcher(testDataProvider *TestDataProvider) *ImageHashMatcher {
|
||||||
|
hashesPath := testDataProvider.Path(hashPath)
|
||||||
|
createMissingHashes := len(os.Getenv(createMissingHashesEnv)) > 0
|
||||||
|
saveTmpImagesPath := os.Getenv(saveTmpImagesPathEnv)
|
||||||
|
|
||||||
|
return &ImageHashMatcher{
|
||||||
|
hashesPath: hashesPath,
|
||||||
|
createMissingHashes: createMissingHashes,
|
||||||
|
saveTmpImagesPath: saveTmpImagesPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageHashMatches is a testing helper, which accepts image as reader, calculates
|
||||||
|
// difference hash and compares it with a hash saved to testdata/test-hashes
|
||||||
|
// folder.
|
||||||
|
func (m *ImageHashMatcher) ImageMatches(t *testing.T, img io.Reader, key string, maxDistance int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Read image in memory
|
||||||
|
buf, err := io.ReadAll(img)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Save tmp image if requested
|
||||||
|
m.saveTmpImage(t, key, buf)
|
||||||
|
|
||||||
|
// Convert to RGBA and read into memory using VIPS
|
||||||
|
var data unsafe.Pointer
|
||||||
|
var size C.size_t
|
||||||
|
var width, height C.int
|
||||||
|
|
||||||
|
// no one knows why this triggers linter
|
||||||
|
//nolint:gocritic
|
||||||
|
readErr := C.vips_image_read_from_to_memory(unsafe.Pointer(unsafe.SliceData(buf)), C.size_t(len(buf)), &data, &size, &width, &height)
|
||||||
|
if readErr != 0 {
|
||||||
|
t.Fatal("failed to load image using VIPS") // FailNow()
|
||||||
|
}
|
||||||
|
defer C.vips_memory_buffer_free(data)
|
||||||
|
|
||||||
|
// Convert raw RGBA pixel data to Go image.Image
|
||||||
|
goImg, err := createRGBAFromRGBAPixels(int(width), int(height), data, size)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sourceHash, err := goimagehash.DifferenceHash(goImg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Calculate image hash path (create folder if missing)
|
||||||
|
hashPath, err := m.makeTargetPath(t, m.hashesPath, t.Name(), key, "hash")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Try to read or create the hash file
|
||||||
|
f, err := os.Open(hashPath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
// If the hash file does not exist, and we are not allowed to create it, fail
|
||||||
|
if !m.createMissingHashes {
|
||||||
|
require.NoError(t, err, "failed to read target hash from %s, use TEST_CREATE_MISSING_HASHES=true to create it", hashPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create missing hash file
|
||||||
|
h, hashErr := os.Create(hashPath)
|
||||||
|
require.NoError(t, hashErr, "failed to create target hash file %s", hashPath)
|
||||||
|
defer h.Close()
|
||||||
|
|
||||||
|
// Dump calculated source hash to this hash file
|
||||||
|
hashErr = sourceHash.Dump(h)
|
||||||
|
require.NoError(t, hashErr, "failed to write target hash to %s", hashPath)
|
||||||
|
|
||||||
|
t.Logf("Created missing hash in %s", hashPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, if there is no error or error is something else
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Load image hash from hash file
|
||||||
|
targetHash, err := goimagehash.LoadImageHash(f)
|
||||||
|
require.NoError(t, err, "failed to load target hash from %s", hashPath)
|
||||||
|
|
||||||
|
// Ensure distance is OK
|
||||||
|
distance, err := sourceHash.Distance(targetHash)
|
||||||
|
require.NoError(t, err, "failed to calculate hash distance for %s", key)
|
||||||
|
|
||||||
|
require.LessOrEqual(t, distance, maxDistance, "image hashes are too different for %s: distance %d", key, distance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeTargetPath creates the target directory and returns file path for saving
|
||||||
|
// the image or hash.
|
||||||
|
func (m *ImageHashMatcher) makeTargetPath(t *testing.T, base, folder, filename, ext string) (string, error) {
|
||||||
|
// Create the target directory if it doesn't exist
|
||||||
|
targetDir := path.Join(base, folder)
|
||||||
|
err := os.MkdirAll(targetDir, 0755)
|
||||||
|
require.NoError(t, err, "failed to create %s target directory", targetDir)
|
||||||
|
|
||||||
|
// Replace the extension with the detected one
|
||||||
|
filename = strings.TrimSuffix(filename, filepath.Ext(filename)) + "." + ext
|
||||||
|
|
||||||
|
// Create the target file
|
||||||
|
targetPath := path.Join(targetDir, filename)
|
||||||
|
|
||||||
|
return targetPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveTmpImage saves the provided image data to a temporary file
|
||||||
|
func (m *ImageHashMatcher) saveTmpImage(t *testing.T, key string, buf []byte) {
|
||||||
|
if m.saveTmpImagesPath == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect the image type to get the correct extension
|
||||||
|
ext, err := imagetype.Detect(bytes.NewReader(buf))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
targetPath, err := m.makeTargetPath(t, m.saveTmpImagesPath, t.Name(), key, ext.String())
|
||||||
|
require.NoError(t, err, "failed to create TEST_SAVE_TMP_IMAGES target path for %s/%s", t.Name(), key)
|
||||||
|
|
||||||
|
targetFile, err := os.Create(targetPath)
|
||||||
|
require.NoError(t, err, "failed to create TEST_SAVE_TMP_IMAGES target file %s", targetPath)
|
||||||
|
defer targetFile.Close()
|
||||||
|
|
||||||
|
// Write the image data to the file
|
||||||
|
_, err = io.Copy(targetFile, bytes.NewReader(buf))
|
||||||
|
require.NoError(t, err, "failed to write to TEST_SAVE_TMP_IMAGES target file %s", targetPath)
|
||||||
|
|
||||||
|
t.Logf("Saved temporary image to %s", targetPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createRGBAFromRGBAPixels creates a Go image.Image from raw RGBA VIPS pixel data
|
||||||
|
func createRGBAFromRGBAPixels(width, height int, data unsafe.Pointer, size C.size_t) (*image.RGBA, error) {
|
||||||
|
// 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
|
||||||
|
}
|
19
testutil/image_hash_matcher.h
Normal file
19
testutil/image_hash_matcher.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdint.h> // uintptr_t
|
||||||
|
|
||||||
|
#include <vips/vips.h>
|
||||||
|
|
||||||
|
#ifndef TESTUTIL_IMAGE_HASH_MATCHER_H
|
||||||
|
#define TESTUTIL_IMAGE_HASH_MATCHER_H
|
||||||
|
|
||||||
|
// Function to read VipsImage as RGBA into memory buffer
|
||||||
|
int vips_image_read_from_to_memory(
|
||||||
|
void *in, size_t in_size, // inner raw buffer and its size
|
||||||
|
void **out, size_t *out_size, // out raw buffer an its size
|
||||||
|
int *out_width, int *out_height // out image width and height
|
||||||
|
);
|
||||||
|
|
||||||
|
// Function to free/discard the memory buffer
|
||||||
|
void vips_memory_buffer_free(void *buf);
|
||||||
|
|
||||||
|
#endif
|
@@ -124,7 +124,7 @@ vips_foreign_save_bmp_build(VipsObject *object)
|
|||||||
// bands (3 or 4) * 8 bits
|
// bands (3 or 4) * 8 bits
|
||||||
int bands = vips_image_get_bands(in);
|
int bands = vips_image_get_bands(in);
|
||||||
|
|
||||||
if ((bands > 3) || (bands > 4)) {
|
if ((bands < 3) || (bands > 4)) {
|
||||||
vips_error("vips_foreign_save_bmp_build", "BMP source file must have 3 or 4 bands (RGB or RGBA)");
|
vips_error("vips_foreign_save_bmp_build", "BMP source file must have 3 or 4 bands (RGB or RGBA)");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
@@ -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