diff --git a/config.go b/config.go index db176544..5d3d50e0 100644 --- a/config.go +++ b/config.go @@ -20,14 +20,14 @@ type HandlerConfigs struct { // Config represents an instance configuration type Config struct { - Workers workers.Config - FallbackImage auximageprovider.StaticConfig - WatermarkImage auximageprovider.StaticConfig - Fetcher fetcher.Config - Handlers HandlerConfigs - Server server.Config - Security security.Config - ProcessingOptions options.Config + Workers workers.Config + FallbackImage auximageprovider.StaticConfig + WatermarkImage auximageprovider.StaticConfig + Fetcher fetcher.Config + Handlers HandlerConfigs + Server server.Config + Security security.Config + Options options.Config } // NewDefaultConfig creates a new default configuration @@ -41,9 +41,9 @@ func NewDefaultConfig() Config { Processing: processinghandler.NewDefaultConfig(), Stream: streamhandler.NewDefaultConfig(), }, - Server: server.NewDefaultConfig(), - Security: security.NewDefaultConfig(), - ProcessingOptions: options.NewDefaultConfig(), + Server: server.NewDefaultConfig(), + Security: security.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 } diff --git a/go.mod b/go.mod index 440e42ec..ca729608 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 4104fbd9..abd0df7d 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/handlers/processing/handler.go b/handlers/processing/handler.go index a67553a0..e7482d7c 100644 --- a/handlers/processing/handler.go +++ b/handlers/processing/handler.go @@ -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)) } diff --git a/handlers/processing/request_methods.go b/handlers/processing/request_methods.go index ab43b612..dd315396 100644 --- a/handlers/processing/request_methods.go +++ b/handlers/processing/request_methods.go @@ -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 diff --git a/imgproxy.go b/imgproxy.go index 25d14c6f..cb366dbb 100644 --- a/imgproxy.go +++ b/imgproxy.go @@ -35,15 +35,15 @@ type ImgproxyHandlers struct { // Imgproxy holds all the components needed for imgproxy to function type Imgproxy struct { - workers *workers.Workers - fallbackImage auximageprovider.Provider - watermarkImage auximageprovider.Provider - fetcher *fetcher.Fetcher - imageDataFactory *imagedata.Factory - handlers ImgproxyHandlers - security *security.Checker - processingOptionsFactory *options.Factory - config *Config + workers *workers.Workers + fallbackImage auximageprovider.Provider + watermarkImage auximageprovider.Provider + fetcher *fetcher.Fetcher + imageDataFactory *imagedata.Factory + handlers ImgproxyHandlers + security *security.Checker + optionsFactory *options.Factory + config *Config } // New creates a new imgproxy instance @@ -75,20 +75,20 @@ 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 } imgproxy := &Imgproxy{ - workers: workers, - fallbackImage: fallbackImage, - watermarkImage: watermarkImage, - fetcher: fetcher, - imageDataFactory: idf, - config: config, - security: security, - processingOptionsFactory: processingOptionsFactory, + workers: workers, + fallbackImage: fallbackImage, + watermarkImage: watermarkImage, + fetcher: fetcher, + imageDataFactory: idf, + config: config, + security: security, + 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 } diff --git a/integration/processing_handler_test.go b/integration/processing_handler_test.go index cd791af2..d2774967 100644 --- a/integration/processing_handler_test.go +++ b/integration/processing_handler_test.go @@ -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") diff --git a/options/config.go b/options/config.go index 21f6c98a..737d1d15 100644 --- a/options/config.go +++ b/options/config.go @@ -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 } diff --git a/options/factory.go b/options/factory.go index ec29bf20..2ec505e7 100644 --- a/options/factory.go +++ b/options/factory.go @@ -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 @@ -13,9 +9,10 @@ type Presets = map[string]urlOptions // Factory creates ProcessingOptions instances type Factory struct { - config *Config // Factory configuration - security *security.Checker // Security checker for generating security options - presets Presets // Parsed presets + 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 @@ -25,9 +22,10 @@ func NewFactory(config *Config, security *security.Checker) (*Factory, error) { } f := &Factory{ - config: config, - security: security, - presets: make(map[string]urlOptions), + 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() } diff --git a/options/presets.go b/options/presets.go index 718246d0..ba584bb7 100644 --- a/options/presets.go +++ b/options/presets.go @@ -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 +} diff --git a/options/processing_options.go b/options/processing_options.go index d9ff35c2..2f45f05a 100644 --- a/options/processing_options.go +++ b/options/processing_options.go @@ -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 diff --git a/options/processing_options_test.go b/options/processing_options_test.go index fa2bfb3a..c1f1f1c1 100644 --- a/options/processing_options_test.go +++ b/options/processing_options_test.go @@ -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)) } diff --git a/processing/pipeline.go b/processing/pipeline.go index 6b9080e2..6536aa91 100644 --- a/processing/pipeline.go +++ b/processing/pipeline.go @@ -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, @@ -90,9 +87,8 @@ func (p pipeline) Run( dprScale: 1.0, vectorBaseScale: 1.0, - cropGravity: po.Crop.Gravity, - watermarkProvider: watermark, - processingOptionsFactory: processingOptionsFactory, + cropGravity: po.Crop.Gravity, + watermarkProvider: watermark, } if pctx.cropGravity.Type == options.GravityUnknown { diff --git a/processing/processing.go b/processing/processing.go index 1f43760a..07c17625 100644 --- a/processing/processing.go +++ b/processing/processing.go @@ -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 } } diff --git a/processing/processing_test.go b/processing/processing_test.go index 2cf152b5..14d4b06c 100644 --- a/processing/processing_test.go +++ b/processing/processing_test.go @@ -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()) diff --git a/processing/watermark.go b/processing/watermark.go index 21f096c0..b0cb0c2a 100644 --- a/processing/watermark.go +++ b/processing/watermark.go @@ -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) } diff --git a/testutil/equal_but_not_same.go b/testutil/equal_but_not_same.go new file mode 100644 index 00000000..2ee19e98 --- /dev/null +++ b/testutil/equal_but_not_same.go @@ -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() + } +}