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

View File

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

View File

@@ -157,7 +157,7 @@ func (r *request) getFallbackImage(
// processImage calls actual image processing
func (r *request) processImage(ctx context.Context, originData imagedata.ImageData) (*processing.Result, error) {
defer monitoring.StartProcessingSegment(ctx, r.monitoringMeta.Filter(monitoring.MetaProcessingOptions))()
return processing.ProcessImage(ctx, originData, r.po, r.WatermarkImage())
return r.Processor().ProcessImage(ctx, originData, r.po, r.WatermarkImage())
}
// 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/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"
@@ -31,6 +32,7 @@ type ImgproxyHandlers struct {
Landing *landinghandler.Handler
Processing *processinghandler.Handler
Stream *streamhandler.Handler
Processor *processing.Processor
}
// Imgproxy holds all the components needed for imgproxy to function
@@ -43,6 +45,7 @@ type Imgproxy struct {
handlers ImgproxyHandlers
security *security.Checker
optionsFactory *options.Factory
processor *processing.Processor
config *Config
}
@@ -80,6 +83,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 +97,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 +220,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
}

View File

@@ -10,7 +10,6 @@ import (
"github.com/imgproxy/imgproxy/v3/gliblog"
"github.com/imgproxy/imgproxy/v3/logger"
"github.com/imgproxy/imgproxy/v3/monitoring"
"github.com/imgproxy/imgproxy/v3/processing"
"github.com/imgproxy/imgproxy/v3/vips"
"go.uber.org/automaxprocs/maxprocs"
)
@@ -47,11 +46,6 @@ func Init() error {
errorreport.Init()
if err := processing.ValidatePreferredFormats(); err != nil {
vips.Shutdown()
return err
}
return nil
}

View File

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

View File

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

View File

