Browse Source

IMG-57: introduce processing options factory (#1526)

* Intoduced options.Factory

* ProcessingOptionsFactory in processing and watermark

* Clone with testutil.Helper
Victor Sokolov 3 weeks ago
parent
commit
4b05e87274

+ 8 - 1
config.go

@@ -6,6 +6,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/fetcher"
 	"github.com/imgproxy/imgproxy/v3/fetcher"
 	processinghandler "github.com/imgproxy/imgproxy/v3/handlers/processing"
 	processinghandler "github.com/imgproxy/imgproxy/v3/handlers/processing"
 	streamhandler "github.com/imgproxy/imgproxy/v3/handlers/stream"
 	streamhandler "github.com/imgproxy/imgproxy/v3/handlers/stream"
+	"github.com/imgproxy/imgproxy/v3/options"
 	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/server"
 	"github.com/imgproxy/imgproxy/v3/server"
 	"github.com/imgproxy/imgproxy/v3/workers"
 	"github.com/imgproxy/imgproxy/v3/workers"
@@ -26,6 +27,7 @@ type Config struct {
 	Handlers       HandlerConfigs
 	Handlers       HandlerConfigs
 	Server         server.Config
 	Server         server.Config
 	Security       security.Config
 	Security       security.Config
+	Options        options.Config
 }
 }
 
 
 // NewDefaultConfig creates a new default configuration
 // NewDefaultConfig creates a new default configuration
@@ -41,6 +43,7 @@ func NewDefaultConfig() Config {
 		},
 		},
 		Server:   server.NewDefaultConfig(),
 		Server:   server.NewDefaultConfig(),
 		Security: security.NewDefaultConfig(),
 		Security: security.NewDefaultConfig(),
+		Options:  options.NewDefaultConfig(),
 	}
 	}
 }
 }
 
 
@@ -78,7 +81,11 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	if _, err := security.LoadConfigFromEnv(&c.Security); err != nil {
+	if _, err = security.LoadConfigFromEnv(&c.Security); err != nil {
+		return nil, err
+	}
+
+	if _, err = options.LoadConfigFromEnv(&c.Options); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 

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

+ 0 - 9
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 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 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 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 h1:NMWdswlRx2M9uPY4Ux8p/Q/rDs7A97OG89fECiQ/Tz0=
 github.com/DarthSim/godotenv v1.3.1/go.mod h1:B3ySe1HYTUFFR6+TPyHyxPWjUdh48il0Blebg9p1cCc=
 github.com/DarthSim/godotenv v1.3.1/go.mod h1:B3ySe1HYTUFFR6+TPyHyxPWjUdh48il0Blebg9p1cCc=
 github.com/DataDog/appsec-internal-go v1.13.0 h1:aO6DmHYsAU8BNFuvYJByhMKGgcQT3WAbj9J/sgAJxtA=
 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/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 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI=
 github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI=
 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.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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 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/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 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
 github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
 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 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
 github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
 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=
 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/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 h1:zY8bolSga5kOjBAaHS6hrdxLgEoYuT875xTy0QDwZWs=
 github.com/trimmer-io/go-xmp v1.0.0/go.mod h1:Aaptr9sp1lLv7UnCAdQ+gSHZyY2miYaKmcNVj7HRBwA=
 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 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM=
 github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
 github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
 github.com/vmihailenco/msgpack/v4 v4.3.13 h1:A2wsiTbvp63ilDaWmsk2wjx6xZdxQOvpiNlKBGKKXKI=
 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-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 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/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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=

+ 2 - 1
handlers/processing/handler.go

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

+ 12 - 0
imgproxy.go

@@ -14,6 +14,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/memory"
 	"github.com/imgproxy/imgproxy/v3/memory"
 	"github.com/imgproxy/imgproxy/v3/monitoring/prometheus"
 	"github.com/imgproxy/imgproxy/v3/monitoring/prometheus"
+	"github.com/imgproxy/imgproxy/v3/options"
 	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/server"
 	"github.com/imgproxy/imgproxy/v3/server"
 	"github.com/imgproxy/imgproxy/v3/workers"
 	"github.com/imgproxy/imgproxy/v3/workers"
@@ -41,6 +42,7 @@ type Imgproxy struct {
 	imageDataFactory *imagedata.Factory
 	imageDataFactory *imagedata.Factory
 	handlers         ImgproxyHandlers
 	handlers         ImgproxyHandlers
 	security         *security.Checker
 	security         *security.Checker
+	optionsFactory   *options.Factory
 	config           *Config
 	config           *Config
 }
 }
 
 
@@ -73,6 +75,11 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
+	processingOptionsFactory, err := options.NewFactory(&config.Options, security)
+	if err != nil {
+		return nil, err
+	}
+
 	imgproxy := &Imgproxy{
 	imgproxy := &Imgproxy{
 		workers:          workers,
 		workers:          workers,
 		fallbackImage:    fallbackImage,
 		fallbackImage:    fallbackImage,
@@ -81,6 +88,7 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
 		imageDataFactory: idf,
 		imageDataFactory: idf,
 		config:           config,
 		config:           config,
 		security:         security,
 		security:         security,
+		optionsFactory:   processingOptionsFactory,
 	}
 	}
 
 
 	imgproxy.handlers.Health = healthhandler.New()
 	imgproxy.handlers.Health = healthhandler.New()
@@ -199,3 +207,7 @@ func (i *Imgproxy) ImageDataFactory() *imagedata.Factory {
 func (i *Imgproxy) Security() *security.Checker {
 func (i *Imgproxy) Security() *security.Checker {
 	return i.security
 	return i.security
 }
 }
+
+func (i *Imgproxy) OptionsFactory() *options.Factory {
+	return i.optionsFactory
+}

+ 0 - 11
init.go

@@ -10,7 +10,6 @@ import (
 	"github.com/imgproxy/imgproxy/v3/gliblog"
 	"github.com/imgproxy/imgproxy/v3/gliblog"
 	"github.com/imgproxy/imgproxy/v3/logger"
 	"github.com/imgproxy/imgproxy/v3/logger"
 	"github.com/imgproxy/imgproxy/v3/monitoring"
 	"github.com/imgproxy/imgproxy/v3/monitoring"
-	"github.com/imgproxy/imgproxy/v3/options"
 	"github.com/imgproxy/imgproxy/v3/processing"
 	"github.com/imgproxy/imgproxy/v3/processing"
 	"github.com/imgproxy/imgproxy/v3/vips"
 	"github.com/imgproxy/imgproxy/v3/vips"
 	"go.uber.org/automaxprocs/maxprocs"
 	"go.uber.org/automaxprocs/maxprocs"
@@ -53,16 +52,6 @@ func Init() error {
 		return err
 		return err
 	}
 	}
 
 
-	if err := options.ParsePresets(config.Presets); err != nil {
-		vips.Shutdown()
-		return err
-	}
-
-	if err := options.ValidatePresets(); err != nil {
-		vips.Shutdown()
-		return err
-	}
-
 	return nil
 	return nil
 }
 }
 
 

+ 6 - 6
integration/processing_handler_test.go

