Processor instance

This commit is contained in:
Viktor Sokolov
2025-09-18 10:29:42 +02:00
parent 9de0ea362f
commit 546e8159b0
26 changed files with 314 additions and 318 deletions

View File

@@ -7,6 +7,7 @@ import (
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/options"
"github.com/imgproxy/imgproxy/v3/processing"
"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"
@@ -27,6 +28,7 @@ type Config struct {
Handlers HandlerConfigs Handlers HandlerConfigs
Server server.Config Server server.Config
Security security.Config Security security.Config
Processing processing.Config
Options options.Config Options options.Config
} }
@@ -43,6 +45,7 @@ func NewDefaultConfig() Config {
}, },
Server: server.NewDefaultConfig(), Server: server.NewDefaultConfig(),
Security: security.NewDefaultConfig(), Security: security.NewDefaultConfig(),
Processing: processing.NewDefaultConfig(),
Options: options.NewDefaultConfig(), Options: options.NewDefaultConfig(),
} }
} }
@@ -89,5 +92,9 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
return nil, err return nil, err
} }
if _, err = processing.LoadConfigFromEnv(&c.Processing); err != nil {
return nil, err
}
return c, nil return c, nil
} }

View File

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

View File

@@ -157,7 +157,7 @@ func (r *request) getFallbackImage(
// processImage calls actual image processing // processImage calls actual image processing
func (r *request) processImage(ctx context.Context, originData imagedata.ImageData) (*processing.Result, error) { func (r *request) processImage(ctx context.Context, originData imagedata.ImageData) (*processing.Result, error) {
defer monitoring.StartProcessingSegment(ctx, r.monitoringMeta.Filter(monitoring.MetaProcessingOptions))() 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 // writeDebugHeaders writes debug headers (X-Origin-*, X-Result-*) to the response

View File

@@ -15,6 +15,7 @@ import (
"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/options"
"github.com/imgproxy/imgproxy/v3/processing"
"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"
@@ -31,6 +32,7 @@ type ImgproxyHandlers struct {
Landing *landinghandler.Handler Landing *landinghandler.Handler
Processing *processinghandler.Handler Processing *processinghandler.Handler
Stream *streamhandler.Handler Stream *streamhandler.Handler
Processor *processing.Processor
} }
// Imgproxy holds all the components needed for imgproxy to function // Imgproxy holds all the components needed for imgproxy to function
@@ -43,6 +45,7 @@ type Imgproxy struct {
handlers ImgproxyHandlers handlers ImgproxyHandlers
security *security.Checker security *security.Checker
optionsFactory *options.Factory optionsFactory *options.Factory
processor *processing.Processor
config *Config config *Config
} }
@@ -80,6 +83,11 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
return nil, err return nil, err
} }
processor, err := processing.New(&config.Processing, watermarkImage)
if err != nil {
return nil, err
}
imgproxy := &Imgproxy{ imgproxy := &Imgproxy{
workers: workers, workers: workers,
fallbackImage: fallbackImage, fallbackImage: fallbackImage,
@@ -89,6 +97,7 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
config: config, config: config,
security: security, security: security,
optionsFactory: processingOptionsFactory, optionsFactory: processingOptionsFactory,
processor: processor,
} }
imgproxy.handlers.Health = healthhandler.New() imgproxy.handlers.Health = healthhandler.New()
@@ -211,3 +220,7 @@ func (i *Imgproxy) Security() *security.Checker {
func (i *Imgproxy) OptionsFactory() *options.Factory { func (i *Imgproxy) OptionsFactory() *options.Factory {
return i.optionsFactory return i.optionsFactory
} }
func (i *Imgproxy) Processor() *processing.Processor {
return i.processor
}

View File

@@ -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/processing"
"github.com/imgproxy/imgproxy/v3/vips" "github.com/imgproxy/imgproxy/v3/vips"
"go.uber.org/automaxprocs/maxprocs" "go.uber.org/automaxprocs/maxprocs"
) )
@@ -47,11 +46,6 @@ func Init() error {
errorreport.Init() errorreport.Init()
if err := processing.ValidatePreferredFormats(); err != nil {
vips.Shutdown()
return err
}
return nil return nil
} }

View File

@@ -1,6 +1,6 @@
package processing 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 { if c.PO.Blur == 0 && c.PO.Sharpen == 0 && c.PO.Pixelate <= 1 {
return nil return nil
} }

View File

