Просмотр исходного кода

IMG-59: processing instance (#1533)

* lazy processing test

* Processor instance

* Fixes to prepare.go
Victor Sokolov 1 неделя назад
Родитель
Сommit
ba73b16c39

+ 10 - 3
config.go

@@ -7,6 +7,7 @@ import (
 	processinghandler "github.com/imgproxy/imgproxy/v3/handlers/processing"
 	streamhandler "github.com/imgproxy/imgproxy/v3/handlers/stream"
 	"github.com/imgproxy/imgproxy/v3/options"
+	"github.com/imgproxy/imgproxy/v3/processing"
 	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/server"
 	"github.com/imgproxy/imgproxy/v3/workers"
@@ -27,6 +28,7 @@ type Config struct {
 	Handlers       HandlerConfigs
 	Server         server.Config
 	Security       security.Config
+	Processing     processing.Config
 	Options        options.Config
 }
 
@@ -41,9 +43,10 @@ func NewDefaultConfig() Config {
 			Processing: processinghandler.NewDefaultConfig(),
 			Stream:     streamhandler.NewDefaultConfig(),
 		},
-		Server:   server.NewDefaultConfig(),
-		Security: security.NewDefaultConfig(),
-		Options:  options.NewDefaultConfig(),
+		Server:     server.NewDefaultConfig(),
+		Security:   security.NewDefaultConfig(),
+		Processing: processing.NewDefaultConfig(),
+		Options:    options.NewDefaultConfig(),
 	}
 }
 
@@ -89,5 +92,9 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
 		return nil, err
 	}
 
+	if _, err = processing.LoadConfigFromEnv(&c.Processing); err != nil {
+		return nil, err
+	}
+
 	return c, nil
 }

+ 2 - 0
handlers/processing/handler.go

@@ -14,6 +14,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/monitoring"
 	"github.com/imgproxy/imgproxy/v3/monitoring/stats"
 	"github.com/imgproxy/imgproxy/v3/options"
+	"github.com/imgproxy/imgproxy/v3/processing"
 	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/server"
 	"github.com/imgproxy/imgproxy/v3/workers"
@@ -27,6 +28,7 @@ type HandlerContext interface {
 	ImageDataFactory() *imagedata.Factory
 	Security() *security.Checker
 	OptionsFactory() *options.Factory
+	Processor() *processing.Processor
 }
 
 // Handler handles image processing requests

+ 1 - 1
handlers/processing/request_methods.go

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

+ 7 - 0
imath/imath.go

@@ -52,3 +52,10 @@ func ShrinkToEven(a int, shrink float64) int {
 
 	return RoundToEven(float64(a) / shrink)
 }
+
+func NonZero[T ~int | ~float32 | ~float64](a, b T) T {
+	if a != 0 {
+		return a
+	}
+	return b
+}

+ 12 - 0
imgproxy.go

@@ -15,6 +15,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/memory"
 	"github.com/imgproxy/imgproxy/v3/monitoring/prometheus"
 	"github.com/imgproxy/imgproxy/v3/options"
+	"github.com/imgproxy/imgproxy/v3/processing"
 	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/server"
 	"github.com/imgproxy/imgproxy/v3/workers"
@@ -43,6 +44,7 @@ type Imgproxy struct {
 	handlers         ImgproxyHandlers
 	security         *security.Checker
 	optionsFactory   *options.Factory
+	processor        *processing.Processor
 	config           *Config
 }
 
@@ -80,6 +82,11 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
 		return nil, err
 	}
 
+	processor, err := processing.New(&config.Processing, watermarkImage)
+	if err != nil {
+		return nil, err
+	}
+
 	imgproxy := &Imgproxy{
 		workers:          workers,
 		fallbackImage:    fallbackImage,
@@ -89,6 +96,7 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
 		config:           config,
 		security:         security,
 		optionsFactory:   processingOptionsFactory,
+		processor:        processor,
 	}
 
 	imgproxy.handlers.Health = healthhandler.New()
