resources/image: Fix fill with smartcrop sometimes returning 0 bytes images
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 15 Jun 2021 16:22:05 +0000 (18:22 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 17 Jun 2021 21:52:27 +0000 (23:52 +0200)
Fixes #7955

resources/image.go
resources/image_test.go
resources/images/smartcrop.go

index edf05639f97c219bbfad6f3cd4c8f39f40aaf0fc..282f008edfd404d4469796e1323a19befe2c4fd1 100644 (file)
@@ -201,9 +201,26 @@ func (i *imageResource) Fill(spec string) (resource.Image, error) {
                return nil, err
        }
 
-       return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
+       img, err := i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
                return i.Proc.ApplyFiltersFromConfig(src, conf)
        })
+
+       if err != nil {
+               return nil, err
+       }
+
+       if conf.Anchor == 0 && img.Width() == 0 || img.Height() == 0 {
+               // See https://github.com/gohugoio/hugo/issues/7955
+               // Smartcrop fails silently in some rare cases.
+               // Fall back to a center fill.
+               conf.Anchor = gift.CenterAnchor
+               conf.AnchorStr = "center"
+               return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
+                       return i.Proc.ApplyFiltersFromConfig(src, conf)
+               })
+       }
+
+       return img, err
 }
 
 func (i *imageResource) Filter(filters ...interface{}) (resource.Image, error) {
index 8c69591cf32caf9ff808f1e8b214f03bae3be55a..cca961ee007a7e221d5847717bcc2593448bc653 100644 (file)
@@ -175,36 +175,6 @@ func TestImageTransformFormat(t *testing.T) {
        assertFileCache(c, fileCache, path.Base(imageGif.RelPermalink()), 225, 141)
 }
 
-// https://github.com/gohugoio/hugo/issues/4261
-func TestImageTransformLongFilename(t *testing.T) {
-       c := qt.New(t)
-
-       image := fetchImage(c, "1234567890qwertyuiopasdfghjklzxcvbnm5to6eeeeee7via8eleph.jpg")
-       c.Assert(image, qt.Not(qt.IsNil))
-
-       resized, err := image.Resize("200x")
-       c.Assert(err, qt.IsNil)
-       c.Assert(resized, qt.Not(qt.IsNil))
-       c.Assert(resized.Width(), qt.Equals, 200)
-       c.Assert(resized.RelPermalink(), qt.Equals, "/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_65b757a6e14debeae720fe8831f0a9bc.jpg")
-       resized, err = resized.Resize("100x")
-       c.Assert(err, qt.IsNil)
-       c.Assert(resized, qt.Not(qt.IsNil))
-       c.Assert(resized.Width(), qt.Equals, 100)
-       c.Assert(resized.RelPermalink(), qt.Equals, "/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c876768085288f41211f768147ba2647.jpg")
-}
-
-// Issue 6137
-func TestImageTransformUppercaseExt(t *testing.T) {
-       c := qt.New(t)
-       image := fetchImage(c, "sunrise.JPG")
-
-       resized, err := image.Resize("200x")
-       c.Assert(err, qt.IsNil)
-       c.Assert(resized, qt.Not(qt.IsNil))
-       c.Assert(resized.Width(), qt.Equals, 200)
-}
-
 // https://github.com/gohugoio/hugo/issues/5730
 func TestImagePermalinkPublishOrder(t *testing.T) {
        for _, checkOriginalFirst := range []bool{true, false} {
@@ -250,6 +220,70 @@ func TestImagePermalinkPublishOrder(t *testing.T) {
        }
 }
 
+func TestImageBugs(t *testing.T) {
+       c := qt.New(t)
+
+       // Issue #4261
+       c.Run("Transform long filename", func(c *qt.C) {
+               image := fetchImage(c, "1234567890qwertyuiopasdfghjklzxcvbnm5to6eeeeee7via8eleph.jpg")
+               c.Assert(image, qt.Not(qt.IsNil))
+
+               resized, err := image.Resize("200x")
+               c.Assert(err, qt.IsNil)
+               c.Assert(resized, qt.Not(qt.IsNil))
+               c.Assert(resized.Width(), qt.Equals, 200)
+               c.Assert(resized.RelPermalink(), qt.Equals, "/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_65b757a6e14debeae720fe8831f0a9bc.jpg")
+               resized, err = resized.Resize("100x")
+               c.Assert(err, qt.IsNil)
+               c.Assert(resized, qt.Not(qt.IsNil))
+               c.Assert(resized.Width(), qt.Equals, 100)
+               c.Assert(resized.RelPermalink(), qt.Equals, "/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c876768085288f41211f768147ba2647.jpg")
+
+       })
+
+       // Issue #6137
+       c.Run("Transform upper case extension", func(c *qt.C) {
+               image := fetchImage(c, "sunrise.JPG")
+
+               resized, err := image.Resize("200x")
+               c.Assert(err, qt.IsNil)
+               c.Assert(resized, qt.Not(qt.IsNil))
+               c.Assert(resized.Width(), qt.Equals, 200)
+
+       })
+
+       // Issue #7955
+       c.Run("Fill with smartcrop", func(c *qt.C) {
+               sunset := fetchImage(c, "sunset.jpg")
+
+               for _, test := range []struct {
+                       originalDimensions string
+                       targetWH           int
+               }{
+                       {"408x403", 400},
+                       {"425x403", 400},
+                       {"459x429", 400},
+                       {"476x442", 400},
+                       {"544x403", 400},
+                       {"476x468", 400},
+                       {"578x585", 550},
+                       {"578x598", 550},
+               } {
+                       c.Run(test.originalDimensions, func(c *qt.C) {
+                               image, err := sunset.Resize(test.originalDimensions)
+                               c.Assert(err, qt.IsNil)
+                               resized, err := image.Fill(fmt.Sprintf("%dx%d smart", test.targetWH, test.targetWH))
+                               c.Assert(err, qt.IsNil)
+                               c.Assert(resized, qt.Not(qt.IsNil))
+                               c.Assert(resized.Width(), qt.Equals, test.targetWH)
+                               c.Assert(resized.Height(), qt.Equals, test.targetWH)
+                       })
+
+               }
+
+       })
+}
+
 func TestImageTransformConcurrent(t *testing.T) {
        var wg sync.WaitGroup
 
index 76f547ef0956db83b2cc1ba0bd000cbfd707533f..864c6de0ae0ecdb279e2f1c8bb6a89f14ef03697 100644 (file)
@@ -15,6 +15,7 @@ package images
 
 import (
        "image"
+       "math"
 
        "github.com/disintegration/gift"
 
@@ -41,6 +42,14 @@ type imagingResizer struct {
 }
 
 func (r imagingResizer) Resize(img image.Image, width, height uint) image.Image {
+       // See https://github.com/gohugoio/hugo/issues/7955#issuecomment-861710681
+       scaleX, scaleY := calcFactorsNfnt(width, height, float64(img.Bounds().Dx()), float64(img.Bounds().Dy()))
+       if width == 0 {
+               width = uint(math.Ceil(float64(img.Bounds().Dx()) / scaleX))
+       }
+       if height == 0 {
+               height = uint(math.Ceil(float64(img.Bounds().Dy()) / scaleY))
+       }
        result, _ := r.p.Filter(img, gift.Resize(int(width), int(height), r.filter))
        return result
 }
@@ -71,3 +80,25 @@ func (p *ImageProcessor) smartCrop(img image.Image, width, height int, filter gi
 
        return img.Bounds().Intersect(rect), nil
 }
+
+// Calculates scaling factors using old and new image dimensions.
+// Code borrowed from https://github.com/nfnt/resize/blob/83c6a9932646f83e3267f353373d47347b6036b2/resize.go#L593
+func calcFactorsNfnt(width, height uint, oldWidth, oldHeight float64) (scaleX, scaleY float64) {
+       if width == 0 {
+               if height == 0 {
+                       scaleX = 1.0
+                       scaleY = 1.0
+               } else {
+                       scaleY = oldHeight / float64(height)
+                       scaleX = scaleY
+               }
+       } else {
+               scaleX = oldWidth / float64(width)
+               if height == 0 {
+                       scaleY = scaleX
+               } else {
+                       scaleY = oldHeight / float64(height)
+               }
+       }
+       return
+}