Clone with testutil.Helper

This commit is contained in:
Viktor Sokolov
2025-09-15 19:41:29 +02:00
parent 4698023504
commit 126fc72ffd
17 changed files with 301 additions and 180 deletions

View File

@@ -27,7 +27,7 @@ type Config struct {
Handlers HandlerConfigs
Server server.Config
Security security.Config
ProcessingOptions options.Config
Options options.Config
}
// NewDefaultConfig creates a new default configuration
@@ -43,7 +43,7 @@ func NewDefaultConfig() Config {
},
Server: server.NewDefaultConfig(),
Security: security.NewDefaultConfig(),
ProcessingOptions: options.NewDefaultConfig(),
Options: options.NewDefaultConfig(),
}
}
@@ -85,7 +85,7 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
return nil, err
}
if _, err = options.LoadConfigFromEnv(&c.ProcessingOptions); err != nil {
if _, err = options.LoadConfigFromEnv(&c.Options); err != nil {
return nil, err
}

7
go.mod
View File

@@ -11,6 +11,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1
github.com/DarthSim/godotenv v1.3.1
github.com/DataDog/datadog-agent/pkg/trace v0.67.0
github.com/DataDog/datadog-go/v5 v5.6.0
github.com/DataDog/dd-trace-go/v2 v2.0.1
github.com/airbrake/gobrake/v5 v5.6.2
@@ -42,6 +43,7 @@ require (
github.com/stretchr/testify v1.10.0
github.com/tdewolff/parse/v2 v2.8.1
github.com/trimmer-io/go-xmp v1.0.0
github.com/urfave/cli/v3 v3.4.1
go.opentelemetry.io/contrib/detectors/aws/ec2 v1.37.0
go.opentelemetry.io/contrib/detectors/aws/ecs v1.37.0
go.opentelemetry.io/contrib/detectors/aws/eks v1.37.0
@@ -58,7 +60,6 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.37.0
go.opentelemetry.io/otel/trace v1.37.0
go.uber.org/automaxprocs v1.6.0
golang.org/x/image v0.28.0
golang.org/x/net v0.41.0
golang.org/x/sync v0.15.0
golang.org/x/sys v0.33.0
@@ -82,7 +83,6 @@ require (
github.com/DataDog/datadog-agent/pkg/obfuscate v0.67.0 // indirect
github.com/DataDog/datadog-agent/pkg/proto v0.67.0 // indirect
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.67.0 // indirect
github.com/DataDog/datadog-agent/pkg/trace v0.67.0 // indirect
github.com/DataDog/datadog-agent/pkg/util/log v0.67.0 // indirect
github.com/DataDog/datadog-agent/pkg/util/scrubber v0.67.0 // indirect
github.com/DataDog/datadog-agent/pkg/version v0.67.0 // indirect
@@ -119,7 +119,6 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 // indirect
@@ -175,7 +174,6 @@ require (
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500 // indirect
@@ -184,7 +182,6 @@ require (
github.com/tinylib/msgp v1.3.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/urfave/cli/v3 v3.4.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zeebo/errs v1.4.0 // indirect

9
go.sum
View File

@@ -44,7 +44,6 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DarthSim/godotenv v1.3.1 h1:NMWdswlRx2M9uPY4Ux8p/Q/rDs7A97OG89fECiQ/Tz0=
github.com/DarthSim/godotenv v1.3.1/go.mod h1:B3ySe1HYTUFFR6+TPyHyxPWjUdh48il0Blebg9p1cCc=
github.com/DataDog/appsec-internal-go v1.13.0 h1:aO6DmHYsAU8BNFuvYJByhMKGgcQT3WAbj9J/sgAJxtA=
@@ -173,8 +172,6 @@ github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI=
github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -397,8 +394,6 @@ github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3 h1:4+LEVO
github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3/go.mod h1:vl5+MqJ1nBINuSsUI2mGgH79UweUT/B5Fy8857PqyyI=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc=
@@ -446,8 +441,6 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/trimmer-io/go-xmp v1.0.0 h1:zY8bolSga5kOjBAaHS6hrdxLgEoYuT875xTy0QDwZWs=
github.com/trimmer-io/go-xmp v1.0.0/go.mod h1:Aaptr9sp1lLv7UnCAdQ+gSHZyY2miYaKmcNVj7HRBwA=
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM=
github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/vmihailenco/msgpack/v4 v4.3.13 h1:A2wsiTbvp63ilDaWmsk2wjx6xZdxQOvpiNlKBGKKXKI=
@@ -582,8 +575,6 @@ golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=

View File

@@ -26,7 +26,7 @@ type HandlerContext interface {
WatermarkImage() auximageprovider.Provider
ImageDataFactory() *imagedata.Factory
Security() *security.Checker
ProcessingOptionsFactory() *options.Factory
OptionsFactory() *options.Factory
}
// Handler handles image processing requests
@@ -109,7 +109,7 @@ func (h *Handler) newRequest(
}
// parse image url and processing options
po, imageURL, err := h.HandlerContext.ProcessingOptionsFactory().ParsePath(path, req.Header)
po, imageURL, err := h.OptionsFactory().ParsePath(path, req.Header)
if err != nil {
return "", nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategoryPathParsing))
}

View File

@@ -157,7 +157,7 @@ func (r *request) getFallbackImage(
// processImage calls actual image processing
func (r *request) processImage(ctx context.Context, originData imagedata.ImageData) (*processing.Result, error) {
defer monitoring.StartProcessingSegment(ctx, r.monitoringMeta.Filter(monitoring.MetaProcessingOptions))()
return processing.ProcessImage(ctx, originData, r.po, r.WatermarkImage(), r.ProcessingOptionsFactory())
return processing.ProcessImage(ctx, originData, r.po, r.WatermarkImage())
}
// writeDebugHeaders writes debug headers (X-Origin-*, X-Result-*) to the response

View File

@@ -42,7 +42,7 @@ type Imgproxy struct {
imageDataFactory *imagedata.Factory
handlers ImgproxyHandlers
security *security.Checker
processingOptionsFactory *options.Factory
optionsFactory *options.Factory
config *Config
}
@@ -75,7 +75,7 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
return nil, err
}
processingOptionsFactory, err := options.NewFactory(&config.ProcessingOptions, security)
processingOptionsFactory, err := options.NewFactory(&config.Options, security)
if err != nil {
return nil, err
}
@@ -88,7 +88,7 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
imageDataFactory: idf,
config: config,
security: security,
processingOptionsFactory: processingOptionsFactory,
optionsFactory: processingOptionsFactory,
}
imgproxy.handlers.Health = healthhandler.New()
@@ -208,6 +208,6 @@ func (i *Imgproxy) Security() *security.Checker {
return i.security
}
func (i *Imgproxy) ProcessingOptionsFactory() *options.Factory {
return i.processingOptionsFactory
func (i *Imgproxy) OptionsFactory() *options.Factory {
return i.optionsFactory
}

View File

@@ -168,7 +168,7 @@ func (s *ProcessingHandlerTestSuite) TestResultingFormatNotSupported() {
}
func (s *ProcessingHandlerTestSuite) TestSkipProcessingConfig() {
s.Config().ProcessingOptions.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
s.Config().Options.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png")
@@ -184,7 +184,7 @@ func (s *ProcessingHandlerTestSuite) TestSkipProcessingPO() {
}
func (s *ProcessingHandlerTestSuite) TestSkipProcessingSameFormat() {
s.Config().ProcessingOptions.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
s.Config().Options.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
@@ -477,7 +477,7 @@ func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvg() {
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithEnforceAvif() {
config.AlwaysRasterizeSvg = true
s.Config().ProcessingOptions.EnforceWebp = true
s.Config().Options.EnforceWebp = true
res := s.GET("/unsafe/plain/local:///test1.svg", http.Header{"Accept": []string{"image/webp"}})
@@ -487,7 +487,7 @@ func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithEnforceAvif() {
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgDisabled() {
config.AlwaysRasterizeSvg = false
s.Config().ProcessingOptions.EnforceWebp = true
s.Config().Options.EnforceWebp = true
res := s.GET("/unsafe/plain/local:///test1.svg")
@@ -497,7 +497,7 @@ func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgDisabled() {
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithFormat() {
config.AlwaysRasterizeSvg = true
s.Config().ProcessingOptions.SkipProcessingFormats = []imagetype.Type{imagetype.SVG}
s.Config().Options.SkipProcessingFormats = []imagetype.Type{imagetype.SVG}
res := s.GET("/unsafe/plain/local:///test1.svg@svg")

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"maps"
"slices"
"github.com/imgproxy/imgproxy/v3/config"
"github.com/imgproxy/imgproxy/v3/ensure"
@@ -107,19 +108,18 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
c.ReturnAttachment = config.ReturnAttachment
// Image processing formats
copy(c.SkipProcessingFormats, config.SkipProcessingFormats)
c.SkipProcessingFormats = slices.Clone(config.SkipProcessingFormats)
// Presets configuration
copy(c.Presets, config.Presets)
c.Presets = slices.Clone(config.Presets)
c.OnlyPresets = config.OnlyPresets
// Quality settings
c.Quality = config.Quality
c.FormatQuality = make(map[imagetype.Type]int, len(config.FormatQuality))
maps.Copy(c.FormatQuality, config.FormatQuality)
c.FormatQuality = maps.Clone(config.FormatQuality)
// Security and validation
copy(c.AllowedProcessingOptions, config.AllowedProcessingOptions)
c.AllowedProcessingOptions = slices.Clone(config.AllowedProcessingOptions)
// Format preference and enforcement
c.AutoWebp = config.AutoWebp
@@ -135,9 +135,8 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
// URL processing
c.ArgumentsSeparator = config.ArgumentsSeparator
c.BaseURL = config.BaseURL
c.URLReplacements = append([]URLReplacement(nil), config.URLReplacements...)
c.URLReplacements = slices.Clone(config.URLReplacements)
c.Base64URLIncludesFilename = config.Base64URLIncludesFilename
c.Presets = config.Presets
return c, nil
}

View File

@@ -1,11 +1,7 @@
package options
import (
"maps"
"github.com/imgproxy/imgproxy/v3/imagetype"
"github.com/imgproxy/imgproxy/v3/security"
"github.com/imgproxy/imgproxy/v3/vips"
)
// Presets is a map of preset names to their corresponding urlOptions
@@ -16,6 +12,7 @@ type Factory struct {
config *Config // Factory configuration
security *security.Checker // Security checker for generating security options
presets Presets // Parsed presets
defaultPO *ProcessingOptions // Default processing options
}
// NewFactory creates new Factory instance
@@ -28,6 +25,7 @@ func NewFactory(config *Config, security *security.Checker) (*Factory, error) {
config: config,
security: security,
presets: make(map[string]urlOptions),
defaultPO: newDefaultProcessingOptions(config, security),
}
if err := f.parsePresets(); err != nil {
@@ -43,47 +41,5 @@ func NewFactory(config *Config, security *security.Checker) (*Factory, error) {
// NewProcessingOptions creates new ProcessingOptions instance
func (f *Factory) NewProcessingOptions() *ProcessingOptions {
po := ProcessingOptions{
ResizingType: ResizeFit,
Width: 0,
Height: 0,
ZoomWidth: 1,
ZoomHeight: 1,
Gravity: GravityOptions{Type: GravityCenter},
Enlarge: false,
Extend: ExtendOptions{Enabled: false, Gravity: GravityOptions{Type: GravityCenter}},
ExtendAspectRatio: ExtendOptions{Enabled: false, Gravity: GravityOptions{Type: GravityCenter}},
Padding: PaddingOptions{Enabled: false},
Trim: TrimOptions{Enabled: false, Threshold: 10, Smart: true},
Rotate: 0,
Quality: 0,
MaxBytes: 0,
Format: imagetype.Unknown,
Background: vips.Color{R: 255, G: 255, B: 255},
Blur: 0,
Sharpen: 0,
Dpr: 1,
Watermark: WatermarkOptions{Opacity: 1, Position: GravityOptions{Type: GravityCenter}},
StripMetadata: f.config.StripMetadata,
KeepCopyright: f.config.KeepCopyright,
StripColorProfile: f.config.StripColorProfile,
AutoRotate: f.config.AutoRotate,
EnforceThumbnail: f.config.EnforceThumbnail,
ReturnAttachment: f.config.ReturnAttachment,
SkipProcessingFormats: append([]imagetype.Type(nil), f.config.SkipProcessingFormats...),
UsedPresets: make([]string, 0, len(f.config.Presets)),
SecurityOptions: f.security.NewOptions(),
// Basically, we need this to update ETag when `IMGPROXY_QUALITY` is changed
defaultQuality: f.config.Quality,
}
po.defaultOptions = &po
po.FormatQuality = make(map[imagetype.Type]int, len(f.config.FormatQuality))
maps.Copy(po.FormatQuality, f.config.FormatQuality)
return &po
return f.defaultPO.clone()
}

View File

@@ -16,18 +16,6 @@ func (f *Factory) parsePresets() error {
return nil
}
// validatePresets validates all presets by applying them to a new ProcessingOptions instance
func (f *Factory) validatePresets() error {
for name, opts := range f.presets {
po := f.NewProcessingOptions()
if err := f.applyURLOptions(po, opts, true, name); err != nil {
return fmt.Errorf("Error in preset `%s`: %s", name, err)
}
}
return nil
}
// parsePreset parses a preset string and returns the name and options
func (f *Factory) parsePreset(presetStr string) error {
presetStr = strings.Trim(presetStr, " ")
@@ -68,3 +56,15 @@ func (f *Factory) parsePreset(presetStr string) error {
return nil
}
// validatePresets validates all presets by applying them to a new ProcessingOptions instance
func (f *Factory) validatePresets() error {
for name, opts := range f.presets {
po := f.NewProcessingOptions()
if err := f.applyURLOptions(po, opts, true, name); err != nil {
return fmt.Errorf("Error in preset `%s`: %s", name, err)
}
}
return nil
}

View File

@@ -2,6 +2,7 @@ package options
import (
"encoding/base64"
"maps"
"net/http"
"slices"
"strconv"
@@ -119,6 +120,46 @@ type ProcessingOptions struct {
defaultOptions *ProcessingOptions
}
func newDefaultProcessingOptions(config *Config, security *security.Checker) *ProcessingOptions {
po := ProcessingOptions{
ResizingType: ResizeFit,
Width: 0,
Height: 0,
ZoomWidth: 1,
ZoomHeight: 1,
Gravity: GravityOptions{Type: GravityCenter},
Enlarge: false,
Extend: ExtendOptions{Enabled: false, Gravity: GravityOptions{Type: GravityCenter}},
ExtendAspectRatio: ExtendOptions{Enabled: false, Gravity: GravityOptions{Type: GravityCenter}},
Padding: PaddingOptions{Enabled: false},
Trim: TrimOptions{Enabled: false, Threshold: 10, Smart: true},
Rotate: 0,
Quality: 0,
FormatQuality: maps.Clone(config.FormatQuality),
MaxBytes: 0,
Format: imagetype.Unknown,
Background: vips.Color{R: 255, G: 255, B: 255},
Blur: 0,
Sharpen: 0,
Dpr: 1,
Watermark: WatermarkOptions{Opacity: 1, Position: GravityOptions{Type: GravityCenter}},
StripMetadata: config.StripMetadata,
KeepCopyright: config.KeepCopyright,
StripColorProfile: config.StripColorProfile,
AutoRotate: config.AutoRotate,
EnforceThumbnail: config.EnforceThumbnail,
ReturnAttachment: config.ReturnAttachment,
SkipProcessingFormats: slices.Clone(config.SkipProcessingFormats),
SecurityOptions: security.NewOptions(),
defaultQuality: config.Quality,
}
return &po
}
func (po *ProcessingOptions) GetQuality() int {
q := po.Quality
@@ -145,6 +186,34 @@ func (po *ProcessingOptions) MarshalJSON() ([]byte, error) {
return po.Diff().MarshalJSON()
}
// Default returns the ProcessingOptions instance with defaults set
func (po *ProcessingOptions) Default() *ProcessingOptions {
return po.defaultOptions.clone()
}
// clone clones ProcessingOptions struct and its slices and maps
func (po *ProcessingOptions) clone() *ProcessingOptions {
clone := *po
clone.FormatQuality = maps.Clone(po.FormatQuality)
clone.SkipProcessingFormats = slices.Clone(po.SkipProcessingFormats)
clone.UsedPresets = slices.Clone(po.UsedPresets)
if po.Expires != nil {
poExipres := *po.Expires
clone.Expires = &poExipres
}
// Copy the pointer to the default options struct from parent.
// Nil means that we have just cloned the default options struct itself
// so we set it as default options.
if clone.defaultOptions == nil {
clone.defaultOptions = po
}
return &clone
}
func parseDimension(d *int, name, arg string) error {
if v, err := strconv.Atoi(arg); err == nil && v >= 0 {
*d = v

View File

@@ -8,6 +8,7 @@ import (
"regexp"
"strings"
"testing"
"time"
"github.com/imgproxy/imgproxy/v3/config"
"github.com/imgproxy/imgproxy/v3/imagetype"
@@ -58,11 +59,6 @@ func (s *ProcessingOptionsTestSuite) SetupSuite() {
)
}
func (s *ProcessingOptionsTestSuite) SetupTest() {
// NOTE: This is temporary, remove once finish img-57-po
config.Reset()
}
func (s *ProcessingOptionsTestSuite) SetupSubTest() {
s.ResetLazyObjects()
}
@@ -520,7 +516,6 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWebpDetection() {
func (s *ProcessingOptionsTestSuite) TestParsePathWebpEnforce() {
s.config().EnforceWebp = true
config.EnforceWebp = true // TODO: REMOVE, THIS IS TEMP
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Accept": []string{"image/webp"}}
@@ -533,8 +528,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWebpEnforce() {
}
func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeader() {
s.config().EnableClientHints = true // TODO: REMOVE
config.EnableClientHints = true // TODO: REMOVE
s.config().EnableClientHints = true
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Width": []string{"100"}}
@@ -691,6 +685,23 @@ func (s *ProcessingOptionsTestSuite) TestParseAllowedOptions() {
}
}
func (s *ProcessingOptionsTestSuite) TestProcessingOptionsClone() {
now := time.Now()
// Create ProcessingOptions using factory
original := s.factory().NewProcessingOptions()
original.SkipProcessingFormats = []imagetype.Type{
imagetype.PNG, imagetype.JPEG,
}
original.UsedPresets = []string{"preset1", "preset2"}
original.Expires = &now
// Clone the original
cloned := original.clone()
testutil.EqualButNotSame(s.T(), original, cloned)
}
func TestProcessingOptions(t *testing.T) {
suite.Run(t, new(ProcessingOptionsTestSuite))
}

View File

@@ -19,8 +19,6 @@ type pipelineContext struct {
// The watermark image provider, if any watermarking is to be done.
watermarkProvider auximageprovider.Provider
processingOptionsFactory *options.Factory
trimmed bool
srcWidth int
@@ -79,7 +77,6 @@ func (p pipeline) Run(
po *options.ProcessingOptions,
imgdata imagedata.ImageData,
watermark auximageprovider.Provider,
processingOptionsFactory *options.Factory,
) error {
pctx := pipelineContext{
ctx: ctx,
@@ -92,7 +89,6 @@ func (p pipeline) Run(
cropGravity: po.Crop.Gravity,
watermarkProvider: watermark,
processingOptionsFactory: processingOptionsFactory,
}
if pctx.cropGravity.Type == options.GravityUnknown {

View File

@@ -85,7 +85,6 @@ func ProcessImage(
imgdata imagedata.ImageData,
po *options.ProcessingOptions,
watermarkProvider auximageprovider.Provider,
processingOptionsFactory *options.Factory,
) (*Result, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
@@ -139,12 +138,12 @@ func ProcessImage(
}
// Transform the image (resize, crop, etc)
if err = transformImage(ctx, img, po, imgdata, animated, watermarkProvider, processingOptionsFactory); err != nil {
if err = transformImage(ctx, img, po, imgdata, animated, watermarkProvider); err != nil {
return nil, err
}
// Finalize the image (colorspace conversion, metadata stripping, etc)
if err = finalizePipeline.Run(ctx, img, po, imgdata, watermarkProvider, processingOptionsFactory); err != nil {
if err = finalizePipeline.Run(ctx, img, po, imgdata, watermarkProvider); err != nil {
return nil, err
}
@@ -389,13 +388,12 @@ func transformImage(
imgdata imagedata.ImageData,
asAnimated bool,
watermark auximageprovider.Provider,
processingOptionsFactory *options.Factory,
) error {
if asAnimated {
return transformAnimated(ctx, img, po, watermark, processingOptionsFactory)
return transformAnimated(ctx, img, po, watermark)
}
return mainPipeline.Run(ctx, img, po, imgdata, watermark, processingOptionsFactory)
return mainPipeline.Run(ctx, img, po, imgdata, watermark)
}
func transformAnimated(
@@ -403,7 +401,6 @@ func transformAnimated(
img *vips.Image,
po *options.ProcessingOptions,
watermark auximageprovider.Provider,
processingOptionsFactory *options.Factory,
) error {
if po.Trim.Enabled {
log.Warning("Trim is not supported for animated images")
@@ -458,7 +455,7 @@ func transformAnimated(
// Transform the frame using the main pipeline.
// We don't provide imgdata here to prevent scale-on-load.
// Let's skip passing watermark here since in would be applied later to all frames at once.
if err = mainPipeline.Run(ctx, frame, po, nil, nil, processingOptionsFactory); err != nil {
if err = mainPipeline.Run(ctx, frame, po, nil, nil); err != nil {
return err
}
@@ -490,7 +487,7 @@ func transformAnimated(
dprScale = 1.0
}
if err = applyWatermark(ctx, img, watermark, processingOptionsFactory, po, dprScale, framesCount); err != nil {
if err = applyWatermark(ctx, img, watermark, po, dprScale, framesCount); err != nil {
return err
}
}

View File

@@ -103,7 +103,7 @@ func (s *ProcessingTestSuite) TestResizeToFit() {
po.Width = tc.width
po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil, s.pof)
result, err := ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err)
s.Require().NotNil(result)
@@ -142,7 +142,7 @@ func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
po.Width = tc.width
po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil, s.pof)
result, err := ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err)
s.Require().NotNil(result)
@@ -186,7 +186,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtend() {
po.Width = tc.width
po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil, s.pof)
result, err := ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err)
s.Require().NotNil(result)
@@ -230,7 +230,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
po.Width = tc.width
po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil, s.pof)
result, err := ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err)
s.Require().NotNil(result)
@@ -268,7 +268,7 @@ func (s *ProcessingTestSuite) TestResizeToFill() {
po.Width = tc.width
po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil, s.pof)
result, err := ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err)
s.Require().NotNil(result)
@@ -307,7 +307,7 @@ func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
po.Width = tc.width
po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil, s.pof)
result, err := ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err)
s.Require().NotNil(result)
@@ -353,7 +353,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtend() {
po.Width = tc.width
po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil, s.pof)
result, err := ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err)
s.Require().NotNil(result)
@@ -399,7 +399,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
po.Width = tc.width
po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil, s.pof)
result, err := ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err)
s.Require().NotNil(result)
@@ -437,7 +437,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDown() {
po.Width = tc.width
po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil, s.pof)
result, err := ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err)
s.Require().NotNil(result)
@@ -476,7 +476,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
po.Width = tc.width
po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil, s.pof)
result, err := ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err)
s.Require().NotNil(result)
@@ -522,7 +522,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
po.Width = tc.width
po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil, s.pof)
result, err := ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err)
s.Require().NotNil(result)
@@ -566,7 +566,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
po.Width = tc.width
po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil, s.pof)
result, err := ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err)
s.Require().NotNil(result)
@@ -995,7 +995,7 @@ func (s *ProcessingTestSuite) TestResultSizeLimit() {
po.Rotate = tc.rotate
po.Padding = tc.padding
result, err := ProcessImage(context.Background(), imgdata, po, nil, s.pof)
result, err := ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err)
s.Require().NotNil(result)
@@ -1010,7 +1010,7 @@ func (s *ProcessingTestSuite) TestImageResolutionTooLarge() {
po.SecurityOptions.MaxSrcResolution = 1
imgdata := s.openFile("test2.jpg")
_, err := ProcessImage(context.Background(), imgdata, po, nil, s.pof)
_, err := ProcessImage(context.Background(), imgdata, po, nil)
s.Require().Error(err)
s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode())

View File

@@ -22,20 +22,22 @@ var watermarkPipeline = pipeline{
padding,
}
func prepareWatermark(wm *vips.Image, wmData imagedata.ImageData, opts *options.WatermarkOptions, pof *options.Factory, imgWidth, imgHeight int, offsetScale float64, framesCount int) error {
func prepareWatermark(wm *vips.Image, wmData imagedata.ImageData, po *options.ProcessingOptions, imgWidth, imgHeight int, offsetScale float64, framesCount int) error {
if err := wm.Load(wmData, 1, 1.0, 1); err != nil {
return err
}
po := pof.NewProcessingOptions()
po.ResizingType = options.ResizeFit
po.Dpr = 1
po.Enlarge = true
po.Format = wmData.Format()
opts := po.Watermark
wmPo := po.Default()
wmPo.ResizingType = options.ResizeFit
wmPo.Dpr = 1
wmPo.Enlarge = true
wmPo.Format = wmData.Format()
if opts.Scale > 0 {
po.Width = max(imath.ScaleToEven(imgWidth, opts.Scale), 1)
po.Height = max(imath.ScaleToEven(imgHeight, opts.Scale), 1)
wmPo.Width = max(imath.ScaleToEven(imgWidth, opts.Scale), 1)
wmPo.Height = max(imath.ScaleToEven(imgHeight, opts.Scale), 1)
}
if opts.ShouldReplicate() {
@@ -53,14 +55,14 @@ func prepareWatermark(wm *vips.Image, wmData imagedata.ImageData, opts *options.
offY = imath.ScaleToEven(imgHeight, opts.Position.Y)
}
po.Padding.Enabled = true
po.Padding.Left = offX / 2
po.Padding.Right = offX - po.Padding.Left
po.Padding.Top = offY / 2
po.Padding.Bottom = offY - po.Padding.Top
wmPo.Padding.Enabled = true
wmPo.Padding.Left = offX / 2
wmPo.Padding.Right = offX - wmPo.Padding.Left
wmPo.Padding.Top = offY / 2
wmPo.Padding.Bottom = offY - wmPo.Padding.Top
}
if err := watermarkPipeline.Run(context.Background(), wm, po, wmData, nil, pof); err != nil {
if err := watermarkPipeline.Run(context.Background(), wm, wmPo, wmData, nil); err != nil {
return err
}
@@ -84,7 +86,6 @@ func applyWatermark(
ctx context.Context,
img *vips.Image,
watermark auximageprovider.Provider,
pof *options.Factory,
po *options.ProcessingOptions,
offsetScale float64,
framesCount int,
@@ -111,7 +112,7 @@ func applyWatermark(
height := img.Height()
frameHeight := height / framesCount
if err := prepareWatermark(wm, wmData, &opts, pof, width, frameHeight, offsetScale, framesCount); err != nil {
if err := prepareWatermark(wm, wmData, po, width, frameHeight, offsetScale, framesCount); err != nil {
return err
}
@@ -194,5 +195,5 @@ func watermark(
return nil
}
return applyWatermark(pctx.ctx, img, pctx.watermarkProvider, pctx.processingOptionsFactory, po, pctx.dprScale, 1)
return applyWatermark(pctx.ctx, img, pctx.watermarkProvider, po, pctx.dprScale, 1)
}

View File

@@ -0,0 +1,104 @@
package testutil
import (
"reflect"
"testing"
"github.com/stretchr/testify/require"
)
// EqualButNotSame asserts that expected and actual objects are not the same.
// It recursively checks all fields to ensure that no pointers are shared.
// If a pointer, slice or map are nil in either object, the test fails.
func EqualButNotSame(t *testing.T, expected, actual any) {
t.Helper()
expectedVal := reflect.ValueOf(expected)
actualVal := reflect.ValueOf(actual)
deepEqual(t, expectedVal, actualVal, "")
}
// deepEqual recursively verifies that all values are equal but pointers are different
// except for the Expires field which is explicitly allowed to be shared
func deepEqual(t *testing.T, left, right reflect.Value, fieldPath string) {
require.True(t, left.IsValid() && right.IsValid(), "invalid value at %s", fieldPath)
require.Equal(t, left.Type(), right.Type(), "types are not equal at %s", fieldPath)
switch left.Kind() {
case reflect.Ptr:
// Pointers should not be nil and must point to different objects
require.False(t, left.IsNil(), "nil pointer at %s (left)", fieldPath)
require.False(t, right.IsNil(), "nil pointer at %s (right)", fieldPath)
require.NotSame(t, left.Interface(), right.Interface(), "shared pointer at %s", fieldPath)
deepEqual(t, left.Elem(), right.Elem(), fieldPath)
case reflect.Slice:
// Slices should contain some elements and must not share the same underlying array
require.Equal(t, left.Len(), right.Len(), "slice length mismatch at %s", fieldPath)
require.NotEmpty(t, left.Len(), "slice must not be empty %s (left)", fieldPath)
require.NotEmpty(t, right.Len(), "slice must not be empty %s (right)", fieldPath)
require.NotEqual(t, left.Pointer(), right.Pointer(), "shared slices at %s", fieldPath)
// Recursively verify slice elements
for i := 0; i < left.Len(); i++ {
elemPath := buildPath(fieldPath, "[", anyToString(i), "]")
deepEqual(t, left.Index(i), right.Index(i), elemPath)
}
case reflect.Map:
// Maps should contain some elements and must not share the same underlying map
require.Equal(t, left.Len(), right.Len(), "map length mismatch at %s", fieldPath)
require.NotEmpty(t, left.Len(), "map must not be empty %s (left)", fieldPath)
require.NotEmpty(t, right.Len(), "map must not be empty %s (right)", fieldPath)
require.NotEqual(t, left.Pointer(), right.Pointer(), "shared maps at %s", fieldPath)
// Recursively verify map values
for _, key := range left.MapKeys() {
keyStr := anyToString(key.Interface())
keyPath := buildPath(fieldPath, "[", keyStr, "]")
originalMapVal := left.MapIndex(key)
clonedMapVal := right.MapIndex(key)
deepEqual(t, originalMapVal, clonedMapVal, keyPath)
}
case reflect.Struct:
require.Equal(t, left.Interface(), right.Interface(), "structs are not equal at %s", fieldPath)
// Fallback to recursive field-by-field comparison
for i := 0; i < left.NumField(); i++ {
field := left.Type().Field(i)
if !field.IsExported() {
continue // Skip unexported fields
}
nestedPath := buildPath(fieldPath, ".", field.Name, "")
originalFieldVal := left.Field(i)
clonedFieldVal := right.Field(i)
deepEqual(t, originalFieldVal, clonedFieldVal, nestedPath)
}
default:
// For primitive types, just verify equality
require.Equal(t, left.Interface(), right.Interface(), "values not equal at %s", fieldPath)
}
}
// buildPath builds a field path for error messages
func buildPath(basePath, separator, element, suffix string) string {
if basePath == "" {
return element + suffix
}
return basePath + separator + element + suffix
}
// anyToString converts an any to a string for path building
func anyToString(v any) string {
switch val := v.(type) {
case string:
return val
default:
return reflect.ValueOf(val).String()
}
}