@@ -211,3 +219,7 @@ func (i *Imgproxy) Security() *security.Checker {
 func (i *Imgproxy) OptionsFactory() *options.Factory {
 	return i.optionsFactory
 }
+
+func (i *Imgproxy) Processor() *processing.Processor {
+	return i.processor
+}

+ 0 - 6
init.go

@@ -13,7 +13,6 @@ import (
 	"github.com/imgproxy/imgproxy/v3/errorreport"
 	"github.com/imgproxy/imgproxy/v3/logger"
 	"github.com/imgproxy/imgproxy/v3/monitoring"
-	"github.com/imgproxy/imgproxy/v3/processing"
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
@@ -50,11 +49,6 @@ func Init() error {
 
 	errorreport.Init()
 
-	if err := processing.ValidatePreferredFormats(); err != nil {
-		vips.Shutdown()
-		return err
-	}
-
 	return nil
 }
 

+ 1 - 1
processing/apply_filters.go

@@ -1,6 +1,6 @@
 package processing
 
-func applyFilters(c *Context) error {
+func (p *Processor) applyFilters(c *Context) error {
 	if c.PO.Blur == 0 && c.PO.Sharpen == 0 && c.PO.Pixelate <= 1 {
 		return nil
 	}

+ 2 - 2
processing/colorspace_to_processing.go

@@ -1,6 +1,6 @@
 package processing
 
-func colorspaceToProcessing(c *Context) error {
+func (p *Processor) colorspaceToProcessing(c *Context) error {
 	if c.Img.ColourProfileImported() {
 		return nil
 	}
@@ -9,7 +9,7 @@ func colorspaceToProcessing(c *Context) error {
 		return err
 	}
 
-	convertToLinear := c.Config.UseLinearColorspace && (c.WScale != 1 || c.HScale != 1)
+	convertToLinear := p.config.UseLinearColorspace && (c.WScale != 1 || c.HScale != 1)
 
 	if c.Img.IsLinear() {
 		// The image is linear. If we keep its ICC, we'll get wrong colors after

+ 1 - 1
processing/colorspace_to_result.go

@@ -1,6 +1,6 @@
 package processing
 
-func colorspaceToResult(c *Context) error {
+func (p *Processor) colorspaceToResult(c *Context) error {
 	keepProfile := !c.PO.StripColorProfile && c.PO.Format.SupportsColourProfile()
 
 	if c.Img.IsLinear() {

+ 28 - 1
processing/pipeline/config.go → processing/config.go

@@ -1,14 +1,19 @@
-package pipeline
+package processing
 
 import (
 	"errors"
+	"fmt"
+	"log/slog"
 
 	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/ensure"
+	"github.com/imgproxy/imgproxy/v3/imagetype"
+	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
 // Config holds pipeline-related configuration.
 type Config struct {
+	PreferredFormats    []imagetype.Type
 	WatermarkOpacity    float64
 	DisableShrinkOnLoad bool
 	UseLinearColorspace bool
@@ -18,6 +23,11 @@ type Config struct {
 func NewDefaultConfig() Config {
 	return Config{
 		WatermarkOpacity: 1,
+		PreferredFormats: []imagetype.Type{
+			imagetype.JPEG,
+			imagetype.PNG,
+			imagetype.GIF,
+		},
 	}
 }
 
@@ -28,6 +38,7 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
 	c.WatermarkOpacity = config.WatermarkOpacity
 	c.DisableShrinkOnLoad = config.DisableShrinkOnLoad
 	c.UseLinearColorspace = config.UseLinearColorspace
+	c.PreferredFormats = config.PreferredFormats
 
 	return c, nil
 }
@@ -40,5 +51,21 @@ func (c *Config) Validate() error {
 		return errors.New("watermark opacity should be less than or equal to 1")
 	}
 
+	filtered := c.PreferredFormats[:0]
+
+	for _, t := range c.PreferredFormats {
+		if !vips.SupportsSave(t) {
+			slog.Warn(fmt.Sprintf("%s can't be a preferred format as it's saving is not supported", t))
+		} else {
+			filtered = append(filtered, t)
+		}
+	}
+
+	if len(filtered) == 0 {
+		return errors.New("no supported preferred formats specified")
+	}
+
+	c.PreferredFormats = filtered
+
 	return nil
 }

+ 2 - 2
processing/crop.go

@@ -31,7 +31,7 @@ func cropImage(img *vips.Image, cropWidth, cropHeight int, gravity *options.Grav
 	return img.Crop(left, top, cropWidth, cropHeight)
 }
 
-func crop(c *Context) error {
+func (p *Processor) crop(c *Context) error {
 	width, height := c.CropWidth, c.CropHeight
 
 	opts := c.CropGravity
@@ -46,6 +46,6 @@ func crop(c *Context) error {
 	return cropImage(c.Img, width, height, &opts, 1.0)
 }
 
-func cropToResult(c *Context) error {
+func (p *Processor) cropToResult(c *Context) error {
 	return cropImage(c.Img, c.ResultCropWidth, c.ResultCropHeight, &c.PO.Gravity, c.DprScale)
 }

+ 2 - 2
processing/extend.go

@@ -24,7 +24,7 @@ func extendImage(img *vips.Image, width, height int, gravity *options.GravityOpt
 	return img.Embed(width, height, offX, offY)
 }
 
-func extend(c *Context) error {
+func (p *Processor) extend(c *Context) error {
 	if !c.PO.Extend.Enabled {
 		return nil
 	}
@@ -33,7 +33,7 @@ func extend(c *Context) error {
 	return extendImage(c.Img, width, height, &c.PO.Extend.Gravity, c.DprScale)
 }
 
-func extendAspectRatio(c *Context) error {
+func (p *Processor) extendAspectRatio(c *Context) error {
 	if !c.PO.ExtendAspectRatio.Enabled {
 		return nil
 	}

+ 1 - 1
processing/fix_size.go

@@ -101,7 +101,7 @@ func fixIcoSize(img *vips.Image) error {
 	return nil
 }
 
-func fixSize(c *Context) error {
+func (p *Processor) fixSize(c *Context) error {
 	switch c.PO.Format {
 	case imagetype.WEBP:
 		return fixWebpSize(c.Img)

+ 1 - 1
processing/flatten.go

@@ -1,6 +1,6 @@
 package processing
 
-func flatten(c *Context) error {
+func (p *Processor) flatten(c *Context) error {
 	if !c.PO.Flatten && c.PO.Format.SupportsAlpha() {
 		return nil
 	}

+ 1 - 1
processing/padding.go

@@ -4,7 +4,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/imath"
 )
 
-func padding(c *Context) error {
+func (p *Processor) padding(c *Context) error {
 	if !c.PO.Padding.Enabled {
 		return nil
 	}

+ 9 - 39
processing/pipeline.go

@@ -6,28 +6,21 @@ import (
 	"github.com/imgproxy/imgproxy/v3/auximageprovider"
 	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/options"
-	"github.com/imgproxy/imgproxy/v3/processing/pipeline"
 	"github.com/imgproxy/imgproxy/v3/server"
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-// NOTE: this will be called pipeline.Context in the separate package
 type Context struct {
-	// The runner that runs this pipeline
-	runner *Runner
-
+	// The context to check for timeouts and cancellations
 	Ctx context.Context
 
-	// Global processing configuration which could be used by individual steps
-	Config *pipeline.Config
-
-	// VIPS image
+	// Current image being processed
 	Img *vips.Image
 
 	// Processing options this pipeline runs with
 	PO *options.ProcessingOptions
 
-	// Source image data
+	// Original image data
 	ImgData imagedata.ImageData
 
 	// The watermark image provider, if any watermarking is to be done.
@@ -80,33 +73,18 @@ type Context struct {
 	ExtendAspectRatioHeight int
 }
 
-// NOTE: same, pipeline.Step, pipeline.Pipeline, pipeline.Runner
-type Step func(ctx *Context) error
+type Step func(c *Context) error
 type Pipeline []Step
 
-// Runner is responsible for running a processing pipeline
-type Runner struct {
-	config    *pipeline.Config
-	watermark auximageprovider.Provider
-}
-
-// New creates a new Runner instance with the given configuration and watermark provider
-func New(config *pipeline.Config, watermark auximageprovider.Provider) *Runner {
-	return &Runner{
-		config:    config,
-		watermark: watermark,
-	}
-}
-
 // Run runs the given pipeline with the given parameters
-func (f *Runner) Run(
-	p Pipeline,
+func (p Pipeline) Run(
 	ctx context.Context,
 	img *vips.Image,
 	po *options.ProcessingOptions,
 	imgdata imagedata.ImageData,
 ) error {
-	pctx := f.newContext(ctx, img, po, imgdata)
+	pctx := p.newContext(ctx, img, po, imgdata)
+	pctx.CalcParams()
 
 	for _, step := range p {
 		if err := step(&pctx); err != nil {
@@ -123,17 +101,14 @@ func (f *Runner) Run(
 	return nil
 }
 
-func (r *Runner) newContext(
+func (p Pipeline) newContext(
 	ctx context.Context,
 	img *vips.Image,
 	po *options.ProcessingOptions,
 	imgdata imagedata.ImageData,
 ) Context {
 	pctx := Context{
-		runner: r,
-
 		Ctx:     ctx,
-		Config:  r.config,
 		Img:     img,
 		PO:      po,
 		ImgData: imgdata,
@@ -144,8 +119,7 @@ func (r *Runner) newContext(
 		DprScale:        1.0,
 		VectorBaseScale: 1.0,
 
-		CropGravity:       po.Crop.Gravity,
-		WatermarkProvider: r.watermark,
+		CropGravity: po.Crop.Gravity,
 	}
 
 	if pctx.CropGravity.Type == options.GravityUnknown {
@@ -154,7 +128,3 @@ func (r *Runner) newContext(
 
 	return pctx
 }
-
-func (c *Context) Runner() *Runner {
-	return c.runner
-}

+ 95 - 94
processing/prepare.go

@@ -8,38 +8,53 @@ import (
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-func extractMeta(img *vips.Image, baseAngle int, useOrientation bool) (int, int, int, bool) {
+// ExtractGeometry extracts image width, height, orientation angle and flip flag from the image metadata.
+func ExtractGeometry(img *vips.Image, baseAngle int, autoRotate bool) (int, int, int, bool) {
 	width := img.Width()
 	height := img.Height()
 
+	angle, flip := angleFlip(img, autoRotate)
+
+	if (angle+baseAngle)%180 != 0 {
+		width, height = height, width
+	}
+
+	return width, height, angle, flip
+}
+
+// angleFlip returns the orientation angle and flip flag based on the image metadata
+// and po.AutoRotate flag.
+func angleFlip(img *vips.Image, autoRotate bool) (int, bool) {
+	if !autoRotate {
+		return 0, false
+	}
+
 	angle := 0
 	flip := false
 
-	if useOrientation {
-		orientation := img.Orientation()
+	orientation := img.Orientation()
 
-		if orientation == 3 || orientation == 4 {
-			angle = 180
-		}
-		if orientation == 5 || orientation == 6 {
-			angle = 90
-		}
-		if orientation == 7 || orientation == 8 {
-			angle = 270
-		}
-		if orientation == 2 || orientation == 4 || orientation == 5 || orientation == 7 {
-			flip = true
-		}
+	if orientation == 3 || orientation == 4 {
+		angle = 180
 	}
 
-	if (angle+baseAngle)%180 != 0 {
-		width, height = height, width
+	if orientation == 5 || orientation == 6 {
+		angle = 90
 	}
 
-	return width, height, angle, flip
+	if orientation == 7 || orientation == 8 {
+		angle = 270
+	}
+
+	if orientation == 2 || orientation == 4 || orientation == 5 || orientation == 7 {
+		flip = true
+	}
+
+	return angle, flip
 }
 
-func calcCropSize(orig int, crop float64) int {
+// CalcCropSize calculates the crop size based on the original size and crop scale.
+func (c *Context) CalcCropSize(orig int, crop float64) int {
 	switch {
 	case crop == 0.0:
 		return 0
@@ -50,29 +65,18 @@ func calcCropSize(orig int, crop float64) int {
 	}
 }
 
-func (pctx *Context) calcScale(width, height int, po *options.ProcessingOptions) {
-	var wshrink, hshrink float64
-
+func (c *Context) calcScale(width, height int, po *options.ProcessingOptions) {
+	wshrink, hshrink := 1.0, 1.0
 	srcW, srcH := float64(width), float64(height)
-	dstW, dstH := float64(po.Width), float64(po.Height)
 
-	if po.Width == 0 {
-		dstW = srcW
-	}
+	dstW := imath.NonZero(float64(po.Width), srcW)
+	dstH := imath.NonZero(float64(po.Height), srcH)
 
-	if dstW == srcW {
-		wshrink = 1
-	} else {
+	if dstW != srcW {
 		wshrink = srcW / dstW
 	}
 
-	if po.Height == 0 {
-		dstH = srcH
-	}
-
-	if dstH == srcH {
-		hshrink = 1
-	} else {
+	if dstH != srcH {
 		hshrink = srcH / dstH
 	}
 
@@ -107,9 +111,9 @@ func (pctx *Context) calcScale(width, height int, po *options.ProcessingOptions)
 	wshrink /= po.ZoomWidth
 	hshrink /= po.ZoomHeight
 
-	pctx.DprScale = po.Dpr
+	c.DprScale = po.Dpr
 
-	isVector := pctx.ImgData != nil && pctx.ImgData.Format().IsVector()
+	isVector := c.ImgData != nil && c.ImgData.Format().IsVector()
 
 	if !po.Enlarge && !isVector {
 		minShrink := math.Min(wshrink, hshrink)
@@ -128,13 +132,13 @@ func (pctx *Context) calcScale(width, height int, po *options.ProcessingOptions)
 			// composition the same regardless of the DPR, so we don't apply this compensation
 			// in this case.
 			if !po.Extend.Enabled {
-				pctx.DprScale /= minShrink
+				c.DprScale /= minShrink
 			}
 		}
 
 		// The minimum of wshrink and hshrink is the maximum dprScale value
 		// that can be used without enlarging the image.
-		pctx.DprScale = math.Min(pctx.DprScale, math.Min(wshrink, hshrink))
+		c.DprScale = math.Min(c.DprScale, math.Min(wshrink, hshrink))
 	}
 
 	if po.MinWidth > 0 {
@@ -151,8 +155,8 @@ func (pctx *Context) calcScale(width, height int, po *options.ProcessingOptions)
 		}
 	}
 
-	wshrink /= pctx.DprScale
-	hshrink /= pctx.DprScale
+	wshrink /= c.DprScale
+	hshrink /= c.DprScale
 
 	if wshrink > srcW {
 		wshrink = srcW
@@ -162,110 +166,109 @@ func (pctx *Context) calcScale(width, height int, po *options.ProcessingOptions)
 		hshrink = srcH
 	}
 
-	pctx.WScale = 1.0 / wshrink
-	pctx.HScale = 1.0 / hshrink
+	c.WScale = 1.0 / wshrink
+	c.HScale = 1.0 / hshrink
 }
 
-func (pctx *Context) calcSizes(widthToScale, heightToScale int, po *options.ProcessingOptions) {
-	pctx.TargetWidth = imath.Scale(po.Width, pctx.DprScale*po.ZoomWidth)
-	pctx.TargetHeight = imath.Scale(po.Height, pctx.DprScale*po.ZoomHeight)
+func (c *Context) calcSizes(widthToScale, heightToScale int, po *options.ProcessingOptions) {
+	c.TargetWidth = imath.Scale(po.Width, c.DprScale*po.ZoomWidth)
+	c.TargetHeight = imath.Scale(po.Height, c.DprScale*po.ZoomHeight)
 
-	pctx.ScaledWidth = imath.Scale(widthToScale, pctx.WScale)
-	pctx.ScaledHeight = imath.Scale(heightToScale, pctx.HScale)
+	c.ScaledWidth = imath.Scale(widthToScale, c.WScale)
+	c.ScaledHeight = imath.Scale(heightToScale, c.HScale)
 
 	if po.ResizingType == options.ResizeFillDown && !po.Enlarge {
-		diffW := float64(pctx.TargetWidth) / float64(pctx.ScaledWidth)
-		diffH := float64(pctx.TargetHeight) / float64(pctx.ScaledHeight)
+		diffW := float64(c.TargetWidth) / float64(c.ScaledWidth)
+		diffH := float64(c.TargetHeight) / float64(c.ScaledHeight)
 
 		switch {
 		case diffW > diffH && diffW > 1.0:
-			pctx.ResultCropHeight = imath.Scale(pctx.ScaledWidth, float64(pctx.TargetHeight)/float64(pctx.TargetWidth))
-			pctx.ResultCropWidth = pctx.ScaledWidth
+			c.ResultCropHeight = imath.Scale(c.ScaledWidth, float64(c.TargetHeight)/float64(c.TargetWidth))
+			c.ResultCropWidth = c.ScaledWidth
 
 		case diffH > diffW && diffH > 1.0:
-			pctx.ResultCropWidth = imath.Scale(pctx.ScaledHeight, float64(pctx.TargetWidth)/float64(pctx.TargetHeight))
-			pctx.ResultCropHeight = pctx.ScaledHeight
+			c.ResultCropWidth = imath.Scale(c.ScaledHeight, float64(c.TargetWidth)/float64(c.TargetHeight))
+			c.ResultCropHeight = c.ScaledHeight
 
 		default:
-			pctx.ResultCropWidth = pctx.TargetWidth
-			pctx.ResultCropHeight = pctx.TargetHeight
+			c.ResultCropWidth = c.TargetWidth
+			c.ResultCropHeight = c.TargetHeight
 		}
 	} else {
-		pctx.ResultCropWidth = pctx.TargetWidth
-		pctx.ResultCropHeight = pctx.TargetHeight
+		c.ResultCropWidth = c.TargetWidth
+		c.ResultCropHeight = c.TargetHeight
 	}
 
-	if po.ExtendAspectRatio.Enabled && pctx.TargetWidth > 0 && pctx.TargetHeight > 0 {
-		outWidth := imath.MinNonZero(pctx.ScaledWidth, pctx.ResultCropWidth)
-		outHeight := imath.MinNonZero(pctx.ScaledHeight, pctx.ResultCropHeight)
+	if po.ExtendAspectRatio.Enabled && c.TargetWidth > 0 && c.TargetHeight > 0 {
+		outWidth := imath.MinNonZero(c.ScaledWidth, c.ResultCropWidth)
+		outHeight := imath.MinNonZero(c.ScaledHeight, c.ResultCropHeight)
 
-		diffW := float64(pctx.TargetWidth) / float64(outWidth)
-		diffH := float64(pctx.TargetHeight) / float64(outHeight)
+		diffW := float64(c.TargetWidth) / float64(outWidth)
+		diffH := float64(c.TargetHeight) / float64(outHeight)
 
 		switch {
 		case diffH > diffW:
-			pctx.ExtendAspectRatioHeight = imath.Scale(outWidth, float64(pctx.TargetHeight)/float64(pctx.TargetWidth))
-			pctx.ExtendAspectRatioWidth = outWidth
+			c.ExtendAspectRatioHeight = imath.Scale(outWidth, float64(c.TargetHeight)/float64(c.TargetWidth))
+			c.ExtendAspectRatioWidth = outWidth
 
 		case diffW > diffH:
-			pctx.ExtendAspectRatioWidth = imath.Scale(outHeight, float64(pctx.TargetWidth)/float64(pctx.TargetHeight))
-			pctx.ExtendAspectRatioHeight = outHeight
+			c.ExtendAspectRatioWidth = imath.Scale(outHeight, float64(c.TargetWidth)/float64(c.TargetHeight))
+			c.ExtendAspectRatioHeight = outHeight
 		}
 	}
 }
 
-func (pctx *Context) limitScale(widthToScale, heightToScale int, po *options.ProcessingOptions) {
+func (c *Context) limitScale(widthToScale, heightToScale int, po *options.ProcessingOptions) {
 	maxresultDim := po.SecurityOptions.MaxResultDimension
 
 	if maxresultDim <= 0 {
 		return
 	}
 
-	outWidth := imath.MinNonZero(pctx.ScaledWidth, pctx.ResultCropWidth)
-	outHeight := imath.MinNonZero(pctx.ScaledHeight, pctx.ResultCropHeight)
+	outWidth := imath.MinNonZero(c.ScaledWidth, c.ResultCropWidth)
+	outHeight := imath.MinNonZero(c.ScaledHeight, c.ResultCropHeight)
 
 	if po.Extend.Enabled {
-		outWidth = max(outWidth, pctx.TargetWidth)
-		outHeight = max(outHeight, pctx.TargetHeight)
+		outWidth = max(outWidth, c.TargetWidth)
+		outHeight = max(outHeight, c.TargetHeight)
 	} else if po.ExtendAspectRatio.Enabled {
-		outWidth = max(outWidth, pctx.ExtendAspectRatioWidth)
-		outHeight = max(outHeight, pctx.ExtendAspectRatioHeight)
+		outWidth = max(outWidth, c.ExtendAspectRatioWidth)
+		outHeight = max(outHeight, c.ExtendAspectRatioHeight)
 	}
 
 	if po.Padding.Enabled {
-		outWidth += imath.ScaleToEven(po.Padding.Left, pctx.DprScale) + imath.ScaleToEven(po.Padding.Right, pctx.DprScale)
-		outHeight += imath.ScaleToEven(po.Padding.Top, pctx.DprScale) + imath.ScaleToEven(po.Padding.Bottom, pctx.DprScale)
+		outWidth += imath.ScaleToEven(po.Padding.Left, c.DprScale) + imath.ScaleToEven(po.Padding.Right, c.DprScale)
+		outHeight += imath.ScaleToEven(po.Padding.Top, c.DprScale) + imath.ScaleToEven(po.Padding.Bottom, c.DprScale)
 	}
 
 	if maxresultDim > 0 && (outWidth > maxresultDim || outHeight > maxresultDim) {
 		downScale := float64(maxresultDim) / float64(max(outWidth, outHeight))
 
-		pctx.WScale *= downScale
-		pctx.HScale *= downScale
+		c.WScale *= downScale
+		c.HScale *= downScale
 
 		// Prevent scaling below 1px
-		if minWScale := 1.0 / float64(widthToScale); pctx.WScale < minWScale {
-			pctx.WScale = minWScale
+		if minWScale := 1.0 / float64(widthToScale); c.WScale < minWScale {
+			c.WScale = minWScale
 		}
-		if minHScale := 1.0 / float64(heightToScale); pctx.HScale < minHScale {
-			pctx.HScale = minHScale
+		if minHScale := 1.0 / float64(heightToScale); c.HScale < minHScale {
+			c.HScale = minHScale
 		}
 
-		pctx.DprScale *= downScale
+		c.DprScale *= downScale
 
 		// Recalculate the sizes after changing the scales
-		pctx.calcSizes(widthToScale, heightToScale, po)
+		c.calcSizes(widthToScale, heightToScale, po)
 	}
 }
 
-// prepare extracts image metadata and calculates scaling factors and target sizes.
-// This can't be done in advance because some steps like trimming and rasterization could
-// happen before this step.
-func prepare(c *Context) error {
-	c.SrcWidth, c.SrcHeight, c.Angle, c.Flip = extractMeta(c.Img, c.PO.Rotate, c.PO.AutoRotate)
+// Prepare calculates context image parameters based on the current image size.
+// Some steps (like trim) must call this function when finished.
+func (c *Context) CalcParams() {
+	c.SrcWidth, c.SrcHeight, c.Angle, c.Flip = ExtractGeometry(c.Img, c.PO.Rotate, c.PO.AutoRotate)
 
-	c.CropWidth = calcCropSize(c.SrcWidth, c.PO.Crop.Width)
-	c.CropHeight = calcCropSize(c.SrcHeight, c.PO.Crop.Height)
+	c.CropWidth = c.CalcCropSize(c.SrcWidth, c.PO.Crop.Width)
+	c.CropHeight = c.CalcCropSize(c.SrcHeight, c.PO.Crop.Height)
 
 	widthToScale := imath.MinNonZero(c.CropWidth, c.SrcWidth)
 	heightToScale := imath.MinNonZero(c.CropHeight, c.SrcHeight)
@@ -273,6 +276,4 @@ func prepare(c *Context) error {
 	c.calcScale(widthToScale, heightToScale, c.PO)
 	c.calcSizes(widthToScale, heightToScale, c.PO)
 	c.limitScale(widthToScale, heightToScale, c.PO)
-
-	return nil
 }

+ 36 - 91
processing/processing.go

@@ -2,7 +2,6 @@ package processing
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"log/slog"
 	"runtime"
@@ -13,59 +12,41 @@ import (
 	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
 	"github.com/imgproxy/imgproxy/v3/options"
-	"github.com/imgproxy/imgproxy/v3/processing/pipeline"
 	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/server"
 	"github.com/imgproxy/imgproxy/v3/svg"
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-// The main processing pipeline (without finalization).
-// Applied to non-animated images and individual frames of animated images.
-var mainPipeline = Pipeline{
-	vectorGuardScale,
-	trim,
-	prepare,
-	scaleOnLoad,
-	colorspaceToProcessing,
-	crop,
-	scale,
-	rotateAndFlip,
-	cropToResult,
-	applyFilters,
-	extend,
-	extendAspectRatio,
-	padding,
-	fixSize,
-	flatten,
-	watermark,
-}
-
-// The finalization pipeline.
-// Applied right before saving the image.
-var finalizePipeline = Pipeline{
-	colorspaceToResult,
-	stripMetadata,
-}
-
-func ValidatePreferredFormats() error {
-	filtered := config.PreferredFormats[:0]
-
-	for _, t := range config.PreferredFormats {
-		if !vips.SupportsSave(t) {
-			slog.Warn(fmt.Sprintf("%s can't be a preferred format as it's saving is not supported", t))
-		} else {
-			filtered = append(filtered, t)
-		}
+// mainPipeline constructs the main image processing pipeline.
+// This pipeline is applied to each image frame.
+func (p *Processor) mainPipeline() Pipeline {
+	return Pipeline{
+		p.vectorGuardScale,
+		p.trim,
+		p.scaleOnLoad,
+		p.colorspaceToProcessing,
+		p.crop,
+		p.scale,
+		p.rotateAndFlip,
+		p.cropToResult,
+		p.applyFilters,
+		p.extend,
+		p.extendAspectRatio,
+		p.padding,
+		p.fixSize,
+		p.flatten,
+		p.watermark,
 	}
+}
 
-	if len(filtered) == 0 {
-		return errors.New("no supported preferred formats specified")
+// finalizePipeline constructs the finalization pipeline.
+// This pipeline is applied before saving the image.
+func (p *Processor) finalizePipeline() Pipeline {
+	return Pipeline{
+		p.colorspaceToResult,
+		p.stripMetadata,
 	}
-
-	config.PreferredFormats = filtered
-
-	return nil
 }
 
 // Result holds the result of image processing.
@@ -81,7 +62,7 @@ type Result struct {
 // and returns a [Result] that includes the processed image data and dimensions.
 //
 // The provided processing options may be modified during processing.
-func ProcessImage(
+func (p *Processor) ProcessImage(
 	ctx context.Context,
 	imgdata imagedata.ImageData,
 	po *options.ProcessingOptions,
@@ -139,19 +120,12 @@ func ProcessImage(
 	}
 
 	// Transform the image (resize, crop, etc)
-	if err = transformImage(ctx, img, po, imgdata, animated, watermarkProvider); err != nil {
+	if err = p.transformImage(ctx, img, po, imgdata, animated); err != nil {
 		return nil, err
 	}
 
-	// NOTE: THIS IS TEMPORARY
-	runner, err := tmpNewRunner(watermarkProvider)
-	if err != nil {
-		return nil, err
-	}
-	// NOTE: END TEMPORARY BLOCK
-
 	// Finalize the image (colorspace conversion, metadata stripping, etc)
-	if err = runner.Run(finalizePipeline, ctx, img, po, imgdata); err != nil {
+	if err = p.finalizePipeline().Run(ctx, img, po, imgdata); err != nil {
 		return nil, err
 	}
 
@@ -389,41 +363,25 @@ func findPreferredFormat(animated, expectTransparency bool) imagetype.Type {
 	return config.PreferredFormats[0]
 }
 
-func transformImage(
+func (p *Processor) transformImage(
 	ctx context.Context,
 	img *vips.Image,
 	po *options.ProcessingOptions,
 	imgdata imagedata.ImageData,
 	asAnimated bool,
-	watermark auximageprovider.Provider,
 ) error {
 	if asAnimated {
-		return transformAnimated(ctx, img, po, watermark)
+		return p.transformAnimated(ctx, img, po)
 	}
 
-	// NOTE: THIS IS TEMPORARY
-	runner, err := tmpNewRunner(watermark)
-	if err != nil {
-		return err
-	}
-	// NOTE: END TEMPORARY BLOCK
-
-	return runner.Run(mainPipeline, ctx, img, po, imgdata)
+	return p.mainPipeline().Run(ctx, img, po, imgdata)
 }
 
-func transformAnimated(
+func (p *Processor) transformAnimated(
 	ctx context.Context,
 	img *vips.Image,
 	po *options.ProcessingOptions,
-	watermark auximageprovider.Provider,
 ) error {
-	// NOTE: THIS IS TEMPORARY
-	runner, rerr := tmpNewRunner(watermark)
-	if rerr != nil {
-		return rerr
-	}
-	// NOTE: END TEMPORARY BLOCK
-
 	if po.Trim.Enabled {
 		slog.Warn("Trim is not supported for animated images")
 		po.Trim.Enabled = false
@@ -477,7 +435,7 @@ func transformAnimated(
 		// Transform the frame using the main pipeline.
 		// We don't provide imgdata here to prevent scale-on-load.
 		// Watermarking is disabled for individual frames (see above)
-		if err = runner.Run(mainPipeline, ctx, frame, po, nil); err != nil {
+		if err = p.mainPipeline().Run(ctx, frame, po, nil); err != nil {
 			return err
 		}
 
@@ -501,7 +459,7 @@ func transformAnimated(
 
 	// Apply watermark to all frames at once if it was requested.
 	// This is much more efficient than applying watermark to individual frames.
-	if watermarkEnabled && watermark != nil {
+	if watermarkEnabled && p.watermarkProvider != nil {
 		// Get DPR scale to apply watermark correctly on HiDPI images.
 		// `imgproxy-dpr-scale` is set by the pipeline.
 		dprScale, derr := img.GetDoubleDefault("imgproxy-dpr-scale", 1.0)
@@ -509,7 +467,7 @@ func transformAnimated(
 			dprScale = 1.0
 		}
 
-		if err = applyWatermark(ctx, runner, img, watermark, po, dprScale, framesCount); err != nil {
+		if err = p.applyWatermark(ctx, img, po, dprScale, framesCount); err != nil {
 			return err
 		}
 	}
@@ -565,16 +523,3 @@ func saveImage(
 	// Otherwise, just save the image with the specified quality.
 	return img.Save(po.Format, po.GetQuality())
 }
-
-func tmpNewRunner(watermarkProvider auximageprovider.Provider) (*Runner, error) {
-	// NOTE: THIS IS TEMPORARY
-	config, err := pipeline.LoadConfigFromEnv(nil)
-	if err != nil {
-		return nil, err
-	}
-
-	runner := New(config, watermarkProvider)
-
-	return runner, nil
-	// NOTE: END TEMPORARY BLOCK
-}

+ 33 - 67
processing/processing_test.go

@@ -27,6 +27,8 @@ type ProcessingTestSuite struct {
 	security         testutil.LazyObj[*security.Checker]
 	poConfig         testutil.LazyObj[*options.Config]
 	po               testutil.LazyObj[*options.Factory]
+	config           testutil.LazyObj[*Config]
+	processor        testutil.LazyObj[*Processor]
 }
 
 func (s *ProcessingTestSuite) SetupSuite() {
@@ -67,6 +69,15 @@ func (s *ProcessingTestSuite) SetupSuite() {
 	s.po, _ = testutil.NewLazySuiteObj(s, func() (*options.Factory, error) {
 		return options.NewFactory(s.poConfig(), s.security())
 	})
+
+	s.config, _ = testutil.NewLazySuiteObj(s, func() (*Config, error) {
+		c := NewDefaultConfig()
+		return &c, nil
+	})
+
+	s.processor, _ = testutil.NewLazySuiteObj(s, func() (*Processor, error) {
+		return New(s.config(), nil)
+	})
 }
 
 func (s *ProcessingTestSuite) TearDownSuite() {
@@ -90,6 +101,14 @@ func (s *ProcessingTestSuite) checkSize(r *Result, width, height int) {
 	s.Require().Equal(height, r.ResultHeight, "Height mismatch")
 }
 
+func (s *ProcessingTestSuite) processImageAndCheck(imgdata imagedata.ImageData, po *options.ProcessingOptions, expectedWidth, expectedHeight int) {
+	result, err := s.processor().ProcessImage(context.Background(), imgdata, po, nil)
+	s.Require().NoError(err)
+	s.Require().NotNil(result)
+
+	s.checkSize(result, expectedWidth, expectedHeight)
+}
+
 func (s *ProcessingTestSuite) TestResizeToFit() {
 	imgdata := s.openFile("test2.jpg")
 
@@ -119,11 +138,7 @@ func (s *ProcessingTestSuite) TestResizeToFit() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			result, err := ProcessImage(context.Background(), imgdata, po, nil)
-			s.Require().NoError(err)
-			s.Require().NotNil(result)
-
-			s.checkSize(result, tc.outWidth, tc.outHeight)
+			s.processImageAndCheck(imgdata, po, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -158,11 +173,7 @@ func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			result, err := ProcessImage(context.Background(), imgdata, po, nil)
-			s.Require().NoError(err)
-			s.Require().NotNil(result)
-
-			s.checkSize(result, tc.outWidth, tc.outHeight)
+			s.processImageAndCheck(imgdata, po, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -202,11 +213,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtend() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			result, err := ProcessImage(context.Background(), imgdata, po, nil)
-			s.Require().NoError(err)
-			s.Require().NotNil(result)
-
-			s.checkSize(result, tc.outWidth, tc.outHeight)
+			s.processImageAndCheck(imgdata, po, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -246,11 +253,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			result, err := ProcessImage(context.Background(), imgdata, po, nil)
-			s.Require().NoError(err)
-			s.Require().NotNil(result)
-
-			s.checkSize(result, tc.outWidth, tc.outHeight)
+			s.processImageAndCheck(imgdata, po, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -284,11 +287,7 @@ func (s *ProcessingTestSuite) TestResizeToFill() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			result, err := ProcessImage(context.Background(), imgdata, po, nil)
-			s.Require().NoError(err)
-			s.Require().NotNil(result)
-
-			s.checkSize(result, tc.outWidth, tc.outHeight)
+			s.processImageAndCheck(imgdata, po, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -323,11 +322,7 @@ func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			result, err := ProcessImage(context.Background(), imgdata, po, nil)
-			s.Require().NoError(err)
-			s.Require().NotNil(result)
-
-			s.checkSize(result, tc.outWidth, tc.outHeight)
+			s.processImageAndCheck(imgdata, po, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -369,11 +364,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtend() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			result, err := ProcessImage(context.Background(), imgdata, po, nil)
-			s.Require().NoError(err)
-			s.Require().NotNil(result)
-
-			s.checkSize(result, tc.outWidth, tc.outHeight)
+			s.processImageAndCheck(imgdata, po, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -415,11 +406,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			result, err := ProcessImage(context.Background(), imgdata, po, nil)
-			s.Require().NoError(err)
-			s.Require().NotNil(result)
-
-			s.checkSize(result, tc.outWidth, tc.outHeight)
+			s.processImageAndCheck(imgdata, po, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -453,11 +440,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDown() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			result, err := ProcessImage(context.Background(), imgdata, po, nil)
-			s.Require().NoError(err)
-			s.Require().NotNil(result)
-
-			s.checkSize(result, tc.outWidth, tc.outHeight)
+			s.processImageAndCheck(imgdata, po, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -492,11 +475,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			result, err := ProcessImage(context.Background(), imgdata, po, nil)
-			s.Require().NoError(err)
-			s.Require().NotNil(result)
-
-			s.checkSize(result, tc.outWidth, tc.outHeight)
+			s.processImageAndCheck(imgdata, po, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -538,11 +517,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			result, err := ProcessImage(context.Background(), imgdata, po, nil)
-			s.Require().NoError(err)
-			s.Require().NotNil(result)
-
-			s.checkSize(result, tc.outWidth, tc.outHeight)
+			s.processImageAndCheck(imgdata, po, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -582,11 +557,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			result, err := ProcessImage(context.Background(), imgdata, po, nil)
-			s.Require().NoError(err)
-			s.Require().NotNil(result)
-
-			s.checkSize(result, tc.outWidth, tc.outHeight)
+			s.processImageAndCheck(imgdata, po, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -1011,12 +982,7 @@ func (s *ProcessingTestSuite) TestResultSizeLimit() {
 			po.Rotate = tc.rotate
 			po.Padding = tc.padding
 
-			result, err := ProcessImage(context.Background(), imgdata, po, nil)
-
-			s.Require().NoError(err)
-			s.Require().NotNil(result)
-
-			s.checkSize(result, tc.outWidth, tc.outHeight)
+			s.processImageAndCheck(imgdata, po, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -1026,7 +992,7 @@ func (s *ProcessingTestSuite) TestImageResolutionTooLarge() {
 	po.SecurityOptions.MaxSrcResolution = 1
 
 	imgdata := s.openFile("test2.jpg")
-	_, err := ProcessImage(context.Background(), imgdata, po, nil)
+	_, err := s.processor().ProcessImage(context.Background(), imgdata, po, nil)
 
 	s.Require().Error(err)
 	s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode())

+ 23 - 0
processing/processor.go

@@ -0,0 +1,23 @@
+package processing
+
+import (
+	"github.com/imgproxy/imgproxy/v3/auximageprovider"
+)
+
+// Processor is responsible for processing images according to the given configuration.
+type Processor struct {
+	config            *Config
+	watermarkProvider auximageprovider.Provider
+}
+
+// New creates a new Processor instance with the given configuration and watermark provider
+func New(config *Config, watermark auximageprovider.Provider) (*Processor, error) {
+	if err := config.Validate(); err != nil {
+		return nil, err
+	}
+
+	return &Processor{
+		config:            config,
+		watermarkProvider: watermark,
+	}, nil
+}

+ 7 - 7
processing/rotate_and_flip.go

@@ -1,23 +1,23 @@
 package processing
 
-func rotateAndFlip(ctx *Context) error {
-	if ctx.Angle%360 == 0 && ctx.PO.Rotate%360 == 0 && !ctx.Flip {
+func (p *Processor) rotateAndFlip(c *Context) error {
+	if c.Angle%360 == 0 && c.PO.Rotate%360 == 0 && !c.Flip {
 		return nil
 	}
 
-	if err := ctx.Img.CopyMemory(); err != nil {
+	if err := c.Img.CopyMemory(); err != nil {
 		return err
 	}
 
-	if err := ctx.Img.Rotate(ctx.Angle); err != nil {
+	if err := c.Img.Rotate(c.Angle); err != nil {
 		return err
 	}
 
-	if ctx.Flip {
-		if err := ctx.Img.Flip(); err != nil {
+	if c.Flip {
+		if err := c.Img.Flip(); err != nil {
 			return err
 		}
 	}
 
-	return ctx.Img.Rotate(ctx.PO.Rotate)
+	return c.Img.Rotate(c.PO.Rotate)
 }

+ 1 - 1
processing/scale.go

@@ -1,6 +1,6 @@
 package processing
 
-func scale(c *Context) error {
+func (p *Processor) scale(c *Context) error {
 	if c.WScale == 1 && c.HScale == 1 {
 		return nil
 	}

+ 6 - 6
processing/scale_on_load.go

@@ -11,7 +11,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-func canScaleOnLoad(c *Context, imgdata imagedata.ImageData, scale float64) bool {
+func (p *Processor) canScaleOnLoad(c *Context, imgdata imagedata.ImageData, scale float64) bool {
 	if imgdata == nil || scale == 1 {
 		return false
 	}
@@ -20,7 +20,7 @@ func canScaleOnLoad(c *Context, imgdata imagedata.ImageData, scale float64) bool
 		return true
 	}
 
-	if c.Config.DisableShrinkOnLoad || scale >= 1 {
+	if p.config.DisableShrinkOnLoad || scale >= 1 {
 		return false
 	}
 
@@ -43,7 +43,7 @@ func calcJpegShink(shrink float64) int {
 	return 1
 }
 
-func scaleOnLoad(c *Context) error {
+func (p *Processor) scaleOnLoad(c *Context) error {
 	wshrink := float64(c.SrcWidth) / float64(imath.Scale(c.SrcWidth, c.WScale))
 	hshrink := float64(c.SrcHeight) / float64(imath.Scale(c.SrcHeight, c.HScale))
 	preshrink := math.Min(wshrink, hshrink)
@@ -54,7 +54,7 @@ func scaleOnLoad(c *Context) error {
 		prescale *= c.VectorBaseScale
 	}
 
-	if !canScaleOnLoad(c, c.ImgData, prescale) {
+	if !p.canScaleOnLoad(c, c.ImgData, prescale) {
 		return nil
 	}
 
@@ -70,7 +70,7 @@ func scaleOnLoad(c *Context) error {
 		}
 
 		angle, flip := 0, false
-		newWidth, newHeight, angle, flip = extractMeta(thumbnail, c.PO.Rotate, c.PO.AutoRotate)
+		newWidth, newHeight, angle, flip = ExtractGeometry(thumbnail, c.PO.Rotate, c.PO.AutoRotate)
 
 		if newWidth >= c.SrcWidth || float64(newWidth)/float64(c.SrcWidth) < prescale {
 			return nil
@@ -90,7 +90,7 @@ func scaleOnLoad(c *Context) error {
 			return err
 		}
 
-		newWidth, newHeight, _, _ = extractMeta(c.Img, c.PO.Rotate, c.PO.AutoRotate)
+		newWidth, newHeight, _, _ = ExtractGeometry(c.Img, c.PO.Rotate, c.PO.AutoRotate)
 	}
 
 	// Update scales after scale-on-load

+ 9 - 9
processing/strip_metadata.go

@@ -103,29 +103,29 @@ func stripXMP(img *vips.Image) []byte {
 	return xmpData
 }
 
-func stripMetadata(ctx *Context) error {
-	if !ctx.PO.StripMetadata {
+func (p *Processor) stripMetadata(c *Context) error {
+	if !c.PO.StripMetadata {
 		return nil
 	}
 
 	var ps3Data, xmpData []byte
 
-	if ctx.PO.KeepCopyright {
-		ps3Data = stripPS3(ctx.Img)
-		xmpData = stripXMP(ctx.Img)
+	if c.PO.KeepCopyright {
+		ps3Data = stripPS3(c.Img)
+		xmpData = stripXMP(c.Img)
 	}
 
-	if err := ctx.Img.Strip(ctx.PO.KeepCopyright); err != nil {
+	if err := c.Img.Strip(c.PO.KeepCopyright); err != nil {
 		return err
 	}
 
-	if ctx.PO.KeepCopyright {
+	if c.PO.KeepCopyright {
 		if len(ps3Data) > 0 {
-			ctx.Img.SetBlob("iptc-data", ps3Data)
+			c.Img.SetBlob("iptc-data", ps3Data)
 		}
 
 		if len(xmpData) > 0 {
-			ctx.Img.SetBlob("xmp-data", xmpData)
+			c.Img.SetBlob("xmp-data", xmpData)
 		}
 	}
 

+ 3 - 2
processing/trim.go

@@ -1,12 +1,12 @@
 package processing
 
-func trim(c *Context) error {
+func (p *Processor) trim(c *Context) error {
 	if !c.PO.Trim.Enabled {
 		return nil
 	}
 
 	// We need to import color profile before trim
-	if err := colorspaceToProcessing(c); err != nil {
+	if err := p.colorspaceToProcessing(c); err != nil {
 		return err
 	}
 
@@ -18,6 +18,7 @@ func trim(c *Context) error {
 	}
 
 	c.ImgData = nil
+	c.CalcParams()
 
 	return nil
 }

+ 2 - 1
processing/vector_guard_scale.go

@@ -6,7 +6,7 @@ import (
 
 // vectorGuardScale checks if the image is a vector format and downscales it
 // to the maximum allowed resolution if necessary
-func vectorGuardScale(c *Context) error {
+func (p *Processor) vectorGuardScale(c *Context) error {
 	if c.ImgData == nil || !c.ImgData.Format().IsVector() {
 		return nil
 	}
@@ -19,6 +19,7 @@ func vectorGuardScale(c *Context) error {
 			return err
 		}
 	}
+	c.CalcParams()
 
 	return nil
 }

+ 20 - 22
processing/watermark.go

@@ -4,7 +4,6 @@ import (
 	"context"
 	"math"
 
-	"github.com/imgproxy/imgproxy/v3/auximageprovider"
 	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/imath"
@@ -12,24 +11,27 @@ import (
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-var watermarkPipeline = Pipeline{
-	vectorGuardScale,
-	prepare,
-	scaleOnLoad,
-	colorspaceToProcessing,
-	scale,
-	rotateAndFlip,
-	padding,
+// watermarkPipeline constructs the watermark processing pipeline.
+// This pipeline is applied to the watermark image.
+func (p *Processor) watermarkPipeline() Pipeline {
+	return Pipeline{
+		p.vectorGuardScale,
+		p.scaleOnLoad,
+		p.colorspaceToProcessing,
+		p.scale,
+		p.rotateAndFlip,
+		p.padding,
+	}
 }
 
-func prepareWatermark(
+func (p *Processor) prepareWatermark(
 	ctx context.Context,
-	runner *Runner,
 	wm *vips.Image,
 	wmData imagedata.ImageData,
 	po *options.ProcessingOptions,
 	imgWidth, imgHeight int,
 	offsetScale float64,
+	framesCount int,
 ) error {
 	if err := wm.Load(wmData, 1, 1.0, 1); err != nil {
 		return err
@@ -70,7 +72,7 @@ func prepareWatermark(
 		wmPo.Padding.Bottom = offY - wmPo.Padding.Top
 	}
 
-	if err := runner.Run(watermarkPipeline, ctx, wm, wmPo, wmData); err != nil {
+	if err := p.watermarkPipeline().Run(ctx, wm, wmPo, wmData); err != nil {
 		return err
 	}
 
@@ -90,20 +92,18 @@ func prepareWatermark(
 	return wm.StripAll()
 }
 
-func applyWatermark(
+func (p *Processor) applyWatermark(
 	ctx context.Context,
-	runner *Runner,
 	img *vips.Image,
-	watermark auximageprovider.Provider,
 	po *options.ProcessingOptions,
 	offsetScale float64,
 	framesCount int,
 ) error {
-	if watermark == nil {
+	if p.watermarkProvider == nil {
 		return nil
 	}
 
-	wmData, _, err := watermark.Get(ctx, po)
+	wmData, _, err := p.watermarkProvider.Get(ctx, po)
 	if err != nil {
 		return err
 	}
@@ -121,9 +121,7 @@ func applyWatermark(
 	height := img.Height()
 	frameHeight := height / framesCount
 
-	if err := prepareWatermark(
-		ctx, runner, wm, wmData, po, width, frameHeight, offsetScale,
-	); err != nil {
+	if err := p.prepareWatermark(ctx, wm, wmData, po, width, frameHeight, offsetScale, framesCount); err != nil {
 		return err
 	}
 
@@ -197,10 +195,10 @@ func applyWatermark(
 	return nil
 }
 
-func watermark(c *Context) error {
+func (p *Processor) watermark(c *Context) error {
 	if !c.PO.Watermark.Enabled || c.WatermarkProvider == nil {
 		return nil
 	}
 
-	return applyWatermark(c.Ctx, c.Runner(), c.Img, c.WatermarkProvider, c.PO, c.DprScale, 1)
+	return p.applyWatermark(c.Ctx, c.Img, c.PO, c.DprScale, 1)
 }