@@ -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() {

View File

@@ -1,14 +1,18 @@
package pipeline
package processing
import (
"errors"
"github.com/imgproxy/imgproxy/v3/config"
"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.
type Config struct {
PreferredFormats []imagetype.Type
WatermarkOpacity float64
DisableShrinkOnLoad bool
UseLinearColorspace bool
@@ -18,6 +22,11 @@ type Config struct {
func NewDefaultConfig() Config {
return Config{
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.DisableShrinkOnLoad = config.DisableShrinkOnLoad
c.UseLinearColorspace = config.UseLinearColorspace
c.PreferredFormats = config.PreferredFormats
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")
}
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
}

View File

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

View File

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

View File

@@ -88,7 +88,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)

View File

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

View File

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

View File

@@ -6,21 +6,14 @@ 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
Img *vips.Image
@@ -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() // calc initial params if not done before
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
}

View File

@@ -8,29 +8,12 @@ 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 (c *Context) ExtractGeometry(img *vips.Image, baseAngle int, autoRotate bool) (int, int, int, bool) {
width := img.Width()
height := img.Height()
angle := 0
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
}
}
angle, flip := c.angleFlip(img, autoRotate)
if (angle+baseAngle)%180 != 0 {
width, height = height, width
@@ -39,7 +22,39 @@ func extractMeta(img *vips.Image, baseAngle int, useOrientation bool) (int, int,
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 {
case crop == 0.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
srcW, srcH := float64(width), float64(height)
dstW, dstH := float64(po.Width), float64(po.Height)
if po.Width == 0 {
dstW = srcW
}
if dstW == srcW {
wshrink = 1
} else {
wshrink = srcW / dstW
}
if po.Height == 0 {
dstH = srcH
}
if dstH == srcH {
hshrink = 1
} else {
hshrink = srcH / dstH
}
dstW, wshrink = calcShrink(po.Width, srcW, dstW)
dstH, hshrink = calcShrink(po.Height, srcH, dstH)
if wshrink != 1 || hshrink != 1 {
rt := po.ResizingType
@@ -107,22 +119,22 @@ func (pctx *Context) calcScale(width, height int, po *options.ProcessingOptions)
wshrink /= po.ZoomWidth
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)
if minShrink < 1 {
wshrink /= minShrink
hshrink /= minShrink
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 {
@@ -139,8 +151,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
@@ -150,110 +162,113 @@ 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() {
if c.ImgData == nil {
return
}
c.CropWidth = calcCropSize(c.SrcWidth, c.PO.Crop.Width)
c.CropHeight = calcCropSize(c.SrcHeight, c.PO.Crop.Height)
c.SrcWidth, c.SrcHeight, c.Angle, c.Flip = c.ExtractGeometry(c.Img, c.PO.Rotate, c.PO.AutoRotate)
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)
@@ -261,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
}

View File

@@ -2,7 +2,6 @@ package processing
import (
"context"
"errors"
"runtime"
"slices"
@@ -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,
// 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,
}
}
// 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) {
log.Warnf("%s can't be a preferred format as it's saving is not supported", t)
} else {
filtered = append(filtered, t)
}
// finalizePipeline constructs the finalization pipeline.
// This pipeline is applied before saving the image.
func (p *Processor) finalizePipeline() Pipeline {
return Pipeline{
p.colorspaceToResult,
p.stripMetadata,
}
if len(filtered) == 0 {
return errors.New("no supported preferred formats specified")
}
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 {
log.Warning("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
}

View File

@@ -29,6 +29,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() {
@@ -68,6 +70,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) openFile(name string) imagedata.ImageData {
@@ -116,7 +127,7 @@ func (s *ProcessingTestSuite) TestResizeToFit() {
po.Width = tc.width
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().NotNil(result)
@@ -155,7 +166,7 @@ func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
po.Width = tc.width
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().NotNil(result)
@@ -199,7 +210,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtend() {
po.Width = tc.width
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().NotNil(result)
@@ -243,7 +254,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
po.Width = tc.width
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().NotNil(result)
@@ -287,7 +298,7 @@ func (s *ProcessingTestSuite) TestResizeToFill() {
po.Width = tc.width
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().NotNil(result)
@@ -326,7 +337,7 @@ func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
po.Width = tc.width
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().NotNil(result)
@@ -372,7 +383,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtend() {
po.Width = tc.width
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().NotNil(result)
@@ -418,7 +429,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
po.Width = tc.width
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().NotNil(result)
@@ -456,7 +467,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDown() {
po.Width = tc.width
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().NotNil(result)
@@ -495,7 +506,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
po.Width = tc.width
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().NotNil(result)
@@ -541,7 +552,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
po.Width = tc.width
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().NotNil(result)
@@ -585,7 +596,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
po.Width = tc.width
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().NotNil(result)
@@ -1014,7 +1025,7 @@ func (s *ProcessingTestSuite) TestResultSizeLimit() {
po.Rotate = tc.rotate
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().NotNil(result)
@@ -1029,7 +1040,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
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
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)
}

View File

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

View File

@@ -12,7 +12,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
}
@@ -21,7 +21,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
}
@@ -44,7 +44,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)
@@ -55,7 +55,7 @@ func scaleOnLoad(c *Context) error {
prescale *= c.VectorBaseScale
}
if !canScaleOnLoad(c, c.ImgData, prescale) {
if !p.canScaleOnLoad(c, c.ImgData, prescale) {
return nil
}
@@ -71,7 +71,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 = c.ExtractGeometry(thumbnail, c.PO.Rotate, c.PO.AutoRotate)
if newWidth >= c.SrcWidth || float64(newWidth)/float64(c.SrcWidth) < prescale {
return nil
@@ -91,7 +91,7 @@ func scaleOnLoad(c *Context) error {
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

View File

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

View File

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

View File

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

View File

@@ -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,25 +11,20 @@ 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(
ctx context.Context,
runner *Runner,
wm *vips.Image,
wmData imagedata.ImageData,
po *options.ProcessingOptions,
imgWidth, imgHeight int,
offsetScale float64,
) error {
func (p *Processor) prepareWatermark(wm *vips.Image, wmData imagedata.ImageData, po *options.ProcessingOptions, imgWidth, imgHeight int, offsetScale float64, framesCount int) error {
if err := wm.Load(wmData, 1, 1.0, 1); err != nil {
return err
}
@@ -70,7 +64,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(context.Background(), wm, wmPo, wmData); err != nil {
return err
}
@@ -90,20 +84,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 +113,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(wm, wmData, po, width, frameHeight, offsetScale, framesCount); err != nil {
return err
}
@@ -197,10 +187,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)
}