Browse Source

Fix the way the `dpr` processing option affects offsets and paddings

DarthSim 2 years ago
parent
commit
2c28252966

+ 1 - 0
CHANGELOG.md

@@ -7,6 +7,7 @@
 
 ### Fix
 - Fix detection of dead HTTP/2 connections.
+- Fix the way the `dpr` processing option affects offsets and paddings.
 
 ### Remove
 - Remove suport for `Viewport-Width` client hint.

+ 4 - 0
docs/generating_the_url.md

@@ -135,6 +135,8 @@ When set, imgproxy will multiply the image dimensions according to these factors
 
 Can be combined with `width` and `height` options. In this case, imgproxy calculates scale factors for the provided size and then multiplies it with the provided zoom factors.
 
+**📝 Note:** Unlike the `dpr` option, the `zoom` option doesn't affect gravities offsets, watermark offsets, and paddings.
+
 **📝 Note:** Unlike [dpr](#dpr), `zoom` doesn't set the `Content-DPR` header in the response.
 
 Default: `1`
@@ -147,6 +149,8 @@ dpr:%dpr
 
 When set, imgproxy will multiply the image dimensions according to this factor for HiDPI (Retina) devices. The value must be greater than 0.
 
+**📝 Note:** The `dpr` option affects gravities offsets, watermark offsets, and paddings to make the resulting image structures with and without the `dpr` option applied match.
+
 **📝 Note:** `dpr` also sets the `Content-DPR` header in the response so the browser can correctly render the image.
 
 Default: `1`

+ 12 - 0
imath/imath.go

@@ -31,6 +31,10 @@ func Round(a float64) int {
 	return int(math.Round(a))
 }
 
+func RoundToEven(a float64) int {
+	return int(math.RoundToEven(a))
+}
+
 func Scale(a int, scale float64) int {
 	if a == 0 {
 		return 0
@@ -39,6 +43,14 @@ func Scale(a int, scale float64) int {
 	return Round(float64(a) * scale)
 }
 
+func ScaleToEven(a int, scale float64) int {
+	if a == 0 {
+		return 0
+	}
+
+	return RoundToEven(float64(a) * scale)
+}
+
 func Shrink(a int, shrink float64) int {
 	if a == 0 {
 		return 0

+ 2 - 2
processing/calc_position.go

@@ -5,7 +5,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/options"
 )
 
-func calcPosition(width, height, innerWidth, innerHeight int, gravity *options.GravityOptions, allowOverflow bool) (left, top int) {
+func calcPosition(width, height, innerWidth, innerHeight int, gravity *options.GravityOptions, dpr float64, allowOverflow bool) (left, top int) {
 	if gravity.Type == options.GravityFocusPoint {
 		pointX := imath.Scale(width, gravity.X)
 		pointY := imath.Scale(height, gravity.Y)
@@ -13,7 +13,7 @@ func calcPosition(width, height, innerWidth, innerHeight int, gravity *options.G
 		left = pointX - innerWidth/2
 		top = pointY - innerHeight/2
 	} else {
-		offX, offY := int(gravity.X), int(gravity.Y)
+		offX, offY := int(gravity.X*dpr), int(gravity.Y*dpr)
 
 		left = (width-innerWidth+1)/2 + offX
 		top = (height-innerHeight+1)/2 + offY

+ 6 - 5
processing/crop.go

@@ -7,7 +7,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-func cropImage(img *vips.Image, cropWidth, cropHeight int, gravity *options.GravityOptions) error {
+func cropImage(img *vips.Image, cropWidth, cropHeight int, gravity *options.GravityOptions, offsetScale float64) error {
 	if cropWidth == 0 && cropHeight == 0 {
 		return nil
 	}
@@ -28,7 +28,7 @@ func cropImage(img *vips.Image, cropWidth, cropHeight int, gravity *options.Grav
 		return img.SmartCrop(cropWidth, cropHeight)
 	}
 
-	left, top := calcPosition(imgWidth, imgHeight, cropWidth, cropHeight, gravity, false)
+	left, top := calcPosition(imgWidth, imgHeight, cropWidth, cropHeight, gravity, offsetScale, false)
 	return img.Crop(left, top, cropWidth, cropHeight)
 }
 
@@ -43,12 +43,13 @@ func crop(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions,
 		width, height = height, width
 	}
 
-	return cropImage(img, width, height, &opts)
+	// Since we crop before scaling, we shouldn't consider DPR
+	return cropImage(img, width, height, &opts, 1.0)
 }
 
 func cropToResult(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
 	// Crop image to the result size
-	resultWidth, resultHeight := resultSize(po)
+	resultWidth, resultHeight := resultSize(po, pctx.dprScale)
 
 	if po.ResizingType == options.ResizeFillDown {
 		diffW := float64(resultWidth) / float64(img.Width())
@@ -65,5 +66,5 @@ func cropToResult(pctx *pipelineContext, img *vips.Image, po *options.Processing
 		}
 	}
 
-	return cropImage(img, resultWidth, resultHeight, &po.Gravity)
+	return cropImage(img, resultWidth, resultHeight, &po.Gravity, pctx.dprScale)
 }

+ 6 - 6
processing/extend.go

@@ -7,7 +7,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-func extendImage(img *vips.Image, resultWidth, resultHeight int, opts *options.ExtendOptions, extendAr bool) error {
+func extendImage(img *vips.Image, resultWidth, resultHeight int, opts *options.ExtendOptions, offsetScale float64, extendAr bool) error {
 	if !opts.Enabled || (resultWidth <= img.Width() && resultHeight <= img.Height()) {
 		return nil
 	}
@@ -31,16 +31,16 @@ func extendImage(img *vips.Image, resultWidth, resultHeight int, opts *options.E
 		}
 	}
 
-	offX, offY := calcPosition(resultWidth, resultHeight, img.Width(), img.Height(), &opts.Gravity, false)
+	offX, offY := calcPosition(resultWidth, resultHeight, img.Width(), img.Height(), &opts.Gravity, offsetScale, false)
 	return img.Embed(resultWidth, resultHeight, offX, offY)
 }
 
 func extend(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
-	resultWidth, resultHeight := resultSize(po)
-	return extendImage(img, resultWidth, resultHeight, &po.Extend, false)
+	resultWidth, resultHeight := resultSize(po, pctx.dprScale)
+	return extendImage(img, resultWidth, resultHeight, &po.Extend, pctx.dprScale, false)
 }
 
 func extendAspectRatio(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
-	resultWidth, resultHeight := resultSize(po)
-	return extendImage(img, resultWidth, resultHeight, &po.ExtendAspectRatio, true)
+	resultWidth, resultHeight := resultSize(po, pctx.dprScale)
+	return extendImage(img, resultWidth, resultHeight, &po.ExtendAspectRatio, pctx.dprScale, true)
 }

+ 4 - 4
processing/padding.go

@@ -12,10 +12,10 @@ func padding(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptio
 		return nil
 	}
 
-	paddingTop := imath.Scale(po.Padding.Top, po.Dpr)
-	paddingRight := imath.Scale(po.Padding.Right, po.Dpr)
-	paddingBottom := imath.Scale(po.Padding.Bottom, po.Dpr)
-	paddingLeft := imath.Scale(po.Padding.Left, po.Dpr)
+	paddingTop := imath.Scale(po.Padding.Top, pctx.dprScale)
+	paddingRight := imath.Scale(po.Padding.Right, pctx.dprScale)
+	paddingBottom := imath.Scale(po.Padding.Bottom, pctx.dprScale)
+	paddingLeft := imath.Scale(po.Padding.Left, pctx.dprScale)
 
 	return img.Embed(
 		img.Width()+paddingLeft+paddingRight,

+ 4 - 0
processing/pipeline.go

@@ -29,6 +29,8 @@ type pipelineContext struct {
 	wscale float64
 	hscale float64
 
+	dprScale float64
+
 	iccImported bool
 }
 
@@ -59,5 +61,7 @@ func (p pipeline) Run(ctx context.Context, img *vips.Image, po *options.Processi
 		}
 	}
 
+	img.SetDouble("imgproxy-dpr-scale", pctx.dprScale)
+
 	return nil
 }

+ 17 - 13
processing/prepare.go

@@ -41,7 +41,7 @@ func extractMeta(img *vips.Image, baseAngle int, useOrientation bool) (int, int,
 	return width, height, angle, flip
 }
 
-func calcScale(width, height int, po *options.ProcessingOptions, imgtype imagetype.Type) (float64, float64) {
+func calcScale(width, height int, po *options.ProcessingOptions, imgtype imagetype.Type) (float64, float64, float64) {
 	var wshrink, hshrink float64
 
 	srcW, srcH := float64(width), float64(height)
@@ -67,9 +67,6 @@ func calcScale(width, height int, po *options.ProcessingOptions, imgtype imagety
 		hshrink = srcH / dstH
 	}
 
-	wshrink /= po.Dpr
-	hshrink /= po.Dpr
-
 	if wshrink != 1 || hshrink != 1 {
 		rt := po.ResizingType
 
@@ -101,17 +98,24 @@ func calcScale(width, height int, po *options.ProcessingOptions, imgtype imagety
 	wshrink /= po.ZoomWidth
 	hshrink /= po.ZoomHeight
 
+	dprScale := po.Dpr
+
 	if !po.Enlarge && imgtype != imagetype.SVG {
-		if wshrink < 1 {
-			hshrink /= wshrink
-			wshrink = 1
-		}
-		if hshrink < 1 {
-			wshrink /= hshrink
-			hshrink = 1
+		minShrink := math.Min(wshrink, hshrink)
+		if minShrink < 1 {
+			wshrink /= minShrink
+			hshrink /= minShrink
+
+			if !po.Extend.Enabled {
+				dprScale /= minShrink
+			}
 		}
+		dprScale = math.Min(dprScale, math.Min(wshrink, hshrink))
 	}
 
+	wshrink /= dprScale
+	hshrink /= dprScale
+
 	if po.MinWidth > 0 {
 		if minShrink := srcW / float64(po.MinWidth); minShrink < wshrink {
 			hshrink /= wshrink / minShrink
@@ -134,7 +138,7 @@ func calcScale(width, height int, po *options.ProcessingOptions, imgtype imagety
 		hshrink = srcH
 	}
 
-	return 1.0 / wshrink, 1.0 / hshrink
+	return 1.0 / wshrink, 1.0 / hshrink, dprScale
 }
 
 func calcCropSize(orig int, crop float64) int {
@@ -162,7 +166,7 @@ func prepare(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptio
 	widthToScale := imath.MinNonZero(pctx.cropWidth, pctx.srcWidth)
 	heightToScale := imath.MinNonZero(pctx.cropHeight, pctx.srcHeight)
 
-	pctx.wscale, pctx.hscale = calcScale(widthToScale, heightToScale, po, pctx.imgtype)
+	pctx.wscale, pctx.hscale, pctx.dprScale = calcScale(widthToScale, heightToScale, po, pctx.imgtype)
 
 	return nil
 }

+ 6 - 1
processing/processing.go

@@ -174,7 +174,12 @@ func transformAnimated(ctx context.Context, img *vips.Image, po *options.Process
 	}
 
 	if watermarkEnabled && imagedata.Watermark != nil {
-		if err = applyWatermark(img, imagedata.Watermark, &po.Watermark, framesCount); err != nil {
+		dprScale, derr := img.GetDoubleDefault("imgproxy-dpr-scale", 1.0)
+		if derr != nil {
+			dprScale = 1.0
+		}
+
+		if err = applyWatermark(img, imagedata.Watermark, &po.Watermark, dprScale, framesCount); err != nil {
 			return err
 		}
 	}

+ 3 - 3
processing/result_size.go

@@ -5,9 +5,9 @@ import (
 	"github.com/imgproxy/imgproxy/v3/options"
 )
 
-func resultSize(po *options.ProcessingOptions) (int, int) {
-	resultWidth := imath.Scale(po.Width, po.Dpr*po.ZoomWidth)
-	resultHeight := imath.Scale(po.Height, po.Dpr*po.ZoomHeight)
+func resultSize(po *options.ProcessingOptions, dprScale float64) (int, int) {
+	resultWidth := imath.Scale(po.Width, dprScale*po.ZoomWidth)
+	resultHeight := imath.Scale(po.Height, dprScale*po.ZoomHeight)
 
 	return resultWidth, resultHeight
 }

+ 13 - 9
processing/watermark.go

@@ -2,6 +2,7 @@ package processing
 
 import (
 	"context"
+	"math"
 
 	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/imagedata"
@@ -19,7 +20,7 @@ var watermarkPipeline = pipeline{
 	padding,
 }
 
-func prepareWatermark(wm *vips.Image, wmData *imagedata.ImageData, opts *options.WatermarkOptions, imgWidth, imgHeight, framesCount int) error {
+func prepareWatermark(wm *vips.Image, wmData *imagedata.ImageData, opts *options.WatermarkOptions, imgWidth, imgHeight int, offsetScale float64, framesCount int) error {
 	if err := wm.Load(wmData, 1, 1.0, 1); err != nil {
 		return err
 	}
@@ -36,11 +37,14 @@ func prepareWatermark(wm *vips.Image, wmData *imagedata.ImageData, opts *options
 	}
 
 	if opts.Replicate {
+		offX := int(math.RoundToEven(opts.Gravity.X * offsetScale))
+		offY := int(math.RoundToEven(opts.Gravity.Y * offsetScale))
+
 		po.Padding.Enabled = true
-		po.Padding.Left = int(opts.Gravity.X / 2)
-		po.Padding.Right = int(opts.Gravity.X) - po.Padding.Left
-		po.Padding.Top = int(opts.Gravity.Y / 2)
-		po.Padding.Bottom = int(opts.Gravity.Y) - po.Padding.Top
+		po.Padding.Left = offX / 2
+		po.Padding.Right = offX - po.Padding.Left
+		po.Padding.Top = offY / 2
+		po.Padding.Bottom = offY - po.Padding.Top
 	}
 
 	if err := watermarkPipeline.Run(context.Background(), wm, po, wmData); err != nil {
@@ -61,7 +65,7 @@ func prepareWatermark(wm *vips.Image, wmData *imagedata.ImageData, opts *options
 			return err
 		}
 	} else {
-		left, top := calcPosition(imgWidth, imgHeight, wm.Width(), wm.Height(), &opts.Gravity, true)
+		left, top := calcPosition(imgWidth, imgHeight, wm.Width(), wm.Height(), &opts.Gravity, offsetScale, true)
 		if err := wm.Embed(imgWidth, imgHeight, left, top); err != nil {
 			return err
 		}
@@ -76,7 +80,7 @@ func prepareWatermark(wm *vips.Image, wmData *imagedata.ImageData, opts *options
 	return nil
 }
 
-func applyWatermark(img *vips.Image, wmData *imagedata.ImageData, opts *options.WatermarkOptions, framesCount int) error {
+func applyWatermark(img *vips.Image, wmData *imagedata.ImageData, opts *options.WatermarkOptions, offsetScale float64, framesCount int) error {
 	if err := img.RgbColourspace(); err != nil {
 		return err
 	}
@@ -87,7 +91,7 @@ func applyWatermark(img *vips.Image, wmData *imagedata.ImageData, opts *options.
 	width := img.Width()
 	height := img.Height()
 
-	if err := prepareWatermark(wm, wmData, opts, width, height/framesCount, framesCount); err != nil {
+	if err := prepareWatermark(wm, wmData, opts, width, height/framesCount, offsetScale, framesCount); err != nil {
 		return err
 	}
 
@@ -101,5 +105,5 @@ func watermark(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOpt
 		return nil
 	}
 
-	return applyWatermark(img, imagedata.Watermark, &po.Watermark, 1)
+	return applyWatermark(img, imagedata.Watermark, &po.Watermark, pctx.dprScale, 1)
 }

+ 2 - 1
vips/vips.c

@@ -615,7 +615,8 @@ vips_strip(VipsImage *in, VipsImage **out, int keep_exif_copyright) {
       (strcmp(name, "yres") == 0) ||
       (strcmp(name, "vips-loader") == 0) ||
       (strcmp(name, "background") == 0) ||
-      (strcmp(name, "vips-sequential") == 0)
+      (strcmp(name, "vips-sequential") == 0) ||
+      (strcmp(name, "imgproxy-dpr-scale") == 0)
     ) continue;
 
     if (keep_exif_copyright) {

+ 21 - 0
vips/vips.go

@@ -434,6 +434,23 @@ func (img *Image) GetIntSliceDefault(name string, def []int) ([]int, error) {
 	return img.GetIntSlice(name)
 }
 
+func (img *Image) GetDouble(name string) (float64, error) {
+	var d C.double
+
+	if C.vips_image_get_double(img.VipsImage, cachedCString(name), &d) != 0 {
+		return 0, Error()
+	}
+	return float64(d), nil
+}
+
+func (img *Image) GetDoubleDefault(name string, def float64) (float64, error) {
+	if C.vips_image_get_typeof(img.VipsImage, cachedCString(name)) == 0 {
+		return def, nil
+	}
+
+	return img.GetDouble(name)
+}
+
 func (img *Image) GetBlob(name string) ([]byte, error) {
 	var (
 		tmp  unsafe.Pointer
@@ -458,6 +475,10 @@ func (img *Image) SetIntSlice(name string, value []int) {
 	C.vips_image_set_array_int_go(img.VipsImage, cachedCString(name), &in[0], C.int(len(value)))
 }
 
+func (img *Image) SetDouble(name string, value float64) {
+	C.vips_image_set_double(img.VipsImage, cachedCString(name), C.double(value))
+}
+
 func (img *Image) SetBlob(name string, value []byte) {
 	defer runtime.KeepAlive(value)
 	C.vips_image_set_blob_copy(img.VipsImage, cachedCString(name), unsafe.Pointer(&value[0]), C.size_t(len(value)))