@@ -168,7 +168,7 @@ func (s *ProcessingHandlerTestSuite) TestResultingFormatNotSupported() {
 }
 }
 
 
 func (s *ProcessingHandlerTestSuite) TestSkipProcessingConfig() {
 func (s *ProcessingHandlerTestSuite) TestSkipProcessingConfig() {
-	config.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")
 	res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png")
 
 
@@ -184,7 +184,7 @@ func (s *ProcessingHandlerTestSuite) TestSkipProcessingPO() {
 }
 }
 
 
 func (s *ProcessingHandlerTestSuite) TestSkipProcessingSameFormat() {
 func (s *ProcessingHandlerTestSuite) TestSkipProcessingSameFormat() {
-	config.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")
 	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() {
 func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithEnforceAvif() {
 	config.AlwaysRasterizeSvg = true
 	config.AlwaysRasterizeSvg = true
-	config.EnforceWebp = true
+	s.Config().Options.EnforceWebp = true
 
 
 	res := s.GET("/unsafe/plain/local:///test1.svg", http.Header{"Accept": []string{"image/webp"}})
 	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() {
 func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgDisabled() {
 	config.AlwaysRasterizeSvg = false
 	config.AlwaysRasterizeSvg = false
-	config.EnforceWebp = true
+	s.Config().Options.EnforceWebp = true
 
 
 	res := s.GET("/unsafe/plain/local:///test1.svg")
 	res := s.GET("/unsafe/plain/local:///test1.svg")
 
 
@@ -497,7 +497,7 @@ func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgDisabled() {
 
 
 func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithFormat() {
 func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithFormat() {
 	config.AlwaysRasterizeSvg = true
 	config.AlwaysRasterizeSvg = true
-	config.SkipProcessingFormats = []imagetype.Type{imagetype.SVG}
+	s.Config().Options.SkipProcessingFormats = []imagetype.Type{imagetype.SVG}
 
 
 	res := s.GET("/unsafe/plain/local:///test1.svg@svg")
 	res := s.GET("/unsafe/plain/local:///test1.svg@svg")
 
 
@@ -506,7 +506,7 @@ func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithFormat() {
 }
 }
 
 
 func (s *ProcessingHandlerTestSuite) TestMaxSrcFileSizeGlobal() {
 func (s *ProcessingHandlerTestSuite) TestMaxSrcFileSizeGlobal() {
-	config.MaxSrcFileSize = 1
+	s.Config().Security.DefaultOptions.MaxSrcFileSize = 1
 
 
 	ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 	ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 		rw.WriteHeader(200)
 		rw.WriteHeader(200)

+ 168 - 0
options/config.go

@@ -0,0 +1,168 @@
+package options
+
+import (
+	"errors"
+	"fmt"
+	"maps"
+	"slices"
+
+	"github.com/imgproxy/imgproxy/v3/config"
+	"github.com/imgproxy/imgproxy/v3/ensure"
+	"github.com/imgproxy/imgproxy/v3/imagetype"
+)
+
+// URLReplacement represents a URL replacement configuration
+type URLReplacement = config.URLReplacement
+
+// Config represents the configuration for options processing
+type Config struct {
+	// Processing behavior defaults
+	StripMetadata     bool // Whether to strip metadata by default
+	KeepCopyright     bool // Whether to keep copyright information when stripping metadata
+	StripColorProfile bool // Whether to strip color profile by default
+	AutoRotate        bool // Whether to auto-rotate images by default
+	EnforceThumbnail  bool // Whether to enforce thumbnail extraction by default
+	ReturnAttachment  bool // Whether to return images as attachments by default
+
+	// Image processing formats
+	SkipProcessingFormats []imagetype.Type // List of formats to skip processing for
+
+	// Presets configuration
+	Presets     []string // Available presets
+	OnlyPresets bool     // Whether to allow only presets
+
+	// Quality settings
+	Quality       int                    // Default quality for image processing
+	FormatQuality map[imagetype.Type]int // Quality settings per image format
+
+	// Security and validation
+	AllowedProcessingOptions []string // List of allowed processing options
+
+	// Format preference and enforcement
+	AutoWebp    bool // Whether to automatically serve WebP when supported
+	EnforceWebp bool // Whether to enforce WebP format
+	AutoAvif    bool // Whether to automatically serve AVIF when supported
+	EnforceAvif bool // Whether to enforce AVIF format
+	AutoJxl     bool // Whether to automatically serve JXL when supported
+	EnforceJxl  bool // Whether to enforce JXL format
+
+	// Client hints
+	EnableClientHints bool // Whether to enable client hints support
+
+	// URL processing
+	ArgumentsSeparator        string           // Separator for URL arguments
+	BaseURL                   string           // Base URL for relative URLs
+	URLReplacements           []URLReplacement // URL replacement rules
+	Base64URLIncludesFilename bool             // Whether base64 URLs include filename
+}
+
+// NewDefaultConfig creates a new default configuration for options processing
+func NewDefaultConfig() Config {
+	return Config{
+		// Processing behavior defaults (copied from global config defaults)
+		StripMetadata:     true,
+		KeepCopyright:     true,
+		StripColorProfile: true,
+		AutoRotate:        true,
+		EnforceThumbnail:  false,
+		ReturnAttachment:  false,
+
+		OnlyPresets: false,
+
+		// Quality settings (copied from global config defaults)
+		Quality: 80,
+		FormatQuality: map[imagetype.Type]int{
+			imagetype.WEBP: 79,
+			imagetype.AVIF: 63,
+			imagetype.JXL:  77,
+		},
+
+		// Format preference and enforcement (copied from global config defaults)
+		AutoWebp:    false,
+		EnforceWebp: false,
+		AutoAvif:    false,
+		EnforceAvif: false,
+		AutoJxl:     false,
+		EnforceJxl:  false,
+
+		// Client hints
+		EnableClientHints: false,
+
+		// URL processing (copied from global config defaults)
+		ArgumentsSeparator:        ":",
+		BaseURL:                   "",
+		Base64URLIncludesFilename: false,
+	}
+}
+
+// LoadConfigFromEnv loads configuration from global config variables
+func LoadConfigFromEnv(c *Config) (*Config, error) {
+	c = ensure.Ensure(c, NewDefaultConfig)
+
+	// Copy from global config variables
+	c.StripMetadata = config.StripMetadata
+	c.KeepCopyright = config.KeepCopyright
+	c.StripColorProfile = config.StripColorProfile
+	c.AutoRotate = config.AutoRotate
+	c.EnforceThumbnail = config.EnforceThumbnail
+	c.ReturnAttachment = config.ReturnAttachment
+
+	// Image processing formats
+	c.SkipProcessingFormats = slices.Clone(config.SkipProcessingFormats)
+
+	// Presets configuration
+	c.Presets = slices.Clone(config.Presets)
+	c.OnlyPresets = config.OnlyPresets
+
+	// Quality settings
+	c.Quality = config.Quality
+	c.FormatQuality = maps.Clone(config.FormatQuality)
+
+	// Security and validation
+	c.AllowedProcessingOptions = slices.Clone(config.AllowedProcessingOptions)
+
+	// Format preference and enforcement
+	c.AutoWebp = config.AutoWebp
+	c.EnforceWebp = config.EnforceWebp
+	c.AutoAvif = config.AutoAvif
+	c.EnforceAvif = config.EnforceAvif
+	c.AutoJxl = config.AutoJxl
+	c.EnforceJxl = config.EnforceJxl
+
+	// Client hints
+	c.EnableClientHints = config.EnableClientHints
+
+	// URL processing
+	c.ArgumentsSeparator = config.ArgumentsSeparator
+	c.BaseURL = config.BaseURL
+	c.URLReplacements = slices.Clone(config.URLReplacements)
+	c.Base64URLIncludesFilename = config.Base64URLIncludesFilename
+
+	return c, nil
+}
+
+// Validate validates the configuration values
+func (c *Config) Validate() error {
+	// Quality validation (copied from global config validation)
+	if c.Quality <= 0 {
+		return fmt.Errorf("quality should be greater than 0, now - %d", c.Quality)
+	} else if c.Quality > 100 {
+		return fmt.Errorf("quality can't be greater than 100, now - %d", c.Quality)
+	}
+
+	// Format quality validation
+	for format, quality := range c.FormatQuality {
+		if quality <= 0 {
+			return fmt.Errorf("format quality for %s should be greater than 0, now - %d", format, quality)
+		} else if quality > 100 {
+			return fmt.Errorf("format quality for %s can't be greater than 100, now - %d", format, quality)
+		}
+	}
+
+	// Arguments separator validation
+	if c.ArgumentsSeparator == "" {
+		return errors.New("arguments separator cannot be empty")
+	}
+
+	return nil
+}

+ 45 - 0
options/factory.go

@@ -0,0 +1,45 @@
+package options
+
+import (
+	"github.com/imgproxy/imgproxy/v3/security"
+)
+
+// Presets is a map of preset names to their corresponding urlOptions
+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
+	defaultPO *ProcessingOptions // Default processing options
+}
+
+// NewFactory creates new Factory instance
+func NewFactory(config *Config, security *security.Checker) (*Factory, error) {
+	if err := config.Validate(); err != nil {
+		return nil, err
+	}
+
+	f := &Factory{
+		config:    config,
+		security:  security,
+		presets:   make(map[string]urlOptions),
+		defaultPO: newDefaultProcessingOptions(config, security),
+	}
+
+	if err := f.parsePresets(); err != nil {
+		return nil, err
+	}
+
+	if err := f.validatePresets(); err != nil {
+		return nil, err
+	}
+
+	return f, nil
+}
+
+// NewProcessingOptions creates new ProcessingOptions instance
+func (f *Factory) NewProcessingOptions() *ProcessingOptions {
+	return f.defaultPO.clone()
+}

+ 20 - 18
options/presets.go

@@ -5,11 +5,10 @@ import (
 	"strings"
 	"strings"
 )
 )
 
 
-var presets map[string]urlOptions
-
-func ParsePresets(presetStrs []string) error {
-	for _, presetStr := range presetStrs {
-		if err := parsePreset(presetStr); err != nil {
+// parsePresets parses presets from the config and fills the presets map
+func (f *Factory) parsePresets() error {
+	for _, presetStr := range f.config.Presets {
+		if err := f.parsePreset(presetStr); err != nil {
 			return err
 			return err
 		}
 		}
 	}
 	}
@@ -17,7 +16,8 @@ func ParsePresets(presetStrs []string) error {
 	return nil
 	return nil
 }
 }
 
 
-func parsePreset(presetStr string) error {
+// parsePreset parses a preset string and returns the name and options
+func (f *Factory) parsePreset(presetStr string) error {
 	presetStr = strings.Trim(presetStr, " ")
 	presetStr = strings.Trim(presetStr, " ")
 
 
 	if len(presetStr) == 0 || strings.HasPrefix(presetStr, "#") {
 	if len(presetStr) == 0 || strings.HasPrefix(presetStr, "#") {
@@ -27,39 +27,41 @@ func parsePreset(presetStr string) error {
 	parts := strings.Split(presetStr, "=")
 	parts := strings.Split(presetStr, "=")
 
 
 	if len(parts) != 2 {
 	if len(parts) != 2 {
-		return fmt.Errorf("Invalid preset string: %s", presetStr)
+		return fmt.Errorf("invalid preset string: %s", presetStr)
 	}
 	}
 
 
 	name := strings.Trim(parts[0], " ")
 	name := strings.Trim(parts[0], " ")
 	if len(name) == 0 {
 	if len(name) == 0 {
-		return fmt.Errorf("Empty preset name: %s", presetStr)
+		return fmt.Errorf("empty preset name: %s", presetStr)
 	}
 	}
 
 
 	value := strings.Trim(parts[1], " ")
 	value := strings.Trim(parts[1], " ")
 	if len(value) == 0 {
 	if len(value) == 0 {
-		return fmt.Errorf("Empty preset value: %s", presetStr)
+		return fmt.Errorf("empty preset value: %s", presetStr)
 	}
 	}
 
 
 	optsStr := strings.Split(value, "/")
 	optsStr := strings.Split(value, "/")
 
 
-	opts, rest := parseURLOptions(optsStr)
+	opts, rest := f.parseURLOptions(optsStr)
 
 
 	if len(rest) > 0 {
 	if len(rest) > 0 {
-		return fmt.Errorf("Invalid preset value: %s", presetStr)
+		return fmt.Errorf("invalid preset value: %s", presetStr)
 	}
 	}
 
 
-	if presets == nil {
-		presets = make(map[string]urlOptions)
+	if f.presets == nil {
+		f.presets = make(Presets)
 	}
 	}
-	presets[name] = opts
+
+	f.presets[name] = opts
 
 
 	return nil
 	return nil
 }
 }
 
 
-func ValidatePresets() error {
-	for name, opts := range presets {
-		po := NewProcessingOptions()
-		if err := applyURLOptions(po, opts, true, name); err != 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 fmt.Errorf("Error in preset `%s`: %s", name, err)
 		}
 		}
 	}
 	}

+ 35 - 43
options/presets_test.go

@@ -1,101 +1,93 @@
 package options
 package options
 
 
 import (
 import (
-	"fmt"
 	"testing"
 	"testing"
 
 
+	"github.com/imgproxy/imgproxy/v3/security"
+	"github.com/imgproxy/imgproxy/v3/testutil"
 	"github.com/stretchr/testify/suite"
 	"github.com/stretchr/testify/suite"
-
-	"github.com/imgproxy/imgproxy/v3/config"
 )
 )
 
 
-type PresetsTestSuite struct{ suite.Suite }
+type PresetsTestSuite struct {
+	testutil.LazySuite
+
+	security *security.Checker
+}
+
+func (s *PresetsTestSuite) SetupSuite() {
+	c := security.NewDefaultConfig()
+	security, err := security.New(&c)
+	s.Require().NoError(err)
+	s.security = security
+}
 
 
-func (s *PresetsTestSuite) SetupTest() {
-	config.Reset()
-	// Reset presets
-	presets = make(map[string]urlOptions)
+func (s *PresetsTestSuite) newFactory(presets ...string) (*Factory, error) {
+	c := NewDefaultConfig()
+	c.Presets = presets
+	return NewFactory(&c, s.security)
 }
 }
 
 
 func (s *PresetsTestSuite) TestParsePreset() {
 func (s *PresetsTestSuite) TestParsePreset() {
-	err := parsePreset("test=resize:fit:100:200/sharpen:2")
+	f, err := s.newFactory("test=resize:fit:100:200/sharpen:2")
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
-
 	s.Require().Equal(urlOptions{
 	s.Require().Equal(urlOptions{
 		urlOption{Name: "resize", Args: []string{"fit", "100", "200"}},
 		urlOption{Name: "resize", Args: []string{"fit", "100", "200"}},
 		urlOption{Name: "sharpen", Args: []string{"2"}},
 		urlOption{Name: "sharpen", Args: []string{"2"}},
-	}, presets["test"])
+	}, f.presets["test"])
 }
 }
 
 
 func (s *PresetsTestSuite) TestParsePresetInvalidString() {
 func (s *PresetsTestSuite) TestParsePresetInvalidString() {
 	presetStr := "resize:fit:100:200/sharpen:2"
 	presetStr := "resize:fit:100:200/sharpen:2"
-	err := parsePreset(presetStr)
+	_, err := s.newFactory(presetStr)
 
 
-	s.Require().Equal(fmt.Errorf("Invalid preset string: %s", presetStr), err)
-	s.Require().Empty(presets)
+	s.Require().Error(err, "invalid preset string: %s", presetStr)
 }
 }
 
 
 func (s *PresetsTestSuite) TestParsePresetEmptyName() {
 func (s *PresetsTestSuite) TestParsePresetEmptyName() {
 	presetStr := "=resize:fit:100:200/sharpen:2"
 	presetStr := "=resize:fit:100:200/sharpen:2"
-	err := parsePreset(presetStr)
+	_, err := s.newFactory(presetStr)
 
 
-	s.Require().Equal(fmt.Errorf("Empty preset name: %s", presetStr), err)
-	s.Require().Empty(presets)
+	s.Require().Error(err, "empty preset name: %s", presetStr)
 }
 }
 
 
 func (s *PresetsTestSuite) TestParsePresetEmptyValue() {
 func (s *PresetsTestSuite) TestParsePresetEmptyValue() {
 	presetStr := "test="
 	presetStr := "test="
-	err := parsePreset(presetStr)
+	_, err := s.newFactory(presetStr)
 
 
-	s.Require().Equal(fmt.Errorf("Empty preset value: %s", presetStr), err)
-	s.Require().Empty(presets)
+	s.Require().Error(err, "empty preset value: %s", presetStr)
 }
 }
 
 
 func (s *PresetsTestSuite) TestParsePresetInvalidValue() {
 func (s *PresetsTestSuite) TestParsePresetInvalidValue() {
 	presetStr := "test=resize:fit:100:200/sharpen:2/blur"
 	presetStr := "test=resize:fit:100:200/sharpen:2/blur"
-	err := parsePreset(presetStr)
+	_, err := s.newFactory(presetStr)
 
 
-	s.Require().Equal(fmt.Errorf("Invalid preset value: %s", presetStr), err)
-	s.Require().Empty(presets)
+	s.Require().Error(err, "invalid preset value: %s", presetStr)
 }
 }
 
 
 func (s *PresetsTestSuite) TestParsePresetEmptyString() {
 func (s *PresetsTestSuite) TestParsePresetEmptyString() {
-	err := parsePreset("  ")
+	f, err := s.newFactory("   ")
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
-	s.Require().Empty(presets)
+	s.Require().Empty(f.presets)
 }
 }
 
 
 func (s *PresetsTestSuite) TestParsePresetComment() {
 func (s *PresetsTestSuite) TestParsePresetComment() {
-	err := parsePreset("#  test=resize:fit:100:200/sharpen:2")
+	f, err := s.newFactory("#  test=resize:fit:100:200/sharpen:2")
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
-	s.Require().Empty(presets)
+	s.Require().Empty(f.presets)
 }
 }
 
 
 func (s *PresetsTestSuite) TestValidatePresets() {
 func (s *PresetsTestSuite) TestValidatePresets() {
-	presets = map[string]urlOptions{
-		"test": {
-			urlOption{Name: "resize", Args: []string{"fit", "100", "200"}},
-			urlOption{Name: "sharpen", Args: []string{"2"}},
-		},
-	}
-
-	err := ValidatePresets()
+	f, err := s.newFactory("test=resize:fit:100:200/sharpen:2")
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
+	s.Require().NotEmpty(f.presets)
 }
 }
 
 
 func (s *PresetsTestSuite) TestValidatePresetsInvalid() {
 func (s *PresetsTestSuite) TestValidatePresetsInvalid() {
-	presets = map[string]urlOptions{
-		"test": {
-			urlOption{Name: "resize", Args: []string{"fit", "-1", "-2"}},
-			urlOption{Name: "sharpen", Args: []string{"2"}},
-		},
-	}
-
-	err := ValidatePresets()
+	_, err := s.newFactory("test=resize:fit:-1:-2/sharpen:2")
 
 
 	s.Require().Error(err)
 	s.Require().Error(err)
 }
 }

+ 91 - 94
options/processing_options.go

@@ -2,7 +2,7 @@ package options
 
 
 import (
 import (
 	"encoding/base64"
 	"encoding/base64"
-	"fmt"
+	"maps"
 	"net/http"
 	"net/http"
 	"slices"
 	"slices"
 	"strconv"
 	"strconv"
@@ -11,7 +11,6 @@ import (
 
 
 	log "github.com/sirupsen/logrus"
 	log "github.com/sirupsen/logrus"
 
 
-	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
 	"github.com/imgproxy/imgproxy/v3/imath"
 	"github.com/imgproxy/imgproxy/v3/imath"
@@ -118,34 +117,10 @@ type ProcessingOptions struct {
 	SecurityOptions security.Options
 	SecurityOptions security.Options
 
 
 	defaultQuality int
 	defaultQuality int
+	defaultOptions *ProcessingOptions
 }
 }
 
 
-func NewProcessingOptions() *ProcessingOptions {
-	// NOTE: This is temporary hack until ProcessingOptions does not have Factory
-	securityCfg, err := security.LoadConfigFromEnv(nil)
-	if err != nil {
-		fmt.Println(err)
-	}
-
-	// NOTE: This is a temporary workaround for logrus bug that deadlocks
-	// if log is used within another log (issue 1448)
-	if len(securityCfg.Salts) == 0 {
-		securityCfg.Salts = [][]byte{[]byte("logrusbugworkaround")}
-	}
-
-	if len(securityCfg.Keys) == 0 {
-		securityCfg.Keys = [][]byte{[]byte("logrusbugworkaround")}
-	}
-	// END OF WORKAROUND
-
-	security, err := security.New(securityCfg)
-	if err != nil {
-		fmt.Println(err)
-	}
-
-	securityOptions := security.NewOptions()
-	// NOTE: This is temporary hack until ProcessingOptions does not have Factory
-
+func newDefaultProcessingOptions(config *Config, security *security.Checker) *ProcessingOptions {
 	po := ProcessingOptions{
 	po := ProcessingOptions{
 		ResizingType:      ResizeFit,
 		ResizingType:      ResizeFit,
 		Width:             0,
 		Width:             0,
@@ -160,6 +135,7 @@ func NewProcessingOptions() *ProcessingOptions {
 		Trim:              TrimOptions{Enabled: false, Threshold: 10, Smart: true},
 		Trim:              TrimOptions{Enabled: false, Threshold: 10, Smart: true},
 		Rotate:            0,
 		Rotate:            0,
 		Quality:           0,
 		Quality:           0,
+		FormatQuality:     maps.Clone(config.FormatQuality),
 		MaxBytes:          0,
 		MaxBytes:          0,
 		Format:            imagetype.Unknown,
 		Format:            imagetype.Unknown,
 		Background:        vips.Color{R: 255, G: 255, B: 255},
 		Background:        vips.Color{R: 255, G: 255, B: 255},
@@ -174,20 +150,13 @@ func NewProcessingOptions() *ProcessingOptions {
 		EnforceThumbnail:  config.EnforceThumbnail,
 		EnforceThumbnail:  config.EnforceThumbnail,
 		ReturnAttachment:  config.ReturnAttachment,
 		ReturnAttachment:  config.ReturnAttachment,
 
 
-		SkipProcessingFormats: append([]imagetype.Type(nil), config.SkipProcessingFormats...),
-		UsedPresets:           make([]string, 0, len(config.Presets)),
+		SkipProcessingFormats: slices.Clone(config.SkipProcessingFormats),
 
 
-		SecurityOptions: securityOptions,
+		SecurityOptions: security.NewOptions(),
 
 
-		// Basically, we need this to update ETag when `IMGPROXY_QUALITY` is changed
 		defaultQuality: config.Quality,
 		defaultQuality: config.Quality,
 	}
 	}
 
 
-	po.FormatQuality = make(map[imagetype.Type]int, len(config.FormatQuality))
-	for k, v := range config.FormatQuality {
-		po.FormatQuality[k] = v
-	}
-
 	return &po
 	return &po
 }
 }
 
 
@@ -206,7 +175,7 @@ func (po *ProcessingOptions) GetQuality() int {
 }
 }
 
 
 func (po *ProcessingOptions) Diff() structdiff.Entries {
 func (po *ProcessingOptions) Diff() structdiff.Entries {
-	return structdiff.Diff(NewProcessingOptions(), po)
+	return structdiff.Diff(po.defaultOptions, po)
 }
 }
 
 
 func (po *ProcessingOptions) String() string {
 func (po *ProcessingOptions) String() string {
@@ -217,6 +186,34 @@ func (po *ProcessingOptions) MarshalJSON() ([]byte, error) {
 	return po.Diff().MarshalJSON()
 	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 {
 func parseDimension(d *int, name, arg string) error {
 	if v, err := strconv.Atoi(arg); err == nil && v >= 0 {
 	if v, err := strconv.Atoi(arg); err == nil && v >= 0 {
 		*d = v
 		*d = v
@@ -707,9 +704,9 @@ func applyPixelateOption(po *ProcessingOptions, args []string) error {
 	return nil
 	return nil
 }
 }
 
 
-func applyPresetOption(po *ProcessingOptions, args []string, usedPresets ...string) error {
+func (f *Factory) applyPresetOption(po *ProcessingOptions, args []string, usedPresets ...string) error {
 	for _, preset := range args {
 	for _, preset := range args {
-		if p, ok := presets[preset]; ok {
+		if p, ok := f.presets[preset]; ok {
 			if slices.Contains(usedPresets, preset) {
 			if slices.Contains(usedPresets, preset) {
 				log.Warningf("Recursive preset usage is detected: %s", preset)
 				log.Warningf("Recursive preset usage is detected: %s", preset)
 				continue
 				continue
@@ -717,7 +714,7 @@ func applyPresetOption(po *ProcessingOptions, args []string, usedPresets ...stri
 
 
 			po.UsedPresets = append(po.UsedPresets, preset)
 			po.UsedPresets = append(po.UsedPresets, preset)
 
 
-			if err := applyURLOptions(po, p, true, append(usedPresets, preset)...); err != nil {
+			if err := f.applyURLOptions(po, p, true, append(usedPresets, preset)...); err != nil {
 				return err
 				return err
 			}
 			}
 		} else {
 		} else {
@@ -1010,7 +1007,7 @@ func applyMaxResultDimensionOption(po *ProcessingOptions, args []string) error {
 	return nil
 	return nil
 }
 }
 
 
-func applyURLOption(po *ProcessingOptions, name string, args []string, usedPresets ...string) error {
+func (f *Factory) applyURLOption(po *ProcessingOptions, name string, args []string, usedPresets ...string) error {
 	switch name {
 	switch name {
 	case "resize", "rs":
 	case "resize", "rs":
 		return applyResizeOption(po, args)
 		return applyResizeOption(po, args)
@@ -1090,7 +1087,7 @@ func applyURLOption(po *ProcessingOptions, name string, args []string, usedPrese
 		return applyReturnAttachmentOption(po, args)
 		return applyReturnAttachmentOption(po, args)
 	// Presets
 	// Presets
 	case "preset", "pr":
 	case "preset", "pr":
-		return applyPresetOption(po, args, usedPresets...)
+		return f.applyPresetOption(po, args, usedPresets...)
 	// Security
 	// Security
 	case "max_src_resolution", "msr":
 	case "max_src_resolution", "msr":
 		return applyMaxSrcResolutionOption(po, args)
 		return applyMaxSrcResolutionOption(po, args)
@@ -1107,15 +1104,15 @@ func applyURLOption(po *ProcessingOptions, name string, args []string, usedPrese
 	return newUnknownOptionError("processing", name)
 	return newUnknownOptionError("processing", name)
 }
 }
 
 
-func applyURLOptions(po *ProcessingOptions, options urlOptions, allowAll bool, usedPresets ...string) error {
-	allowAll = allowAll || len(config.AllowedProcessingOptions) == 0
+func (f *Factory) applyURLOptions(po *ProcessingOptions, options urlOptions, allowAll bool, usedPresets ...string) error {
+	allowAll = allowAll || len(f.config.AllowedProcessingOptions) == 0
 
 
 	for _, opt := range options {
 	for _, opt := range options {
-		if !allowAll && !slices.Contains(config.AllowedProcessingOptions, opt.Name) {
+		if !allowAll && !slices.Contains(f.config.AllowedProcessingOptions, opt.Name) {
 			return newForbiddenOptionError("processing", opt.Name)
 			return newForbiddenOptionError("processing", opt.Name)
 		}
 		}
 
 
-		if err := applyURLOption(po, opt.Name, opt.Args, usedPresets...); err != nil {
+		if err := f.applyURLOption(po, opt.Name, opt.Args, usedPresets...); err != nil {
 			return err
 			return err
 		}
 		}
 	}
 	}
@@ -1123,27 +1120,27 @@ func applyURLOptions(po *ProcessingOptions, options urlOptions, allowAll bool, u
 	return nil
 	return nil
 }
 }
 
 
-func defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
-	po := NewProcessingOptions()
+func (f *Factory) defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
+	po := f.NewProcessingOptions()
 
 
 	headerAccept := headers.Get("Accept")
 	headerAccept := headers.Get("Accept")
 
 
 	if strings.Contains(headerAccept, "image/webp") {
 	if strings.Contains(headerAccept, "image/webp") {
-		po.PreferWebP = config.AutoWebp || config.EnforceWebp
-		po.EnforceWebP = config.EnforceWebp
+		po.PreferWebP = f.config.AutoWebp || f.config.EnforceWebp
+		po.EnforceWebP = f.config.EnforceWebp
 	}
 	}
 
 
 	if strings.Contains(headerAccept, "image/avif") {
 	if strings.Contains(headerAccept, "image/avif") {
-		po.PreferAvif = config.AutoAvif || config.EnforceAvif
-		po.EnforceAvif = config.EnforceAvif
+		po.PreferAvif = f.config.AutoAvif || f.config.EnforceAvif
+		po.EnforceAvif = f.config.EnforceAvif
 	}
 	}
 
 
 	if strings.Contains(headerAccept, "image/jxl") {
 	if strings.Contains(headerAccept, "image/jxl") {
-		po.PreferJxl = config.AutoJxl || config.EnforceJxl
-		po.EnforceJxl = config.EnforceJxl
+		po.PreferJxl = f.config.AutoJxl || f.config.EnforceJxl
+		po.EnforceJxl = f.config.EnforceJxl
 	}
 	}
 
 
-	if config.EnableClientHints {
+	if f.config.EnableClientHints {
 		headerDPR := headers.Get("Sec-CH-DPR")
 		headerDPR := headers.Get("Sec-CH-DPR")
 		if len(headerDPR) == 0 {
 		if len(headerDPR) == 0 {
 			headerDPR = headers.Get("DPR")
 			headerDPR = headers.Get("DPR")
@@ -1165,8 +1162,8 @@ func defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
 		}
 		}
 	}
 	}
 
 
-	if _, ok := presets["default"]; ok {
-		if err := applyPresetOption(po, []string{"default"}); err != nil {
+	if _, ok := f.presets["default"]; ok {
+		if err := f.applyPresetOption(po, []string{"default"}); err != nil {
 			return po, err
 			return po, err
 		}
 		}
 	}
 	}
@@ -1174,23 +1171,48 @@ func defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
 	return po, nil
 	return po, nil
 }
 }
 
 
-func parsePathOptions(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
+// ParsePath parses the given request path and returns the processing options and image URL
+func (f *Factory) ParsePath(
+	path string,
+	headers http.Header,
+) (po *ProcessingOptions, imageURL string, err error) {
+	if path == "" || path == "/" {
+		return nil, "", newInvalidURLError("invalid path: %s", path)
+	}
+
+	parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
+
+	if f.config.OnlyPresets {
+		po, imageURL, err = f.parsePathPresets(parts, headers)
+	} else {
+		po, imageURL, err = f.parsePathOptions(parts, headers)
+	}
+
+	if err != nil {
+		return nil, "", ierrors.Wrap(err, 0)
+	}
+
+	return po, imageURL, nil
+}
+
+// parsePathOptions parses processing options from the URL path
+func (f *Factory) parsePathOptions(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
 	if _, ok := resizeTypes[parts[0]]; ok {
 	if _, ok := resizeTypes[parts[0]]; ok {
 		return nil, "", newInvalidURLError("It looks like you're using the deprecated basic URL format")
 		return nil, "", newInvalidURLError("It looks like you're using the deprecated basic URL format")
 	}
 	}
 
 
-	po, err := defaultProcessingOptions(headers)
+	po, err := f.defaultProcessingOptions(headers)
 	if err != nil {
 	if err != nil {
 		return nil, "", err
 		return nil, "", err
 	}
 	}
 
 
-	options, urlParts := parseURLOptions(parts)
+	options, urlParts := f.parseURLOptions(parts)
 
 
-	if err = applyURLOptions(po, options, false); err != nil {
+	if err = f.applyURLOptions(po, options, false); err != nil {
 		return nil, "", err
 		return nil, "", err
 	}
 	}
 
 
-	url, extension, err := DecodeURL(urlParts)
+	url, extension, err := f.DecodeURL(urlParts)
 	if err != nil {
 	if err != nil {
 		return nil, "", err
 		return nil, "", err
 	}
 	}
@@ -1204,20 +1226,21 @@ func parsePathOptions(parts []string, headers http.Header) (*ProcessingOptions,
 	return po, url, nil
 	return po, url, nil
 }
 }
 
 
-func parsePathPresets(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
-	po, err := defaultProcessingOptions(headers)
+// parsePathPresets parses presets from the URL path
+func (f *Factory) parsePathPresets(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
+	po, err := f.defaultProcessingOptions(headers)
 	if err != nil {
 	if err != nil {
 		return nil, "", err
 		return nil, "", err
 	}
 	}
 
 
-	presets := strings.Split(parts[0], config.ArgumentsSeparator)
+	presets := strings.Split(parts[0], f.config.ArgumentsSeparator)
 	urlParts := parts[1:]
 	urlParts := parts[1:]
 
 
-	if err = applyPresetOption(po, presets); err != nil {
+	if err = f.applyPresetOption(po, presets); err != nil {
 		return nil, "", err
 		return nil, "", err
 	}
 	}
 
 
-	url, extension, err := DecodeURL(urlParts)
+	url, extension, err := f.DecodeURL(urlParts)
 	if err != nil {
 	if err != nil {
 		return nil, "", err
 		return nil, "", err
 	}
 	}
@@ -1230,29 +1253,3 @@ func parsePathPresets(parts []string, headers http.Header) (*ProcessingOptions,
 
 
 	return po, url, nil
 	return po, url, nil
 }
 }
-
-func ParsePath(path string, headers http.Header) (*ProcessingOptions, string, error) {
-	if path == "" || path == "/" {
-		return nil, "", newInvalidURLError("Invalid path: %s", path)
-	}
-
-	parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
-
-	var (
-		imageURL string
-		po       *ProcessingOptions
-		err      error
-	)
-
-	if config.OnlyPresets {
-		po, imageURL, err = parsePathPresets(parts, headers)
-	} else {
-		po, imageURL, err = parsePathOptions(parts, headers)
-	}
-
-	if err != nil {
-		return nil, "", ierrors.Wrap(err, 0)
-	}
-
-	return po, imageURL, nil
-}

+ 155 - 132
options/processing_options_test.go

@@ -8,25 +8,65 @@ import (
 	"regexp"
 	"regexp"
 	"strings"
 	"strings"
 	"testing"
 	"testing"
-
-	"github.com/stretchr/testify/suite"
+	"time"
 
 
 	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
+	"github.com/imgproxy/imgproxy/v3/security"
+	"github.com/imgproxy/imgproxy/v3/testutil"
+	"github.com/stretchr/testify/suite"
 )
 )
 
 
-type ProcessingOptionsTestSuite struct{ suite.Suite }
+type ProcessingOptionsTestSuite struct {
+	testutil.LazySuite
+
+	securityCfg testutil.LazyObj[*security.Config]
+	security    testutil.LazyObj[*security.Checker]
+
+	config  testutil.LazyObj[*Config]
+	factory testutil.LazyObj[*Factory]
+}
+
+func (s *ProcessingOptionsTestSuite) SetupSuite() {
+	s.config, _ = testutil.NewLazySuiteObj(
+		s,
+		func() (*Config, error) {
+			c := NewDefaultConfig()
+			return &c, nil
+		},
+	)
+
+	s.securityCfg, _ = testutil.NewLazySuiteObj(
+		s,
+		func() (*security.Config, error) {
+			c := security.NewDefaultConfig()
+			return &c, nil
+		},
+	)
+
+	s.security, _ = testutil.NewLazySuiteObj(
+		s,
+		func() (*security.Checker, error) {
+			return security.New(s.securityCfg())
+		},
+	)
 
 
-func (s *ProcessingOptionsTestSuite) SetupTest() {
-	config.Reset()
-	// Reset presets
-	presets = make(map[string]urlOptions)
+	s.factory, _ = testutil.NewLazySuiteObj(
+		s,
+		func() (*Factory, error) {
+			return NewFactory(s.config(), s.security())
+		},
+	)
+}
+
+func (s *ProcessingOptionsTestSuite) SetupSubTest() {
+	s.ResetLazyObjects()
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParseBase64URL() {
 func (s *ProcessingOptionsTestSuite) TestParseBase64URL() {
 	originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
 	originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
 	path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
 	path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
-	po, imageURL, err := ParsePath(path, make(http.Header))
+	po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 	s.Require().Equal(originURL, imageURL)
 	s.Require().Equal(originURL, imageURL)
@@ -34,11 +74,11 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URL() {
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithFilename() {
 func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithFilename() {
-	config.Base64URLIncludesFilename = true
+	s.config().Base64URLIncludesFilename = true
 
 
 	originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
 	originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
 	path := fmt.Sprintf("/size:100:100/%s.png/puppy.jpg", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
 	path := fmt.Sprintf("/size:100:100/%s.png/puppy.jpg", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
-	po, imageURL, err := ParsePath(path, make(http.Header))
+	po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 	s.Require().Equal(originURL, imageURL)
 	s.Require().Equal(originURL, imageURL)
@@ -48,7 +88,7 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithFilename() {
 func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithoutExtension() {
 func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithoutExtension() {
 	originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
 	originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
 	path := fmt.Sprintf("/size:100:100/%s", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
 	path := fmt.Sprintf("/size:100:100/%s", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
-	po, imageURL, err := ParsePath(path, make(http.Header))
+	po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 	s.Require().Equal(originURL, imageURL)
 	s.Require().Equal(originURL, imageURL)
@@ -56,26 +96,26 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithoutExtension() {
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithBase() {
 func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithBase() {
-	config.BaseURL = "http://images.dev/"
+	s.config().BaseURL = "http://images.dev/"
 
 
 	originURL := "lorem/ipsum.jpg?param=value"
 	originURL := "lorem/ipsum.jpg?param=value"
 	path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
 	path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
-	po, imageURL, err := ParsePath(path, make(http.Header))
+	po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
-	s.Require().Equal(fmt.Sprintf("%s%s", config.BaseURL, originURL), imageURL)
+	s.Require().Equal(fmt.Sprintf("%s%s", s.config().BaseURL, originURL), imageURL)
 	s.Require().Equal(imagetype.PNG, po.Format)
 	s.Require().Equal(imagetype.PNG, po.Format)
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithReplacement() {
 func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithReplacement() {
-	config.URLReplacements = []config.URLReplacement{
+	s.config().URLReplacements = []config.URLReplacement{
 		{Regexp: regexp.MustCompile("^test://([^/]*)/"), Replacement: "test2://images.dev/${1}/dolor/"},
 		{Regexp: regexp.MustCompile("^test://([^/]*)/"), Replacement: "test2://images.dev/${1}/dolor/"},
 		{Regexp: regexp.MustCompile("^test2://"), Replacement: "http://"},
 		{Regexp: regexp.MustCompile("^test2://"), Replacement: "http://"},
 	}
 	}
 
 
 	originURL := "test://lorem/ipsum.jpg?param=value"
 	originURL := "test://lorem/ipsum.jpg?param=value"
 	path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
 	path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
-	po, imageURL, err := ParsePath(path, make(http.Header))
+	po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 	s.Require().Equal("http://images.dev/lorem/dolor/ipsum.jpg?param=value", imageURL)
 	s.Require().Equal("http://images.dev/lorem/dolor/ipsum.jpg?param=value", imageURL)
@@ -85,7 +125,7 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithReplacement() {
 func (s *ProcessingOptionsTestSuite) TestParsePlainURL() {
 func (s *ProcessingOptionsTestSuite) TestParsePlainURL() {
 	originURL := "http://images.dev/lorem/ipsum.jpg"
 	originURL := "http://images.dev/lorem/ipsum.jpg"
 	path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL)
 	path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL)
-	po, imageURL, err := ParsePath(path, make(http.Header))
+	po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 	s.Require().Equal(originURL, imageURL)
 	s.Require().Equal(originURL, imageURL)
@@ -96,7 +136,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithoutExtension() {
 	originURL := "http://images.dev/lorem/ipsum.jpg"
 	originURL := "http://images.dev/lorem/ipsum.jpg"
 	path := fmt.Sprintf("/size:100:100/plain/%s", originURL)
 	path := fmt.Sprintf("/size:100:100/plain/%s", originURL)
 
 
-	po, imageURL, err := ParsePath(path, make(http.Header))
+	po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 	s.Require().Equal(originURL, imageURL)
 	s.Require().Equal(originURL, imageURL)
@@ -105,7 +145,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithoutExtension() {
 func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscaped() {
 func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscaped() {
 	originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
 	originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
 	path := fmt.Sprintf("/size:100:100/plain/%s@png", url.PathEscape(originURL))
 	path := fmt.Sprintf("/size:100:100/plain/%s@png", url.PathEscape(originURL))
-	po, imageURL, err := ParsePath(path, make(http.Header))
+	po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 	s.Require().Equal(originURL, imageURL)
 	s.Require().Equal(originURL, imageURL)
@@ -113,26 +153,26 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscaped() {
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithBase() {
 func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithBase() {
-	config.BaseURL = "http://images.dev/"
+	s.config().BaseURL = "http://images.dev/"
 
 
 	originURL := "lorem/ipsum.jpg"
 	originURL := "lorem/ipsum.jpg"
 	path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL)
 	path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL)
-	po, imageURL, err := ParsePath(path, make(http.Header))
+	po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
-	s.Require().Equal(fmt.Sprintf("%s%s", config.BaseURL, originURL), imageURL)
+	s.Require().Equal(fmt.Sprintf("%s%s", s.config().BaseURL, originURL), imageURL)
 	s.Require().Equal(imagetype.PNG, po.Format)
 	s.Require().Equal(imagetype.PNG, po.Format)
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithReplacement() {
 func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithReplacement() {
-	config.URLReplacements = []config.URLReplacement{
+	s.config().URLReplacements = []config.URLReplacement{
 		{Regexp: regexp.MustCompile("^test://([^/]*)/"), Replacement: "test2://images.dev/${1}/dolor/"},
 		{Regexp: regexp.MustCompile("^test://([^/]*)/"), Replacement: "test2://images.dev/${1}/dolor/"},
 		{Regexp: regexp.MustCompile("^test2://"), Replacement: "http://"},
 		{Regexp: regexp.MustCompile("^test2://"), Replacement: "http://"},
 	}
 	}
 
 
 	originURL := "test://lorem/ipsum.jpg"
 	originURL := "test://lorem/ipsum.jpg"
 	path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL)
 	path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL)
-	po, imageURL, err := ParsePath(path, make(http.Header))
+	po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 	s.Require().Equal("http://images.dev/lorem/dolor/ipsum.jpg", imageURL)
 	s.Require().Equal("http://images.dev/lorem/dolor/ipsum.jpg", imageURL)
@@ -140,22 +180,22 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithReplacement() {
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscapedWithBase() {
 func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscapedWithBase() {
-	config.BaseURL = "http://images.dev/"
+	s.config().BaseURL = "http://images.dev/"
 
 
 	originURL := "lorem/ipsum.jpg?param=value"
 	originURL := "lorem/ipsum.jpg?param=value"
 	path := fmt.Sprintf("/size:100:100/plain/%s@png", url.PathEscape(originURL))
 	path := fmt.Sprintf("/size:100:100/plain/%s@png", url.PathEscape(originURL))
-	po, imageURL, err := ParsePath(path, make(http.Header))
+	po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
-	s.Require().Equal(fmt.Sprintf("%s%s", config.BaseURL, originURL), imageURL)
+	s.Require().Equal(fmt.Sprintf("%s%s", s.config().BaseURL, originURL), imageURL)
 	s.Require().Equal(imagetype.PNG, po.Format)
 	s.Require().Equal(imagetype.PNG, po.Format)
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParseWithArgumentsSeparator() {
 func (s *ProcessingOptionsTestSuite) TestParseWithArgumentsSeparator() {
-	config.ArgumentsSeparator = ","
+	s.config().ArgumentsSeparator = ","
 
 
 	path := "/size,100,100,1/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/size,100,100,1/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -164,27 +204,9 @@ func (s *ProcessingOptionsTestSuite) TestParseWithArgumentsSeparator() {
 	s.Require().True(po.Enlarge)
 	s.Require().True(po.Enlarge)
 }
 }
 
 
-// func (s *ProcessingOptionsTestSuite) TestParseURLAllowedSource() {
-// 	config.AllowedSources = []string{"local://", "http://images.dev/"}
-
-// 	path := "/plain/http://images.dev/lorem/ipsum.jpg"
-// 	_, _, err := ParsePath(path, make(http.Header))
-
-// 	s.Require().NoError(err)
-// }
-
-// func (s *ProcessingOptionsTestSuite) TestParseURLNotAllowedSource() {
-// 	config.AllowedSources = []string{"local://", "http://images.dev/"}
-
-// 	path := "/plain/s3://images/lorem/ipsum.jpg"
-// 	_, _, err := ParsePath(path, make(http.Header))
-
-// 	s.Require().Error(err)
-// }
-
 func (s *ProcessingOptionsTestSuite) TestParsePathFormat() {
 func (s *ProcessingOptionsTestSuite) TestParsePathFormat() {
 	path := "/format:webp/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/format:webp/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -193,7 +215,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathFormat() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathResize() {
 func (s *ProcessingOptionsTestSuite) TestParsePathResize() {
 	path := "/resize:fill:100:200:1/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/resize:fill:100:200:1/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -205,7 +227,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathResize() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathResizingType() {
 func (s *ProcessingOptionsTestSuite) TestParsePathResizingType() {
 	path := "/resizing_type:fill/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/resizing_type:fill/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -214,7 +236,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathResizingType() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathSize() {
 func (s *ProcessingOptionsTestSuite) TestParsePathSize() {
 	path := "/size:100:200:1/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/size:100:200:1/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -225,7 +247,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathSize() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathWidth() {
 func (s *ProcessingOptionsTestSuite) TestParsePathWidth() {
 	path := "/width:100/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/width:100/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -234,7 +256,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWidth() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathHeight() {
 func (s *ProcessingOptionsTestSuite) TestParsePathHeight() {
 	path := "/height:100/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/height:100/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -243,7 +265,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathHeight() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathEnlarge() {
 func (s *ProcessingOptionsTestSuite) TestParsePathEnlarge() {
 	path := "/enlarge:1/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/enlarge:1/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -252,7 +274,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathEnlarge() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathExtend() {
 func (s *ProcessingOptionsTestSuite) TestParsePathExtend() {
 	path := "/extend:1:so:10:20/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/extend:1:so:10:20/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -264,21 +286,21 @@ func (s *ProcessingOptionsTestSuite) TestParsePathExtend() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathExtendSmartGravity() {
 func (s *ProcessingOptionsTestSuite) TestParsePathExtendSmartGravity() {
 	path := "/extend:1:sm/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/extend:1:sm/plain/http://images.dev/lorem/ipsum.jpg"
-	_, _, err := ParsePath(path, make(http.Header))
+	_, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().Error(err)
 	s.Require().Error(err)
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathExtendReplicateGravity() {
 func (s *ProcessingOptionsTestSuite) TestParsePathExtendReplicateGravity() {
 	path := "/extend:1:re/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/extend:1:re/plain/http://images.dev/lorem/ipsum.jpg"
-	_, _, err := ParsePath(path, make(http.Header))
+	_, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().Error(err)
 	s.Require().Error(err)
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathGravity() {
 func (s *ProcessingOptionsTestSuite) TestParsePathGravity() {
 	path := "/gravity:soea/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/gravity:soea/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -287,7 +309,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathGravity() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathGravityFocusPoint() {
 func (s *ProcessingOptionsTestSuite) TestParsePathGravityFocusPoint() {
 	path := "/gravity:fp:0.5:0.75/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/gravity:fp:0.5:0.75/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -298,14 +320,14 @@ func (s *ProcessingOptionsTestSuite) TestParsePathGravityFocusPoint() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathGravityReplicate() {
 func (s *ProcessingOptionsTestSuite) TestParsePathGravityReplicate() {
 	path := "/gravity:re/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/gravity:re/plain/http://images.dev/lorem/ipsum.jpg"
-	_, _, err := ParsePath(path, make(http.Header))
+	_, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().Error(err)
 	s.Require().Error(err)
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathCrop() {
 func (s *ProcessingOptionsTestSuite) TestParsePathCrop() {
 	path := "/crop:100:200/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/crop:100:200/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -318,7 +340,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathCrop() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathCropGravity() {
 func (s *ProcessingOptionsTestSuite) TestParsePathCropGravity() {
 	path := "/crop:100:200:nowe:10:20/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/crop:100:200:nowe:10:20/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -331,14 +353,14 @@ func (s *ProcessingOptionsTestSuite) TestParsePathCropGravity() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathCropGravityReplicate() {
 func (s *ProcessingOptionsTestSuite) TestParsePathCropGravityReplicate() {
 	path := "/crop:100:200:re/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/crop:100:200:re/plain/http://images.dev/lorem/ipsum.jpg"
-	_, _, err := ParsePath(path, make(http.Header))
+	_, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().Error(err)
 	s.Require().Error(err)
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathQuality() {
 func (s *ProcessingOptionsTestSuite) TestParsePathQuality() {
 	path := "/quality:55/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/quality:55/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -347,7 +369,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathQuality() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathBackground() {
 func (s *ProcessingOptionsTestSuite) TestParsePathBackground() {
 	path := "/background:128:129:130/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/background:128:129:130/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -359,7 +381,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathBackground() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundHex() {
 func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundHex() {
 	path := "/background:ffddee/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/background:ffddee/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -371,7 +393,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundHex() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundDisable() {
 func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundDisable() {
 	path := "/background:fff/background:/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/background:fff/background:/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -380,7 +402,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundDisable() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathBlur() {
 func (s *ProcessingOptionsTestSuite) TestParsePathBlur() {
 	path := "/blur:0.2/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/blur:0.2/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -389,7 +411,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathBlur() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathSharpen() {
 func (s *ProcessingOptionsTestSuite) TestParsePathSharpen() {
 	path := "/sharpen:0.2/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/sharpen:0.2/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -397,7 +419,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathSharpen() {
 }
 }
 func (s *ProcessingOptionsTestSuite) TestParsePathDpr() {
 func (s *ProcessingOptionsTestSuite) TestParsePathDpr() {
 	path := "/dpr:2/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/dpr:2/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -405,7 +427,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathDpr() {
 }
 }
 func (s *ProcessingOptionsTestSuite) TestParsePathWatermark() {
 func (s *ProcessingOptionsTestSuite) TestParsePathWatermark() {
 	path := "/watermark:0.5:soea:10:20:0.6/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/watermark:0.5:soea:10:20:0.6/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -417,17 +439,13 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWatermark() {
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathPreset() {
 func (s *ProcessingOptionsTestSuite) TestParsePathPreset() {
-	presets["test1"] = urlOptions{
-		urlOption{Name: "resizing_type", Args: []string{"fill"}},
-	}
-
-	presets["test2"] = urlOptions{
-		urlOption{Name: "blur", Args: []string{"0.2"}},
-		urlOption{Name: "quality", Args: []string{"50"}},
+	s.config().Presets = []string{
+		"test1=resizing_type:fill",
+		"test2=blur:0.2/quality:50",
 	}
 	}
 
 
 	path := "/preset:test1:test2/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/preset:test1:test2/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -437,14 +455,12 @@ func (s *ProcessingOptionsTestSuite) TestParsePathPreset() {
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathPresetDefault() {
 func (s *ProcessingOptionsTestSuite) TestParsePathPresetDefault() {
-	presets["default"] = urlOptions{
-		urlOption{Name: "resizing_type", Args: []string{"fill"}},
-		urlOption{Name: "blur", Args: []string{"0.2"}},
-		urlOption{Name: "quality", Args: []string{"50"}},
+	s.config().Presets = []string{
+		"default=resizing_type:fill/blur:0.2/quality:50",
 	}
 	}
 
 
 	path := "/quality:70/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/quality:70/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -454,18 +470,13 @@ func (s *ProcessingOptionsTestSuite) TestParsePathPresetDefault() {
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathPresetLoopDetection() {
 func (s *ProcessingOptionsTestSuite) TestParsePathPresetLoopDetection() {
-	presets["test1"] = urlOptions{
-		urlOption{Name: "resizing_type", Args: []string{"fill"}},
-		urlOption{Name: "preset", Args: []string{"test2"}},
-	}
-
-	presets["test2"] = urlOptions{
-		urlOption{Name: "blur", Args: []string{"0.2"}},
-		urlOption{Name: "preset", Args: []string{"test1"}},
+	s.config().Presets = []string{
+		"test1=resizing_type:fill/preset:test2",
+		"test2=blur:0.2/preset:test1",
 	}
 	}
 
 
 	path := "/preset:test1/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/preset:test1/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -474,7 +485,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathPresetLoopDetection() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathCachebuster() {
 func (s *ProcessingOptionsTestSuite) TestParsePathCachebuster() {
 	path := "/cachebuster:123/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/cachebuster:123/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -483,7 +494,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathCachebuster() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathStripMetadata() {
 func (s *ProcessingOptionsTestSuite) TestParsePathStripMetadata() {
 	path := "/strip_metadata:true/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/strip_metadata:true/plain/http://images.dev/lorem/ipsum.jpg"
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -491,11 +502,11 @@ func (s *ProcessingOptionsTestSuite) TestParsePathStripMetadata() {
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathWebpDetection() {
 func (s *ProcessingOptionsTestSuite) TestParsePathWebpDetection() {
-	config.AutoWebp = true
+	s.config().AutoWebp = true
 
 
 	path := "/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/plain/http://images.dev/lorem/ipsum.jpg"
 	headers := http.Header{"Accept": []string{"image/webp"}}
 	headers := http.Header{"Accept": []string{"image/webp"}}
-	po, _, err := ParsePath(path, headers)
+	po, _, err := s.factory().ParsePath(path, headers)
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -504,11 +515,11 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWebpDetection() {
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathWebpEnforce() {
 func (s *ProcessingOptionsTestSuite) TestParsePathWebpEnforce() {
-	config.EnforceWebp = true
+	s.config().EnforceWebp = true
 
 
 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
 	headers := http.Header{"Accept": []string{"image/webp"}}
 	headers := http.Header{"Accept": []string{"image/webp"}}
-	po, _, err := ParsePath(path, headers)
+	po, _, err := s.factory().ParsePath(path, headers)
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -517,11 +528,11 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWebpEnforce() {
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeader() {
 func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeader() {
-	config.EnableClientHints = true
+	s.config().EnableClientHints = true
 
 
 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
 	headers := http.Header{"Width": []string{"100"}}
 	headers := http.Header{"Width": []string{"100"}}
-	po, _, err := ParsePath(path, headers)
+	po, _, err := s.factory().ParsePath(path, headers)
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -531,7 +542,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeader() {
 func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderDisabled() {
 func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderDisabled() {
 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
 	headers := http.Header{"Width": []string{"100"}}
 	headers := http.Header{"Width": []string{"100"}}
-	po, _, err := ParsePath(path, headers)
+	po, _, err := s.factory().ParsePath(path, headers)
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -539,11 +550,11 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderDisabled() {
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderRedefine() {
 func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderRedefine() {
-	config.EnableClientHints = true
+	s.config().EnableClientHints = true
 
 
 	path := "/width:150/plain/http://images.dev/lorem/ipsum.jpg@png"
 	path := "/width:150/plain/http://images.dev/lorem/ipsum.jpg@png"
 	headers := http.Header{"Width": []string{"100"}}
 	headers := http.Header{"Width": []string{"100"}}
-	po, _, err := ParsePath(path, headers)
+	po, _, err := s.factory().ParsePath(path, headers)
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -551,11 +562,11 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderRedefine() {
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathDprHeader() {
 func (s *ProcessingOptionsTestSuite) TestParsePathDprHeader() {
-	config.EnableClientHints = true
+	s.config().EnableClientHints = true
 
 
 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
 	headers := http.Header{"Dpr": []string{"2"}}
 	headers := http.Header{"Dpr": []string{"2"}}
-	po, _, err := ParsePath(path, headers)
+	po, _, err := s.factory().ParsePath(path, headers)
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -565,7 +576,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathDprHeader() {
 func (s *ProcessingOptionsTestSuite) TestParsePathDprHeaderDisabled() {
 func (s *ProcessingOptionsTestSuite) TestParsePathDprHeaderDisabled() {
 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
 	headers := http.Header{"Dpr": []string{"2"}}
 	headers := http.Header{"Dpr": []string{"2"}}
-	po, _, err := ParsePath(path, headers)
+	po, _, err := s.factory().ParsePath(path, headers)
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -575,7 +586,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathDprHeaderDisabled() {
 func (s *ProcessingOptionsTestSuite) TestParseSkipProcessing() {
 func (s *ProcessingOptionsTestSuite) TestParseSkipProcessing() {
 	path := "/skp:jpg:png/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/skp:jpg:png/plain/http://images.dev/lorem/ipsum.jpg"
 
 
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -585,7 +596,7 @@ func (s *ProcessingOptionsTestSuite) TestParseSkipProcessing() {
 func (s *ProcessingOptionsTestSuite) TestParseSkipProcessingInvalid() {
 func (s *ProcessingOptionsTestSuite) TestParseSkipProcessingInvalid() {
 	path := "/skp:jpg:png:bad_format/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/skp:jpg:png:bad_format/plain/http://images.dev/lorem/ipsum.jpg"
 
 
-	_, _, err := ParsePath(path, make(http.Header))
+	_, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().Error(err)
 	s.Require().Error(err)
 	s.Require().Equal("Invalid image format in skip processing: bad_format", err.Error())
 	s.Require().Equal("Invalid image format in skip processing: bad_format", err.Error())
@@ -593,30 +604,28 @@ func (s *ProcessingOptionsTestSuite) TestParseSkipProcessingInvalid() {
 
 
 func (s *ProcessingOptionsTestSuite) TestParseExpires() {
 func (s *ProcessingOptionsTestSuite) TestParseExpires() {
 	path := "/exp:32503669200/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/exp:32503669200/plain/http://images.dev/lorem/ipsum.jpg"
-	_, _, err := ParsePath(path, make(http.Header))
+	_, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParseExpiresExpired() {
 func (s *ProcessingOptionsTestSuite) TestParseExpiresExpired() {
 	path := "/exp:1609448400/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/exp:1609448400/plain/http://images.dev/lorem/ipsum.jpg"
-	_, _, err := ParsePath(path, make(http.Header))
+	_, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().Error(err, "Expired URL")
 	s.Require().Error(err, "Expired URL")
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParsePathOnlyPresets() {
 func (s *ProcessingOptionsTestSuite) TestParsePathOnlyPresets() {
-	config.OnlyPresets = true
-	presets["test1"] = urlOptions{
-		urlOption{Name: "blur", Args: []string{"0.2"}},
-	}
-	presets["test2"] = urlOptions{
-		urlOption{Name: "quality", Args: []string{"50"}},
+	s.config().OnlyPresets = true
+	s.config().Presets = []string{
+		"test1=blur:0.2",
+		"test2=quality:50",
 	}
 	}
 
 
 	path := "/test1:test2/plain/http://images.dev/lorem/ipsum.jpg"
 	path := "/test1:test2/plain/http://images.dev/lorem/ipsum.jpg"
 
 
-	po, _, err := ParsePath(path, make(http.Header))
+	po, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -625,18 +634,16 @@ func (s *ProcessingOptionsTestSuite) TestParsePathOnlyPresets() {
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParseBase64URLOnlyPresets() {
 func (s *ProcessingOptionsTestSuite) TestParseBase64URLOnlyPresets() {
-	config.OnlyPresets = true
-	presets["test1"] = urlOptions{
-		urlOption{Name: "blur", Args: []string{"0.2"}},
-	}
-	presets["test2"] = urlOptions{
-		urlOption{Name: "quality", Args: []string{"50"}},
+	s.config().OnlyPresets = true
+	s.config().Presets = []string{
+		"test1=blur:0.2",
+		"test2=quality:50",
 	}
 	}
 
 
 	originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
 	originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
 	path := fmt.Sprintf("/test1:test2/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
 	path := fmt.Sprintf("/test1:test2/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
 
 
-	po, imageURL, err := ParsePath(path, make(http.Header))
+	po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
 
 
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
@@ -646,12 +653,6 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLOnlyPresets() {
 }
 }
 
 
 func (s *ProcessingOptionsTestSuite) TestParseAllowedOptions() {
 func (s *ProcessingOptionsTestSuite) TestParseAllowedOptions() {
-	config.AllowedProcessingOptions = []string{"w", "h", "pr"}
-
-	presets["test1"] = urlOptions{
-		urlOption{Name: "blur", Args: []string{"0.2"}},
-	}
-
 	originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
 	originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
 
 
 	testCases := []struct {
 	testCases := []struct {
@@ -666,8 +667,13 @@ func (s *ProcessingOptionsTestSuite) TestParseAllowedOptions() {
 
 
 	for _, tc := range testCases {
 	for _, tc := range testCases {
 		s.Run(strings.ReplaceAll(tc.options, "/", "_"), func() {
 		s.Run(strings.ReplaceAll(tc.options, "/", "_"), func() {
+			s.config().AllowedProcessingOptions = []string{"w", "h", "pr"}
+			s.config().Presets = []string{
+				"test1=blur:0.2",
+			}
+
 			path := fmt.Sprintf("/%s/%s.png", tc.options, base64.RawURLEncoding.EncodeToString([]byte(originURL)))
 			path := fmt.Sprintf("/%s/%s.png", tc.options, base64.RawURLEncoding.EncodeToString([]byte(originURL)))
-			_, _, err := ParsePath(path, make(http.Header))
+			_, _, err := s.factory().ParsePath(path, make(http.Header))
 
 
 			if len(tc.expectedError) > 0 {
 			if len(tc.expectedError) > 0 {
 				s.Require().Error(err)
 				s.Require().Error(err)
@@ -679,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) {
 func TestProcessingOptions(t *testing.T) {
 	suite.Run(t, new(ProcessingOptionsTestSuite))
 	suite.Run(t, new(ProcessingOptionsTestSuite))
 }
 }

+ 12 - 14
options/url.go

@@ -5,28 +5,26 @@ import (
 	"fmt"
 	"fmt"
 	"net/url"
 	"net/url"
 	"strings"
 	"strings"
-
-	"github.com/imgproxy/imgproxy/v3/config"
 )
 )
 
 
 const urlTokenPlain = "plain"
 const urlTokenPlain = "plain"
 
 
-func preprocessURL(u string) string {
-	for _, repl := range config.URLReplacements {
+func (f *Factory) preprocessURL(u string) string {
+	for _, repl := range f.config.URLReplacements {
 		u = repl.Regexp.ReplaceAllString(u, repl.Replacement)
 		u = repl.Regexp.ReplaceAllString(u, repl.Replacement)
 	}
 	}
 
 
-	if len(config.BaseURL) == 0 || strings.HasPrefix(u, config.BaseURL) {
+	if len(f.config.BaseURL) == 0 || strings.HasPrefix(u, f.config.BaseURL) {
 		return u
 		return u
 	}
 	}
 
 
-	return fmt.Sprintf("%s%s", config.BaseURL, u)
+	return fmt.Sprintf("%s%s", f.config.BaseURL, u)
 }
 }
 
 
-func decodeBase64URL(parts []string) (string, string, error) {
+func (f *Factory) decodeBase64URL(parts []string) (string, string, error) {
 	var format string
 	var format string
 
 
-	if len(parts) > 1 && config.Base64URLIncludesFilename {
+	if len(parts) > 1 && f.config.Base64URLIncludesFilename {
 		parts = parts[:len(parts)-1]
 		parts = parts[:len(parts)-1]
 	}
 	}
 
 
@@ -50,10 +48,10 @@ func decodeBase64URL(parts []string) (string, string, error) {
 		return "", "", newInvalidURLError("Invalid url encoding: %s", encoded)
 		return "", "", newInvalidURLError("Invalid url encoding: %s", encoded)
 	}
 	}
 
 
-	return preprocessURL(string(imageURL)), format, nil
+	return f.preprocessURL(string(imageURL)), format, nil
 }
 }
 
 
-func decodePlainURL(parts []string) (string, string, error) {
+func (f *Factory) decodePlainURL(parts []string) (string, string, error) {
 	var format string
 	var format string
 
 
 	encoded := strings.Join(parts, "/")
 	encoded := strings.Join(parts, "/")
@@ -76,17 +74,17 @@ func decodePlainURL(parts []string) (string, string, error) {
 		return "", "", newInvalidURLError("Invalid url encoding: %s", encoded)
 		return "", "", newInvalidURLError("Invalid url encoding: %s", encoded)
 	}
 	}
 
 
-	return preprocessURL(unescaped), format, nil
+	return f.preprocessURL(unescaped), format, nil
 }
 }
 
 
-func DecodeURL(parts []string) (string, string, error) {
+func (f *Factory) DecodeURL(parts []string) (string, string, error) {
 	if len(parts) == 0 {
 	if len(parts) == 0 {
 		return "", "", newInvalidURLError("Image URL is empty")
 		return "", "", newInvalidURLError("Image URL is empty")
 	}
 	}
 
 
 	if parts[0] == urlTokenPlain && len(parts) > 1 {
 	if parts[0] == urlTokenPlain && len(parts) > 1 {
-		return decodePlainURL(parts[1:])
+		return f.decodePlainURL(parts[1:])
 	}
 	}
 
 
-	return decodeBase64URL(parts)
+	return f.decodeBase64URL(parts)
 }
 }

+ 2 - 4
options/url_options.go

@@ -2,8 +2,6 @@ package options
 
 
 import (
 import (
 	"strings"
 	"strings"
-
-	"github.com/imgproxy/imgproxy/v3/config"
 )
 )
 
 
 type urlOption struct {
 type urlOption struct {
@@ -13,12 +11,12 @@ type urlOption struct {
 
 
 type urlOptions []urlOption
 type urlOptions []urlOption
 
 
-func parseURLOptions(opts []string) (urlOptions, []string) {
+func (f *Factory) parseURLOptions(opts []string) (urlOptions, []string) {
 	parsed := make(urlOptions, 0, len(opts))
 	parsed := make(urlOptions, 0, len(opts))
 	urlStart := len(opts) + 1
 	urlStart := len(opts) + 1
 
 
 	for i, opt := range opts {
 	for i, opt := range opts {
-		args := strings.Split(opt, config.ArgumentsSeparator)
+		args := strings.Split(opt, f.config.ArgumentsSeparator)
 
 
 		if len(args) == 1 {
 		if len(args) == 1 {
 			urlStart = i
 			urlStart = i

+ 28 - 14
processing/processing_test.go

@@ -16,12 +16,14 @@ import (
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/options"
 	"github.com/imgproxy/imgproxy/v3/options"
+	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/vips"
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 )
 
 
 type ProcessingTestSuite struct {
 type ProcessingTestSuite struct {
 	suite.Suite
 	suite.Suite
 	idf *imagedata.Factory
 	idf *imagedata.Factory
+	pof *options.Factory
 }
 }
 
 
 func (s *ProcessingTestSuite) SetupSuite() {
 func (s *ProcessingTestSuite) SetupSuite() {
@@ -41,6 +43,18 @@ func (s *ProcessingTestSuite) SetupSuite() {
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
 	s.idf = imagedata.NewFactory(f)
 	s.idf = imagedata.NewFactory(f)
+
+	scfg, err := security.LoadConfigFromEnv(nil)
+	s.Require().NoError(err)
+
+	security, err := security.New(scfg)
+	s.Require().NoError(err)
+
+	cfg, err := options.LoadConfigFromEnv(nil)
+	s.Require().NoError(err)
+
+	s.pof, err = options.NewFactory(cfg, security)
+	s.Require().NoError(err)
 }
 }
 
 
 func (s *ProcessingTestSuite) openFile(name string) imagedata.ImageData {
 func (s *ProcessingTestSuite) openFile(name string) imagedata.ImageData {
@@ -63,7 +77,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 := options.NewProcessingOptions()
+	po := s.pof.NewProcessingOptions()
 	po.ResizingType = options.ResizeFit
 	po.ResizingType = options.ResizeFit
 
 
 	testCases := []struct {
 	testCases := []struct {
@@ -101,7 +115,7 @@ func (s *ProcessingTestSuite) TestResizeToFit() {
 func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
 func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := options.NewProcessingOptions()
+	po := s.pof.NewProcessingOptions()
 	po.ResizingType = options.ResizeFit
 	po.ResizingType = options.ResizeFit
 	po.Enlarge = true
 	po.Enlarge = true
 
 
@@ -140,7 +154,7 @@ func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
 func (s *ProcessingTestSuite) TestResizeToFitExtend() {
 func (s *ProcessingTestSuite) TestResizeToFitExtend() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := options.NewProcessingOptions()
+	po := s.pof.NewProcessingOptions()
 	po.ResizingType = options.ResizeFit
 	po.ResizingType = options.ResizeFit
 	po.Extend = options.ExtendOptions{
 	po.Extend = options.ExtendOptions{
 		Enabled: true,
 		Enabled: true,
@@ -184,7 +198,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtend() {
 func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
 func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := options.NewProcessingOptions()
+	po := s.pof.NewProcessingOptions()
 	po.ResizingType = options.ResizeFit
 	po.ResizingType = options.ResizeFit
 	po.ExtendAspectRatio = options.ExtendOptions{
 	po.ExtendAspectRatio = options.ExtendOptions{
 		Enabled: true,
 		Enabled: true,
@@ -228,7 +242,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
 func (s *ProcessingTestSuite) TestResizeToFill() {
 func (s *ProcessingTestSuite) TestResizeToFill() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := options.NewProcessingOptions()
+	po := s.pof.NewProcessingOptions()
 	po.ResizingType = options.ResizeFill
 	po.ResizingType = options.ResizeFill
 
 
 	testCases := []struct {
 	testCases := []struct {
@@ -266,7 +280,7 @@ func (s *ProcessingTestSuite) TestResizeToFill() {
 func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
 func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := options.NewProcessingOptions()
+	po := s.pof.NewProcessingOptions()
 	po.ResizingType = options.ResizeFill
 	po.ResizingType = options.ResizeFill
 	po.Enlarge = true
 	po.Enlarge = true
 
 
@@ -305,7 +319,7 @@ func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
 func (s *ProcessingTestSuite) TestResizeToFillExtend() {
 func (s *ProcessingTestSuite) TestResizeToFillExtend() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := options.NewProcessingOptions()
+	po := s.pof.NewProcessingOptions()
 	po.ResizingType = options.ResizeFill
 	po.ResizingType = options.ResizeFill
 	po.Extend = options.ExtendOptions{
 	po.Extend = options.ExtendOptions{
 		Enabled: true,
 		Enabled: true,
@@ -351,7 +365,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtend() {
 func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
 func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := options.NewProcessingOptions()
+	po := s.pof.NewProcessingOptions()
 	po.ResizingType = options.ResizeFill
 	po.ResizingType = options.ResizeFill
 	po.ExtendAspectRatio = options.ExtendOptions{
 	po.ExtendAspectRatio = options.ExtendOptions{
 		Enabled: true,
 		Enabled: true,
@@ -397,7 +411,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
 func (s *ProcessingTestSuite) TestResizeToFillDown() {
 func (s *ProcessingTestSuite) TestResizeToFillDown() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := options.NewProcessingOptions()
+	po := s.pof.NewProcessingOptions()
 	po.ResizingType = options.ResizeFillDown
 	po.ResizingType = options.ResizeFillDown
 
 
 	testCases := []struct {
 	testCases := []struct {
@@ -435,7 +449,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDown() {
 func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
 func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := options.NewProcessingOptions()
+	po := s.pof.NewProcessingOptions()
 	po.ResizingType = options.ResizeFillDown
 	po.ResizingType = options.ResizeFillDown
 	po.Enlarge = true
 	po.Enlarge = true
 
 
@@ -474,7 +488,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
 func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
 func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := options.NewProcessingOptions()
+	po := s.pof.NewProcessingOptions()
 	po.ResizingType = options.ResizeFillDown
 	po.ResizingType = options.ResizeFillDown
 	po.Extend = options.ExtendOptions{
 	po.Extend = options.ExtendOptions{
 		Enabled: true,
 		Enabled: true,
@@ -520,7 +534,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
 func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
 func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := options.NewProcessingOptions()
+	po := s.pof.NewProcessingOptions()
 	po.ResizingType = options.ResizeFillDown
 	po.ResizingType = options.ResizeFillDown
 	po.ExtendAspectRatio = options.ExtendOptions{
 	po.ExtendAspectRatio = options.ExtendOptions{
 		Enabled: true,
 		Enabled: true,
@@ -564,7 +578,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
 func (s *ProcessingTestSuite) TestResultSizeLimit() {
 func (s *ProcessingTestSuite) TestResultSizeLimit() {
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")
 
 
-	po := options.NewProcessingOptions()
+	po := s.pof.NewProcessingOptions()
 
 
 	testCases := []struct {
 	testCases := []struct {
 		limit        int
 		limit        int
@@ -992,7 +1006,7 @@ func (s *ProcessingTestSuite) TestResultSizeLimit() {
 }
 }
 
 
 func (s *ProcessingTestSuite) TestImageResolutionTooLarge() {
 func (s *ProcessingTestSuite) TestImageResolutionTooLarge() {
-	po := options.NewProcessingOptions()
+	po := s.pof.NewProcessingOptions()
 	po.SecurityOptions.MaxSrcResolution = 1
 	po.SecurityOptions.MaxSrcResolution = 1
 
 
 	imgdata := s.openFile("test2.jpg")
 	imgdata := s.openFile("test2.jpg")

+ 17 - 15
processing/watermark.go

@@ -22,20 +22,22 @@ var watermarkPipeline = pipeline{
 	padding,
 	padding,
 }
 }
 
 
-func prepareWatermark(wm *vips.Image, wmData imagedata.ImageData, opts *options.WatermarkOptions, 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 {
 	if err := wm.Load(wmData, 1, 1.0, 1); err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	po := options.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 {
 	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() {
 	if opts.ShouldReplicate() {
@@ -53,14 +55,14 @@ func prepareWatermark(wm *vips.Image, wmData imagedata.ImageData, opts *options.
 			offY = imath.ScaleToEven(imgHeight, opts.Position.Y)
 			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); err != nil {
+	if err := watermarkPipeline.Run(context.Background(), wm, wmPo, wmData, nil); err != nil {
 		return err
 		return err
 	}
 	}
 
 
@@ -110,7 +112,7 @@ func applyWatermark(
 	height := img.Height()
 	height := img.Height()
 	frameHeight := height / framesCount
 	frameHeight := height / framesCount
 
 
-	if err := prepareWatermark(wm, wmData, &opts, width, frameHeight, offsetScale, framesCount); err != nil {
+	if err := prepareWatermark(wm, wmData, po, width, frameHeight, offsetScale, framesCount); err != nil {
 		return err
 		return err
 	}
 	}
 
 

+ 104 - 0
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()
+	}
+}