浏览代码

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

DarthSim 2 年之前
父节点
当前提交
2c28252966

+ 1 - 0
CHANGELOG.md

@@ -7,6 +7,7 @@
 
 
 ### Fix
 ### Fix
 - Fix detection of dead HTTP/2 connections.
 - Fix detection of dead HTTP/2 connections.
+- Fix the way the `dpr` processing option affects offsets and paddings.
 
 
 ### Remove
 ### Remove
 - Remove suport for `Viewport-Width` client hint.
 - 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.
 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.
 **📝 Note:** Unlike [dpr](#dpr), `zoom` doesn't set the `Content-DPR` header in the response.
 
 
 Default: `1`
 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.
 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.
 **📝 Note:** `dpr` also sets the `Content-DPR` header in the response so the browser can correctly render the image.
 
 
 Default: `1`
 Default: `1`

+ 12 - 0
imath/imath.go

@@ -31,6 +31,10 @@ func Round(a float64) int {
 	return int(math.Round(a))
 	return int(math.Round(a))
 }
 }
 
 
+func RoundToEven(a float64) int {
+	return int(math.RoundToEven(a))
+}
+
 func Scale(a int, scale float64) int {
 func Scale(a int, scale float64) int {
 	if a == 0 {
 	if a == 0 {
 		return 0
 		return 0
@@ -39,6 +43,14 @@ func Scale(a int, scale float64) int {
 	return Round(float64(a) * scale)
 	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 {
 func Shrink(a int, shrink float64) int {
 	if a == 0 {
 	if a == 0 {
 		return 0
 		return 0

+ 2 - 2
processing/calc_position.go

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

+ 6 - 5
processing/crop.go

@@ -7,7 +7,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/vips"
 	"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 {
 	if cropWidth == 0 && cropHeight == 0 {
 		return nil
 		return nil
 	}
 	}
@@ -28,7 +28,7 @@ func cropImage(img *vips.Image, cropWidth, cropHeight int, gravity *options.Grav
 		return img.SmartCrop(cropWidth, cropHeight)
 		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)
 	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
 		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 {
 func cropToResult(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
 	// Crop image to the result size
 	// Crop image to the result size
-	resultWidth, resultHeight := resultSize(po)
+	resultWidth, resultHeight := resultSize(po, pctx.dprScale)
 
 
 	if po.ResizingType == options.ResizeFillDown {
 	if po.ResizingType == options.ResizeFillDown {
 		diffW := float64(resultWidth) / float64(img.Width())
 		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"
 	"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()) {
 	if !opts.Enabled || (resultWidth <= img.Width() && resultHeight <= img.Height()) {
 		return nil
 		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)
 	return img.Embed(resultWidth, resultHeight, offX, offY)
 }
 }
 
 
 func extend(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
 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 {
 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
 		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(
 	return img.Embed(
 		img.Width()+paddingLeft+paddingRight,
 		img.Width()+paddingLeft+paddingRight,

+ 4 - 0
processing/pipeline.go

@@ -29,6 +29,8 @@ type pipelineContext struct {
 	wscale float64
 	wscale float64
 	hscale float64
 	hscale float64
 
 
+	dprScale float64
+
 	iccImported bool
 	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
 	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
 	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
 	var wshrink, hshrink float64
 
 
 	srcW, srcH := float64(width), float64(height)
 	srcW, srcH := float64(width), float64(height)
@@ -67,9 +67,6 @@ func calcScale(width, height int, po *options.ProcessingOptions, imgtype imagety
 		hshrink = srcH / dstH
 		hshrink = srcH / dstH
 	}
 	}
 
 
-	wshrink /= po.Dpr
-	hshrink /= po.Dpr
-
 	if wshrink != 1 || hshrink != 1 {
 	if wshrink != 1 || hshrink != 1 {
 		rt := po.ResizingType
 		rt := po.ResizingType
 
 
@@ -101,17 +98,24 @@ func calcScale(width, height int, po *options.ProcessingOptions, imgtype imagety
 	wshrink /= po.ZoomWidth
 	wshrink /= po.ZoomWidth
 	hshrink /= po.ZoomHeight
 	hshrink /= po.ZoomHeight
 
 
+	dprScale := po.Dpr
+
 	if !po.Enlarge && imgtype != imagetype.SVG {
 	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 po.MinWidth > 0 {
 		if minShrink := srcW / float64(po.MinWidth); minShrink < wshrink {
 		if minShrink := srcW / float64(po.MinWidth); minShrink < wshrink {
 			hshrink /= wshrink / minShrink
 			hshrink /= wshrink / minShrink
@@ -134,7 +138,7 @@ func calcScale(width, height int, po *options.ProcessingOptions, imgtype imagety
 		hshrink = srcH
 		hshrink = srcH
 	}
 	}
 
 
-	return 1.0 / wshrink, 1.0 / hshrink
+	return 1.0 / wshrink, 1.0 / hshrink, dprScale
 }
 }
 
 
 func calcCropSize(orig int, crop float64) int {
 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)
 	widthToScale := imath.MinNonZero(pctx.cropWidth, pctx.srcWidth)
 	heightToScale := imath.MinNonZero(pctx.cropHeight, pctx.srcHeight)
 	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
 	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 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
 			return err
 		}
 		}
 	}
 	}

+ 3 - 3
processing/result_size.go

@@ -5,9 +5,9 @@ import (
 	"github.com/imgproxy/imgproxy/v3/options"
 	"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
 	return resultWidth, resultHeight
 }
 }

+ 13 - 9
processing/watermark.go

@@ -2,6 +2,7 @@ package processing
 
 
 import (
 import (
 	"context"
 	"context"
+	"math"
 
 
 	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/imagedata"
@@ -19,7 +20,7 @@ var watermarkPipeline = pipeline{
 	padding,
 	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 {
 	if err := wm.Load(wmData, 1, 1.0, 1); err != nil {
 		return err
 		return err
 	}
 	}
@@ -36,11 +37,14 @@ func prepareWatermark(wm *vips.Image, wmData *imagedata.ImageData, opts *options
 	}
 	}
 
 
 	if opts.Replicate {
 	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.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 {
 	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
 			return err
 		}
 		}
 	} else {
 	} 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 {
 		if err := wm.Embed(imgWidth, imgHeight, left, top); err != nil {
 			return err
 			return err
 		}
 		}
@@ -76,7 +80,7 @@ func prepareWatermark(wm *vips.Image, wmData *imagedata.ImageData, opts *options
 	return nil
 	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 {
 	if err := img.RgbColourspace(); err != nil {
 		return err
 		return err
 	}
 	}
@@ -87,7 +91,7 @@ func applyWatermark(img *vips.Image, wmData *imagedata.ImageData, opts *options.
 	width := img.Width()
 	width := img.Width()
 	height := img.Height()
 	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
 		return err
 	}
 	}
 
 
@@ -101,5 +105,5 @@ func watermark(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOpt
 		return nil
 		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, "yres") == 0) ||
       (strcmp(name, "vips-loader") == 0) ||
       (strcmp(name, "vips-loader") == 0) ||
       (strcmp(name, "background") == 0) ||
       (strcmp(name, "background") == 0) ||
-      (strcmp(name, "vips-sequential") == 0)
+      (strcmp(name, "vips-sequential") == 0) ||
+      (strcmp(name, "imgproxy-dpr-scale") == 0)
     ) continue;
     ) continue;
 
 
     if (keep_exif_copyright) {
     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)
 	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) {
 func (img *Image) GetBlob(name string) ([]byte, error) {
 	var (
 	var (
 		tmp  unsafe.Pointer
 		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)))
 	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) {
 func (img *Image) SetBlob(name string, value []byte) {
 	defer runtime.KeepAlive(value)
 	defer runtime.KeepAlive(value)
 	C.vips_image_set_blob_copy(img.VipsImage, cachedCString(name), unsafe.Pointer(&value[0]), C.size_t(len(value)))
 	C.vips_image_set_blob_copy(img.VipsImage, cachedCString(name), unsafe.Pointer(&value[0]), C.size_t(len(value)))