From 2c28252966c4894b3e82f681293d6c742f8792ad Mon Sep 17 00:00:00 2001 From: DarthSim Date: Sun, 16 Apr 2023 20:20:44 +0300 Subject: [PATCH] Fix the way the `dpr` processing option affects offsets and paddings --- CHANGELOG.md | 1 + docs/generating_the_url.md | 4 ++++ imath/imath.go | 12 ++++++++++++ processing/calc_position.go | 4 ++-- processing/crop.go | 11 ++++++----- processing/extend.go | 12 ++++++------ processing/padding.go | 8 ++++---- processing/pipeline.go | 4 ++++ processing/prepare.go | 30 +++++++++++++++++------------- processing/processing.go | 7 ++++++- processing/result_size.go | 6 +++--- processing/watermark.go | 22 +++++++++++++--------- vips/vips.c | 3 ++- vips/vips.go | 21 +++++++++++++++++++++ 14 files changed, 101 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c04b4699..49d8bc85 100644 --- a/CHANGELOG.md +++ b/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. diff --git a/docs/generating_the_url.md b/docs/generating_the_url.md index 498a1b2f..398182cb 100644 --- a/docs/generating_the_url.md +++ b/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` diff --git a/imath/imath.go b/imath/imath.go index 72349ee5..18571f13 100644 --- a/imath/imath.go +++ b/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 diff --git a/processing/calc_position.go b/processing/calc_position.go index 581853cf..0f414660 100644 --- a/processing/calc_position.go +++ b/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 diff --git a/processing/crop.go b/processing/crop.go index c47fddce..8934a6e4 100644 --- a/processing/crop.go +++ b/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) } diff --git a/processing/extend.go b/processing/extend.go index b19add48..24197327 100644 --- a/processing/extend.go +++ b/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) } diff --git a/processing/padding.go b/processing/padding.go index 3c4cd2b4..e7f6327e 100644 --- a/processing/padding.go +++ b/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, diff --git a/processing/pipeline.go b/processing/pipeline.go index 70516443..0644262a 100644 --- a/processing/pipeline.go +++ b/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 } diff --git a/processing/prepare.go b/processing/prepare.go index c3232794..ba475527 100644 --- a/processing/prepare.go +++ b/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 } diff --git a/processing/processing.go b/processing/processing.go index cfed9448..0e764e3f 100644 --- a/processing/processing.go +++ b/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 } } diff --git a/processing/result_size.go b/processing/result_size.go index e679e749..18de1069 100644 --- a/processing/result_size.go +++ b/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 } diff --git a/processing/watermark.go b/processing/watermark.go index 0f334974..cd64b482 100644 --- a/processing/watermark.go +++ b/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) } diff --git a/vips/vips.c b/vips/vips.c index 03a566bb..cbc59b3f 100644 --- a/vips/vips.c +++ b/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) { diff --git a/vips/vips.go b/vips/vips.go index 49064e0e..1d837088 100644 --- a/vips/vips.go +++ b/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)))