diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f703ab..c9e24eec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ - (pro) Add `video_meta` to the `/info` response. - Add 1/2/4-bit BMP support. +### Change +- Optimized `crop`. + ### Fix - Fix Datadog support. diff --git a/imath/imath.go b/imath/imath.go index 42a6e3a1..72349ee5 100644 --- a/imath/imath.go +++ b/imath/imath.go @@ -38,3 +38,11 @@ func Scale(a int, scale float64) int { return Round(float64(a) * scale) } + +func Shrink(a int, shrink float64) int { + if a == 0 { + return 0 + } + + return Round(float64(a) / shrink) +} diff --git a/options/gravity_options.go b/options/gravity_options.go new file mode 100644 index 00000000..41ac2759 --- /dev/null +++ b/options/gravity_options.go @@ -0,0 +1,158 @@ +package options + +import ( + "fmt" +) + +type GravityType int + +const ( + GravityUnknown GravityType = iota + GravityCenter + GravityNorth + GravityEast + GravitySouth + GravityWest + GravityNorthWest + GravityNorthEast + GravitySouthWest + GravitySouthEast + GravitySmart + GravityFocusPoint +) + +var gravityTypes = map[string]GravityType{ + "ce": GravityCenter, + "no": GravityNorth, + "ea": GravityEast, + "so": GravitySouth, + "we": GravityWest, + "nowe": GravityNorthWest, + "noea": GravityNorthEast, + "sowe": GravitySouthWest, + "soea": GravitySouthEast, + "sm": GravitySmart, + "fp": GravityFocusPoint, +} + +var gravityTypesRotationMap = map[int]map[GravityType]GravityType{ + 90: { + GravityNorth: GravityWest, + GravityEast: GravityNorth, + GravitySouth: GravityEast, + GravityWest: GravitySouth, + GravityNorthWest: GravitySouthWest, + GravityNorthEast: GravityNorthWest, + GravitySouthWest: GravitySouthEast, + GravitySouthEast: GravityNorthEast, + }, + 180: { + GravityNorth: GravitySouth, + GravityEast: GravityWest, + GravitySouth: GravityNorth, + GravityWest: GravityEast, + GravityNorthWest: GravitySouthEast, + GravityNorthEast: GravitySouthWest, + GravitySouthWest: GravityNorthEast, + GravitySouthEast: GravityNorthWest, + }, + 270: { + GravityNorth: GravityEast, + GravityEast: GravitySouth, + GravitySouth: GravityWest, + GravityWest: GravityNorth, + GravityNorthWest: GravityNorthEast, + GravityNorthEast: GravitySouthEast, + GravitySouthWest: GravityNorthWest, + GravitySouthEast: GravitySouthWest, + }, +} + +var gravityTypesFlipMap = map[GravityType]GravityType{ + GravityEast: GravityWest, + GravityWest: GravityEast, + GravityNorthWest: GravityNorthEast, + GravityNorthEast: GravityNorthWest, + GravitySouthWest: GravitySouthEast, + GravitySouthEast: GravitySouthWest, +} + +func (gt GravityType) String() string { + for k, v := range gravityTypes { + if v == gt { + return k + } + } + return "" +} + +func (gt GravityType) MarshalJSON() ([]byte, error) { + for k, v := range gravityTypes { + if v == gt { + return []byte(fmt.Sprintf("%q", k)), nil + } + } + return []byte("null"), nil +} + +type GravityOptions struct { + Type GravityType + X, Y float64 +} + +func (g *GravityOptions) RotateAndFlip(angle int, flip bool) { + angle %= 360 + + if flip { + if gt, ok := gravityTypesFlipMap[g.Type]; ok { + g.Type = gt + } + + switch g.Type { + case GravityCenter, GravityNorth, GravitySouth: + g.X = -g.X + case GravityFocusPoint: + g.X = 1.0 - g.X + } + } + + if angle > 0 { + if rotMap := gravityTypesRotationMap[angle]; rotMap != nil { + if gt, ok := rotMap[g.Type]; ok { + g.Type = gt + } + + switch angle { + case 90: + switch g.Type { + case GravityCenter, GravityEast, GravityWest: + g.X, g.Y = g.Y, -g.X + case GravityFocusPoint: + g.X, g.Y = g.Y, 1.0-g.X + default: + g.X, g.Y = g.Y, g.X + } + case 180: + switch g.Type { + case GravityCenter: + g.X, g.Y = -g.X, -g.Y + case GravityNorth, GravitySouth: + g.X = -g.X + case GravityEast, GravityWest: + g.Y = -g.Y + case GravityFocusPoint: + g.X, g.Y = 1.0-g.X, 1.0-g.Y + } + case 270: + switch g.Type { + case GravityCenter, GravityNorth, GravitySouth: + g.X, g.Y = -g.Y, g.X + case GravityFocusPoint: + g.X, g.Y = 1.0-g.Y, g.X + default: + g.X, g.Y = g.Y, g.X + } + } + } + } +} diff --git a/options/gravity_type.go b/options/gravity_type.go deleted file mode 100644 index 0354bbe4..00000000 --- a/options/gravity_type.go +++ /dev/null @@ -1,52 +0,0 @@ -package options - -import "fmt" - -type GravityType int - -const ( - GravityUnknown GravityType = iota - GravityCenter - GravityNorth - GravityEast - GravitySouth - GravityWest - GravityNorthWest - GravityNorthEast - GravitySouthWest - GravitySouthEast - GravitySmart - GravityFocusPoint -) - -var gravityTypes = map[string]GravityType{ - "ce": GravityCenter, - "no": GravityNorth, - "ea": GravityEast, - "so": GravitySouth, - "we": GravityWest, - "nowe": GravityNorthWest, - "noea": GravityNorthEast, - "sowe": GravitySouthWest, - "soea": GravitySouthEast, - "sm": GravitySmart, - "fp": GravityFocusPoint, -} - -func (gt GravityType) String() string { - for k, v := range gravityTypes { - if v == gt { - return k - } - } - return "" -} - -func (gt GravityType) MarshalJSON() ([]byte, error) { - for k, v := range gravityTypes { - if v == gt { - return []byte(fmt.Sprintf("%q", k)), nil - } - } - return []byte("null"), nil -} diff --git a/options/processing_options.go b/options/processing_options.go index 091598cb..c85c63ea 100644 --- a/options/processing_options.go +++ b/options/processing_options.go @@ -23,11 +23,6 @@ const maxClientHintDPR = 8 var errExpiredURL = errors.New("Expired URL") -type GravityOptions struct { - Type GravityType - X, Y float64 -} - type ExtendOptions struct { Enabled bool Gravity GravityOptions @@ -211,11 +206,7 @@ func parseBoolOption(str string) bool { } func isGravityOffcetValid(gravity GravityType, offset float64) bool { - if gravity == GravityCenter { - return true - } - - return offset >= 0 && (gravity != GravityFocusPoint || offset <= 1) + return gravity != GravityFocusPoint || (offset >= 0 && offset <= 1) } func parseGravity(g *GravityOptions, args []string) error { diff --git a/processing/crop.go b/processing/crop.go index d1ba6bd9..ad020e18 100644 --- a/processing/crop.go +++ b/processing/crop.go @@ -38,10 +38,20 @@ func cropImage(img *vips.Image, cropWidth, cropHeight int, gravity *options.Grav } func crop(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error { - if err := cropImage(img, pctx.cropWidth, pctx.cropHeight, &pctx.cropGravity); err != nil { - return err + width, height := pctx.cropWidth, pctx.cropHeight + + opts := pctx.cropGravity + opts.RotateAndFlip(pctx.angle, pctx.flip) + opts.RotateAndFlip(po.Rotate, false) + + if (pctx.angle+po.Rotate)%180 == 90 { + width, height = height, width } + return cropImage(img, width, height, &opts) +} + +func cropToResult(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error { // Crop image to the result size resultWidth := imath.Scale(po.Width, po.Dpr) resultHeight := imath.Scale(po.Height, po.Dpr) diff --git a/processing/prepare.go b/processing/prepare.go index ecf8a8bb..f896b669 100644 --- a/processing/prepare.go +++ b/processing/prepare.go @@ -161,16 +161,5 @@ func prepare(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptio pctx.wscale, pctx.hscale = calcScale(widthToScale, heightToScale, po, pctx.imgtype) - if pctx.cropWidth > 0 { - pctx.cropWidth = imath.Max(1, imath.Scale(pctx.cropWidth, pctx.wscale)) - } - if pctx.cropHeight > 0 { - pctx.cropHeight = imath.Max(1, imath.Scale(pctx.cropHeight, pctx.hscale)) - } - if pctx.cropGravity.Type != options.GravityFocusPoint { - pctx.cropGravity.X *= pctx.wscale - pctx.cropGravity.Y *= pctx.hscale - } - return nil } diff --git a/processing/processing.go b/processing/processing.go index 8349e56e..600f5af2 100644 --- a/processing/processing.go +++ b/processing/processing.go @@ -23,9 +23,10 @@ var mainPipeline = pipeline{ prepare, scaleOnLoad, importColorProfile, + crop, scale, rotateAndFlip, - crop, + cropToResult, fixWebpSize, applyFilters, extend, diff --git a/processing/scale_on_load.go b/processing/scale_on_load.go index d2f7cdbc..9b50e410 100644 --- a/processing/scale_on_load.go +++ b/processing/scale_on_load.go @@ -58,15 +58,29 @@ func scaleOnLoad(pctx *pipelineContext, img *vips.Image, po *options.ProcessingO // Update scales after scale-on-load newWidth, newHeight, _, _ := extractMeta(img, po.Rotate, po.AutoRotate) - pctx.wscale = float64(pctx.srcWidth) * pctx.wscale / float64(newWidth) + wpreshrink := float64(pctx.srcWidth) / float64(newWidth) + hpreshrink := float64(pctx.srcHeight) / float64(newHeight) + + pctx.wscale = wpreshrink * pctx.wscale if newWidth == imath.Scale(newWidth, pctx.wscale) { pctx.wscale = 1.0 } - pctx.hscale = float64(pctx.srcHeight) * pctx.hscale / float64(newHeight) + pctx.hscale = hpreshrink * pctx.hscale if newHeight == imath.Scale(newHeight, pctx.hscale) { pctx.hscale = 1.0 } + if pctx.cropWidth > 0 { + pctx.cropWidth = imath.Max(1, imath.Shrink(pctx.cropWidth, wpreshrink)) + } + if pctx.cropHeight > 0 { + pctx.cropHeight = imath.Max(1, imath.Shrink(pctx.cropHeight, hpreshrink)) + } + if pctx.cropGravity.Type != options.GravityFocusPoint { + pctx.cropGravity.X /= wpreshrink + pctx.cropGravity.Y /= hpreshrink + } + return nil }