@@ -1,6 +1,6 @@
package processing package processing
func colorspaceToProcessing(c *Context) error { func (p *Processor) colorspaceToProcessing(c *Context) error {
if c.Img.ColourProfileImported() { if c.Img.ColourProfileImported() {
return nil return nil
} }
@@ -9,7 +9,7 @@ func colorspaceToProcessing(c *Context) error {
return err 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() { if c.Img.IsLinear() {
// The image is linear. If we keep its ICC, we'll get wrong colors after // The image is linear. If we keep its ICC, we'll get wrong colors after

View File

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

View File

@@ -1,14 +1,18 @@
package pipeline package processing
import ( import (
"errors" "errors"
"github.com/imgproxy/imgproxy/v3/config" "github.com/imgproxy/imgproxy/v3/config"
"github.com/imgproxy/imgproxy/v3/ensure" "github.com/imgproxy/imgproxy/v3/ensure"
"github.com/imgproxy/imgproxy/v3/imagetype"
"github.com/imgproxy/imgproxy/v3/vips"
log "github.com/sirupsen/logrus"
) )
// Config holds pipeline-related configuration. // Config holds pipeline-related configuration.
type Config struct { type Config struct {
PreferredFormats []imagetype.Type
WatermarkOpacity float64 WatermarkOpacity float64
DisableShrinkOnLoad bool DisableShrinkOnLoad bool
UseLinearColorspace bool UseLinearColorspace bool
@@ -18,6 +22,11 @@ type Config struct {
func NewDefaultConfig() Config { func NewDefaultConfig() Config {
return Config{ return Config{
WatermarkOpacity: 1, WatermarkOpacity: 1,
PreferredFormats: []imagetype.Type{
imagetype.JPEG,
imagetype.PNG,
imagetype.GIF,
},
} }
} }
@@ -28,6 +37,7 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
c.WatermarkOpacity = config.WatermarkOpacity c.WatermarkOpacity = config.WatermarkOpacity
c.DisableShrinkOnLoad = config.DisableShrinkOnLoad c.DisableShrinkOnLoad = config.DisableShrinkOnLoad
c.UseLinearColorspace = config.UseLinearColorspace c.UseLinearColorspace = config.UseLinearColorspace
c.PreferredFormats = config.PreferredFormats
return c, nil return c, nil
} }
@@ -40,5 +50,21 @@ func (c *Config) Validate() error {
return errors.New("watermark opacity should be less than or equal to 1") 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) {
log.Warnf("%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 return nil
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,21 +6,14 @@ import (
"github.com/imgproxy/imgproxy/v3/auximageprovider" "github.com/imgproxy/imgproxy/v3/auximageprovider"
"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/processing/pipeline"
"github.com/imgproxy/imgproxy/v3/server" "github.com/imgproxy/imgproxy/v3/server"
"github.com/imgproxy/imgproxy/v3/vips" "github.com/imgproxy/imgproxy/v3/vips"
) )
// NOTE: this will be called pipeline.Context in the separate package
type Context struct { type Context struct {
// The runner that runs this pipeline // The context to check for timeouts and cancellations
runner *Runner
Ctx context.Context Ctx context.Context
// Global processing configuration which could be used by individual steps
Config *pipeline.Config
// VIPS image // VIPS image
Img *vips.Image Img *vips.Image
@@ -80,33 +73,18 @@ type Context struct {
ExtendAspectRatioHeight int ExtendAspectRatioHeight int
} }
// NOTE: same, pipeline.Step, pipeline.Pipeline, pipeline.Runner type Step func(c *Context) error
type Step func(ctx *Context) error
type Pipeline []Step 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 // Run runs the given pipeline with the given parameters
func (f *Runner) Run( func (p Pipeline) Run(
p Pipeline,
ctx context.Context, ctx context.Context,
img *vips.Image, img *vips.Image,
po *options.ProcessingOptions, po *options.ProcessingOptions,
imgdata imagedata.ImageData, imgdata imagedata.ImageData,
) error { ) error {
pctx := f.newContext(ctx, img, po, imgdata) pctx := p.newContext(ctx, img, po, imgdata)
pctx.CalcParams() // calc initial params if not done before
for _, step := range p { for _, step := range p {
if err := step(&pctx); err != nil { if err := step(&pctx); err != nil {
@@ -123,17 +101,14 @@ func (f *Runner) Run(
return nil return nil
} }
func (r *Runner) newContext( func (p Pipeline) newContext(
ctx context.Context, ctx context.Context,
img *vips.Image, img *vips.Image,
po *options.ProcessingOptions, po *options.ProcessingOptions,
imgdata imagedata.ImageData, imgdata imagedata.ImageData,
) Context { ) Context {
pctx := Context{ pctx := Context{
runner: r,
Ctx: ctx, Ctx: ctx,
Config: r.config,
Img: img, Img: img,
PO: po, PO: po,
ImgData: imgdata, ImgData: imgdata,
@@ -145,7 +120,6 @@ func (r *Runner) newContext(
VectorBaseScale: 1.0, VectorBaseScale: 1.0,
CropGravity: po.Crop.Gravity, CropGravity: po.Crop.Gravity,
WatermarkProvider: r.watermark,
} }
if pctx.CropGravity.Type == options.GravityUnknown { if pctx.CropGravity.Type == options.GravityUnknown {
@@ -154,7 +128,3 @@ func (r *Runner) newContext(
return pctx return pctx
} }
func (c *Context) Runner() *Runner {
return c.runner
}

View File

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

View File

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

View File

@@ -29,6 +29,8 @@ type ProcessingTestSuite struct {
security testutil.LazyObj[*security.Checker] security testutil.LazyObj[*security.Checker]
poConfig testutil.LazyObj[*options.Config] poConfig testutil.LazyObj[*options.Config]
po testutil.LazyObj[*options.Factory] po testutil.LazyObj[*options.Factory]
config testutil.LazyObj[*Config]
processor testutil.LazyObj[*Processor]
} }
func (s *ProcessingTestSuite) SetupSuite() { func (s *ProcessingTestSuite) SetupSuite() {
@@ -68,6 +70,15 @@ func (s *ProcessingTestSuite) SetupSuite() {
s.po, _ = testutil.NewLazySuiteObj(s, func() (*options.Factory, error) { s.po, _ = testutil.NewLazySuiteObj(s, func() (*options.Factory, error) {
return options.NewFactory(s.poConfig(), s.security()) 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) openFile(name string) imagedata.ImageData { func (s *ProcessingTestSuite) openFile(name string) imagedata.ImageData {
@@ -116,7 +127,7 @@ func (s *ProcessingTestSuite) TestResizeToFit() {
po.Width = tc.width po.Width = tc.width
po.Height = tc.height po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil) result, err := s.processor().ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(result) s.Require().NotNil(result)
@@ -155,7 +166,7 @@ func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
po.Width = tc.width po.Width = tc.width
po.Height = tc.height po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil) result, err := s.processor().ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(result) s.Require().NotNil(result)
@@ -199,7 +210,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtend() {
po.Width = tc.width po.Width = tc.width
po.Height = tc.height po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil) result, err := s.processor().ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(result) s.Require().NotNil(result)
@@ -243,7 +254,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
po.Width = tc.width po.Width = tc.width
po.Height = tc.height po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil) result, err := s.processor().ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(result) s.Require().NotNil(result)
@@ -287,7 +298,7 @@ func (s *ProcessingTestSuite) TestResizeToFill() {
po.Width = tc.width po.Width = tc.width
po.Height = tc.height po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil) result, err := s.processor().ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(result) s.Require().NotNil(result)
@@ -326,7 +337,7 @@ func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
po.Width = tc.width po.Width = tc.width
po.Height = tc.height po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil) result, err := s.processor().ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(result) s.Require().NotNil(result)
@@ -372,7 +383,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtend() {
po.Width = tc.width po.Width = tc.width
po.Height = tc.height po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil) result, err := s.processor().ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(result) s.Require().NotNil(result)
@@ -418,7 +429,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
po.Width = tc.width po.Width = tc.width
po.Height = tc.height po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil) result, err := s.processor().ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(result) s.Require().NotNil(result)
@@ -456,7 +467,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDown() {
po.Width = tc.width po.Width = tc.width
po.Height = tc.height po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil) result, err := s.processor().ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(result) s.Require().NotNil(result)
@@ -495,7 +506,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
po.Width = tc.width po.Width = tc.width
po.Height = tc.height po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil) result, err := s.processor().ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(result) s.Require().NotNil(result)
@@ -541,7 +552,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
po.Width = tc.width po.Width = tc.width
po.Height = tc.height po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil) result, err := s.processor().ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(result) s.Require().NotNil(result)
@@ -585,7 +596,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
po.Width = tc.width po.Width = tc.width
po.Height = tc.height po.Height = tc.height
result, err := ProcessImage(context.Background(), imgdata, po, nil) result, err := s.processor().ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(result) s.Require().NotNil(result)
@@ -1014,7 +1025,7 @@ func (s *ProcessingTestSuite) TestResultSizeLimit() {
po.Rotate = tc.rotate po.Rotate = tc.rotate
po.Padding = tc.padding po.Padding = tc.padding
result, err := ProcessImage(context.Background(), imgdata, po, nil) result, err := s.processor().ProcessImage(context.Background(), imgdata, po, nil)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(result) s.Require().NotNil(result)
@@ -1029,7 +1040,7 @@ func (s *ProcessingTestSuite) TestImageResolutionTooLarge() {
po.SecurityOptions.MaxSrcResolution = 1 po.SecurityOptions.MaxSrcResolution = 1
imgdata := s.openFile("test2.jpg") 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().Error(err)
s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode()) s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode())

23
processing/processor.go Normal file
View File

@@ -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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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