]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
resources: Fix 2 image file cache key issues
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 16 Jan 2025 18:15:30 +0000 (19:15 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 18 Jan 2025 11:23:18 +0000 (12:23 +0100)
* Always include the content hash in the cache key for unprocessed images.
* Always include the image config hash in the cache key.

This is also a major cleanup/simplification of the implementation in this area.

Note that this, unfortunately, forces new hashes/filenames for generated images.

Fixes #13273
Fixes #13272

38 files changed:
config/namespace.go
config/namespace_test.go
hugolib/hugo_sites_multihost_test.go
hugolib/image_test.go
hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go
hugolib/resource_chain_test.go
resources/image.go
resources/image_cache.go
resources/image_extended_test.go
resources/image_test.go
resources/images/config.go
resources/images/config_test.go
resources/images/filters.go
resources/images/image.go
resources/images/images_golden_integration_test.go
resources/images/imagetesting/testing.go
resources/images/smartcrop.go
resources/images/testdata/images_golden/filters/mask2/green.jpg [new file with mode: 0644]
resources/images/testdata/images_golden/filters/mask2/pink.jpg [new file with mode: 0644]
resources/images/testdata/images_golden/methods/crop-sunsetjpg-200x200.jpg [new file with mode: 0644]
resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center-q20.jpg [new file with mode: 0644]
resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center-r90.jpg [new file with mode: 0644]
resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center.jpg [new file with mode: 0644]
resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-smart.jpg [new file with mode: 0644]
resources/images/testdata/images_golden/methods/fill-sunsetjpg-90x120-left.jpg [new file with mode: 0644]
resources/images/testdata/images_golden/methods/fill-sunsetjpg-90x120-right.jpg [new file with mode: 0644]
resources/images/testdata/images_golden/methods/fit-sunsetjpg-200x200.jpg [new file with mode: 0644]
resources/images/testdata/images_golden/methods/resize-gopherpng-100x-03fc56-jpg.jpg [new file with mode: 0644]
resources/images/testdata/images_golden/methods/resize-gopherpng-100x-fc03ec.png [new file with mode: 0644]
resources/images/testdata/images_golden/methods/resize-gopherpng-100x.png [new file with mode: 0644]
resources/images/testdata/images_golden/methods/resize-sunsetjpg-300x.jpg [new file with mode: 0644]
resources/images/testdata/images_golden/methods/resize-sunsetjpg-x200.jpg [new file with mode: 0644]
resources/resource.go
resources/resource_spec.go
resources/resources_integration_test.go
resources/testdata/mask2.png [new file with mode: 0644]
resources/transform_test.go
tpl/resources/resources_integration_test.go

index 46b5014c34c1cc4498502b35ece0bd5bdfe067c8..e41b56e2d9e1f302d7e77884e927fc60bb5a51ec 100644 (file)
@@ -22,7 +22,7 @@ import (
 func DecodeNamespace[S, C any](configSource any, buildConfig func(any) (C, any, error)) (*ConfigNamespace[S, C], error) {
        // Calculate the hash of the input (not including any defaults applied later).
        // This allows us to introduce new config options without breaking the hash.
-       h := hashing.HashString(configSource)
+       h := hashing.HashStringHex(configSource)
 
        // Build the config
        c, ext, err := buildConfig(configSource)
index 5eacdeac7ddd4611ac3c70e65bcd2c23acd48590..df27ae05cf0aee4e08b89aebccc5cd26779e49ae 100644 (file)
@@ -43,7 +43,7 @@ func TestNamespace(t *testing.T) {
        c.Assert(err, qt.IsNil)
        c.Assert(ns, qt.Not(qt.IsNil))
        c.Assert(ns.SourceStructure, qt.DeepEquals, map[string]interface{}{"foo": "bar"})
-       c.Assert(ns.SourceHash, qt.Equals, "1450430416588600409")
+       c.Assert(ns.SourceHash, qt.Equals, "1420f6c7782f7459")
        c.Assert(ns.Config, qt.DeepEquals, &tstNsExt{Foo: "bar"})
        c.Assert(ns.Signature(), qt.DeepEquals, []*tstNsExt(nil))
 }
index 39504202b2ba28c1807607d955fffe1b84996145..37f7ab9279157099d9320970ec2045a1a6a72950 100644 (file)
@@ -205,9 +205,9 @@ title: mybundle-en
        b.AssertFileExists("public/de/mybundle/pixel.png", true)
        b.AssertFileExists("public/en/mybundle/pixel.png", true)
 
-       b.AssertFileExists("public/de/mybundle/pixel_hu8581513846771248023.png", true)
+       b.AssertFileExists("public/de/mybundle/pixel_hu_58204cbc58507d74.png", true)
        // failing test below
-       b.AssertFileExists("public/en/mybundle/pixel_hu8581513846771248023.png", true)
+       b.AssertFileExists("public/en/mybundle/pixel_hu_58204cbc58507d74.png", true)
 }
 
 func TestMultihostResourceOneBaseURLWithSuPath(t *testing.T) {
index 7dcd9fc26ab8a62a2103b93fa415c9df9b07c9e3..09a5b841e902ee244f00ff62c19c5148edf69684 100644 (file)
@@ -72,20 +72,20 @@ SUNSET2: {{ $resized2.RelPermalink }}/{{ $resized2.Width }}/Lat: {{ $resized2.Ex
 
        b.Build(BuildCfg{})
 
-       b.AssertFileContent("public/index.html", "SUNSET FOR: en: /bundle/sunset_hu13235715490294913361.jpg/200/Lat: 36.59744166666667")
-       b.AssertFileContent("public/fr/index.html", "SUNSET FOR: fr: /bundle/sunset_hu13235715490294913361.jpg/200/Lat: 36.59744166666667")
-       b.AssertFileContent("public/index.html", " SUNSET2: /images/sunset_hu1573057890424052540.jpg/123/Lat: 36.59744166666667")
-       b.AssertFileContent("public/nn/index.html", " SUNSET2: /images/sunset_hu1573057890424052540.jpg/123/Lat: 36.59744166666667")
+       b.AssertFileContent("public/index.html", "SUNSET FOR: en: /bundle/sunset_hu_77061c65c31d2244.jpg/200/Lat: 36.59744166666667")
+       b.AssertFileContent("public/fr/index.html", "SUNSET FOR: fr: /bundle/sunset_hu_77061c65c31d2244.jpg/200/Lat: 36.59744166666667")
+       b.AssertFileContent("public/index.html", " SUNSET2: /images/sunset_hu_b52e3343ea6a8764.jpg/123/Lat: 36.59744166666667")
+       b.AssertFileContent("public/nn/index.html", " SUNSET2: /images/sunset_hu_b52e3343ea6a8764.jpg/123/Lat: 36.59744166666667")
 
-       b.AssertImage(200, 200, "public/bundle/sunset_hu13235715490294913361.jpg")
+       b.AssertImage(200, 200, "public/bundle/sunset_hu_77061c65c31d2244.jpg")
 
        // Check the file cache
-       b.AssertImage(200, 200, "resources/_gen/images/bundle/sunset_hu13235715490294913361.jpg")
+       b.AssertImage(200, 200, "resources/_gen/images/bundle/sunset_hu_77061c65c31d2244.jpg")
 
-       b.AssertFileContent("resources/_gen/images/bundle/sunset_17710516992648092201.json",
+       b.AssertFileContent("resources/_gen/images/bundle/sunset_d209dcdc6b875e26.json",
                "FocalLengthIn35mmFormat|uint16", "PENTAX")
 
-       b.AssertFileContent("resources/_gen/images/images/sunset_17710516992648092201.json",
+       b.AssertFileContent("resources/_gen/images/images/sunset_d209dcdc6b875e26.json",
                "FocalLengthIn35mmFormat|uint16", "PENTAX")
 
        b.AssertNoDuplicateWrites()
index b033aad2b658436bd0a8df89e9f9c3b9a6dfcc13..64ee7039731b06b4cefd17379e15d36f5b7cf846 100644 (file)
@@ -119,7 +119,7 @@ docs/p1/sub/mymixcasetext2.txt
                "RelPermalink: /docs/p1/sub/mymixcasetext2.txt|Name: sub/mymixcasetext2.txt|",
                "RelPermalink: /mydata.yaml|Name: sub/data1.yaml|Title: Sub data|Params: map[]|",
                "Featured Image: /a/pixel.png|featured.png|",
-               "Resized Featured Image: /a/pixel_hu16809842526914527184.png|10|",
+               "Resized Featured Image: /a/pixel_hu_a32b3e361d55df1.png|10|",
                // Resource from string
                "RelPermalink: /docs/p1/mytext.txt|Name: textresource|Title: My Text Resource|Params: map[param1:param1v]|",
                // Dates
index 669114c8a4acd48604d89821e6444eb5bbfc1c54..942873ae4c904af29555b84e71e88d4270210ef8 100644 (file)
@@ -106,12 +106,12 @@ FAILED REMOTE ERROR DETAILS CONTENT: {{ with $failedImg }}{{ with .Err }}{{ with
                b.AssertFileContent("public/index.html",
                        fmt.Sprintf(`
 SUNSET: /images/sunset.jpg|/images/sunset.a9bf1d944e19c0f382e0d8f51de690f7d0bc8fa97390c4242a86c3e5c0737e71.jpg|900|90587
-FIT: /images/sunset.jpg|/images/sunset_hu15210517121918042184.jpg|200
+FIT: /images/sunset.jpg|/images/sunset_hu_f2aae87288f3c13b.jpg|200
 CSS integrity Data first: sha256-od9YaHw8nMOL8mUy97Sy8sKwMV3N4hI3aVmZXATxH&#43;8= /styles.min.a1df58687c3c9cc38bf26532f7b4b2f2c2b0315dcde212376959995c04f11fef.css
 CSS integrity Data last:  /styles2.min.1cfc52986836405d37f9998a63fd6dd8608e8c410e5e3db1daaa30f78bc273ba.css sha256-HPxSmGg2QF03&#43;ZmKY/1t2GCOjEEOXj2x2qow94vCc7o=
 
 SUNSET REMOTE: /sunset_%[1]s.jpg|/sunset_%[1]s.a9bf1d944e19c0f382e0d8f51de690f7d0bc8fa97390c4242a86c3e5c0737e71.jpg|900|90587
-FIT REMOTE: /sunset_%[1]s.jpg|/sunset_%[1]s_hu15210517121918042184.jpg|200
+FIT REMOTE: /sunset_%[1]s.jpg|/sunset_%[1]s_hu_f2aae87288f3c13b.jpg|200
 REMOTE NOT FOUND: OK
 LOCAL NOT FOUND: OK
 PRINT PROTOCOL ERROR DETAILS: Err: template: index.html:22:36: executing "index.html" at <resources.GetRemote>: error calling GetRemote: Get "gopher://example.org": unsupported protocol scheme "gopher"|
index 686f70e274d36797c78d386fb7c4e4f03607fb7e..c1f107b592fb39281502a6139d4e327289a6dfe0 100644 (file)
@@ -30,7 +30,6 @@ import (
 
        "github.com/gohugoio/hugo/cache/filecache"
        "github.com/gohugoio/hugo/common/hashing"
-       "github.com/gohugoio/hugo/common/hstrings"
        "github.com/gohugoio/hugo/common/paths"
 
        "github.com/disintegration/gift"
@@ -205,15 +204,12 @@ func (i *imageResource) cloneWithUpdates(u *transformationUpdate) (baseResource,
        }, nil
 }
 
-var imageActions = []string{images.ActionResize, images.ActionCrop, images.ActionFit, images.ActionFill}
-
 // Process processes the image with the given spec.
 // The spec can contain an optional action, one of "resize", "crop", "fit" or "fill".
 // This makes this method a more flexible version that covers all of Resize, Crop, Fit and Fill,
 // but it also supports e.g. format conversions without any resize action.
 func (i *imageResource) Process(spec string) (images.ImageResource, error) {
-       action, options := i.resolveActionOptions(spec)
-       return i.processActionOptions(action, options)
+       return i.processActionSpec("", spec)
 }
 
 // Resize resizes the image to the specified width and height using the specified resampling
@@ -243,7 +239,7 @@ func (i *imageResource) Fill(spec string) (images.ImageResource, error) {
 }
 
 func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) {
-       var conf images.ImageConfig
+       var confMain images.ImageConfig
 
        var gfilters []gift.Filter
 
@@ -251,47 +247,30 @@ func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) {
                gfilters = append(gfilters, images.ToFilters(f)...)
        }
 
-       var (
-               targetFormat images.Format
-               configSet    bool
-       )
+       var options []string
+
        for _, f := range gfilters {
                f = images.UnwrapFilter(f)
                if specProvider, ok := f.(images.ImageProcessSpecProvider); ok {
-                       action, options := i.resolveActionOptions(specProvider.ImageProcessSpec())
-                       var err error
-                       conf, err = images.DecodeImageConfig(action, options, i.Proc.Cfg, i.Format)
-                       if err != nil {
-                               return nil, err
-                       }
-                       configSet = true
-                       if conf.TargetFormat != 0 {
-                               targetFormat = conf.TargetFormat
-                               // We only support one target format, but prefer the last one,
-                               // so we keep going.
-                       }
+                       options = append(options, strings.Fields(specProvider.ImageProcessSpec())...)
                }
        }
 
-       if !configSet {
-               conf = images.GetDefaultImageConfig("filter", i.Proc.Cfg)
+       confMain, err := images.DecodeImageConfig(options, i.Proc.Cfg, i.Format)
+       if err != nil {
+               return nil, err
        }
 
-       conf.Action = "filter"
-       conf.Key = hashing.HashString(gfilters)
-       conf.TargetFormat = targetFormat
-       if conf.TargetFormat == 0 {
-               conf.TargetFormat = i.Format
-       }
+       confMain.Action = "filter"
+       confMain.Key = hashing.HashString(gfilters)
 
-       return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
+       return i.doWithImageConfig(confMain, func(src image.Image) (image.Image, error) {
                var filters []gift.Filter
                for _, f := range gfilters {
                        f = images.UnwrapFilter(f)
                        if specProvider, ok := f.(images.ImageProcessSpecProvider); ok {
-                               processSpec := specProvider.ImageProcessSpec()
-                               action, options := i.resolveActionOptions(processSpec)
-                               conf, err := images.DecodeImageConfig(action, options, i.Proc.Cfg, i.Format)
+                               options := strings.Fields(specProvider.ImageProcessSpec())
+                               conf, err := images.DecodeImageConfig(options, i.Proc.Cfg, i.Format)
                                if err != nil {
                                        return nil, err
                                }
@@ -313,25 +292,13 @@ func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) {
        })
 }
 
-func (i *imageResource) resolveActionOptions(spec string) (string, []string) {
-       var action string
-       options := strings.Fields(spec)
-       for i, p := range options {
-               if hstrings.InSlicEqualFold(imageActions, p) {
-                       action = p
-                       options = append(options[:i], options[i+1:]...)
-                       break
-               }
-       }
-       return action, options
-}
-
 func (i *imageResource) processActionSpec(action, spec string) (images.ImageResource, error) {
-       return i.processActionOptions(action, strings.Fields(spec))
+       options := append([]string{action}, strings.Fields(strings.ToLower(spec))...)
+       return i.processOptions(options)
 }
 
-func (i *imageResource) processActionOptions(action string, options []string) (images.ImageResource, error) {
-       conf, err := images.DecodeImageConfig(action, options, i.Proc.Cfg, i.Format)
+func (i *imageResource) processOptions(options []string) (images.ImageResource, error) {
+       conf, err := images.DecodeImageConfig(options, i.Proc.Cfg, i.Format)
        if err != nil {
                return nil, err
        }
@@ -343,13 +310,12 @@ func (i *imageResource) processActionOptions(action string, options []string) (i
                return nil, err
        }
 
-       if action == images.ActionFill {
-               if conf.Anchor == 0 && img.Width() == 0 || img.Height() == 0 {
+       if conf.Action == images.ActionFill {
+               if conf.Anchor == images.SmartCropAnchor && 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"
+                       conf = conf.Reanchor(gift.CenterAnchor)
                        return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
                                return i.Proc.ApplyFiltersFromConfig(src, conf)
                        })
@@ -417,7 +383,7 @@ func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src im
                }
 
                ci := i.clone(converted)
-               targetPath := i.relTargetPathFromConfig(conf)
+               targetPath := i.relTargetPathFromConfig(conf, i.getSpec().imaging.Cfg.SourceHash)
                ci.setTargetPath(targetPath)
                ci.Format = conf.TargetFormat
                ci.setMediaType(conf.TargetFormat.MediaType())
@@ -485,26 +451,30 @@ func (i *imageResource) getImageMetaCacheTargetPath() string {
        df := i.getResourcePaths()
        p1, _ := paths.FileAndExt(df.File)
        h := i.hash()
-       idStr := hashing.HashString(h, i.size(), imageMetaVersionNumber, cfgHash)
+       idStr := hashing.HashStringHex(h, i.size(), imageMetaVersionNumber, cfgHash)
        df.File = fmt.Sprintf("%s_%s.json", p1, idStr)
        return df.TargetPath()
 }
 
-func (i *imageResource) relTargetPathFromConfig(conf images.ImageConfig) internal.ResourcePaths {
+func (i *imageResource) relTargetPathFromConfig(conf images.ImageConfig, imagingConfigSourceHash string) internal.ResourcePaths {
        p1, p2 := paths.FileAndExt(i.getResourcePaths().File)
        if conf.TargetFormat != i.Format {
                p2 = conf.TargetFormat.DefaultExtension()
        }
-       const prefix = "_hu"
-       huIdx := strings.LastIndex(p1, prefix)
-       incomingID := "i"
+
+       // Do not change.
+       const imageHashPrefix = "_hu_"
+
+       huIdx := strings.LastIndex(p1, imageHashPrefix)
+       incomingID := ""
        if huIdx > -1 {
-               incomingID = p1[huIdx+len(prefix):]
+               incomingID = p1[huIdx+len(imageHashPrefix):]
                p1 = p1[:huIdx]
        }
-       hash := hashing.HashUint64(incomingID, i.hash(), conf.GetKey(i.Format))
+
+       hash := hashing.HashStringHex(incomingID, i.hash(), conf.Key, imagingConfigSourceHash)
        rp := i.getResourcePaths()
-       rp.File = fmt.Sprintf("%s%s%d%s", p1, prefix, hash, p2)
+       rp.File = fmt.Sprintf("%s%s%s%s", p1, imageHashPrefix, hash, p2)
 
        return rp
 }
index d824c5d1a066066a84fede1df1b9e6defb94f798..1fc7226095e9d60381266d4947c447c49f3b6307 100644 (file)
@@ -37,7 +37,7 @@ func (c *ImageCache) getOrCreate(
        parent *imageResource, conf images.ImageConfig,
        createImage func() (*imageResource, image.Image, error),
 ) (*resourceAdapter, error) {
-       relTarget := parent.relTargetPathFromConfig(conf)
+       relTarget := parent.relTargetPathFromConfig(conf, parent.getSpec().imaging.Cfg.SourceHash)
        relTargetPath := relTarget.TargetPath()
        memKey := relTargetPath
 
index e570c4a3d243ff72e69bdda15f96a83205850f39..f481be9bb400e1c37f450b02f5655700cd221bd4 100644 (file)
@@ -42,6 +42,6 @@ func TestImageResizeWebP(t *testing.T) {
        resized, err := image.Resize("123x")
        c.Assert(err, qt.IsNil)
        c.Assert(image.MediaType(), qt.Equals, media.Builtin.WEBPType)
-       c.Assert(resized.RelPermalink(), qt.Equals, "/a/sunrise_hu544374262273649331.webp")
+       c.Assert(resized.RelPermalink(), qt.Equals, "/a/sunrise_hu_a1deb893888915d9.webp")
        c.Assert(resized.Width(), qt.Equals, 123)
 }
index 5639d457e07d66bf06bf1ee822086262e5aa0471..1ba5a149a32caf2ee154f2eef198442d8a90379e 100644 (file)
@@ -113,28 +113,28 @@ func TestImageTransformBasic(t *testing.T) {
        assertWidthHeight(resizedAndRotated, 125, 200)
 
        assertWidthHeight(resized, 300, 200)
-       c.Assert(resized.RelPermalink(), qt.Equals, "/a/sunset_hu2082030801149749592.jpg")
+       c.Assert(resized.RelPermalink(), qt.Equals, "/a/sunset_hu_d2115125d9324a79.jpg")
 
        fitted, err := resized.Fit("50x50")
        c.Assert(err, qt.IsNil)
-       c.Assert(fitted.RelPermalink(), qt.Equals, "/a/sunset_hu16263619592447877226.jpg")
+       c.Assert(fitted.RelPermalink(), qt.Equals, "/a/sunset_hu_c2c98e06123b048e.jpg")
        assertWidthHeight(fitted, 50, 33)
 
        // Check the MD5 key threshold
        fittedAgain, _ := fitted.Fit("10x20")
        fittedAgain, err = fittedAgain.Fit("10x20")
        c.Assert(err, qt.IsNil)
-       c.Assert(fittedAgain.RelPermalink(), qt.Equals, "/a/sunset_hu847809310637164306.jpg")
+       c.Assert(fittedAgain.RelPermalink(), qt.Equals, "/a/sunset_hu_dc9e89c10109de72.jpg")
        assertWidthHeight(fittedAgain, 10, 7)
 
        filled, err := image.Fill("200x100 bottomLeft")
        c.Assert(err, qt.IsNil)
-       c.Assert(filled.RelPermalink(), qt.Equals, "/a/sunset_hu18289448341423092707.jpg")
+       c.Assert(filled.RelPermalink(), qt.Equals, "/a/sunset_hu_b9f6d350738928fe.jpg")
        assertWidthHeight(filled, 200, 100)
 
        smart, err := image.Fill("200x100 smart")
        c.Assert(err, qt.IsNil)
-       c.Assert(smart.RelPermalink(), qt.Equals, "/a/sunset_hu11649371610839769766.jpg")
+       c.Assert(smart.RelPermalink(), qt.Equals, "/a/sunset_hu_6fd390e7b0d26f0b.jpg")
        assertWidthHeight(smart, 200, 100)
 
        // Check cache
@@ -144,12 +144,12 @@ func TestImageTransformBasic(t *testing.T) {
 
        cropped, err := image.Crop("300x300 topRight")
        c.Assert(err, qt.IsNil)
-       c.Assert(cropped.RelPermalink(), qt.Equals, "/a/sunset_hu2242042514052853140.jpg")
+       c.Assert(cropped.RelPermalink(), qt.Equals, "/a/sunset_hu_3df036e11f4ddd43.jpg")
        assertWidthHeight(cropped, 300, 300)
 
        smartcropped, err := image.Crop("200x200 smart")
        c.Assert(err, qt.IsNil)
-       c.Assert(smartcropped.RelPermalink(), qt.Equals, "/a/sunset_hu12983255101170993571.jpg")
+       c.Assert(smartcropped.RelPermalink(), qt.Equals, "/a/sunset_hu_12e2d26de89b464b.jpg")
        assertWidthHeight(smartcropped, 200, 200)
 
        // Check cache
@@ -216,7 +216,7 @@ func TestImageTransformFormat(t *testing.T) {
 
        imagePng, err := image.Resize("450x png")
        c.Assert(err, qt.IsNil)
-       c.Assert(imagePng.RelPermalink(), qt.Equals, "/a/sunset_hu11737890885216583918.png")
+       c.Assert(imagePng.RelPermalink(), qt.Equals, "/a/sunset_hu_e8b9444dcf2e75ef.png")
        c.Assert(imagePng.ResourceType(), qt.Equals, "image")
        assertExtWidthHeight(imagePng, ".png", 450, 281)
        c.Assert(imagePng.Name(), qt.Equals, "sunset.jpg")
@@ -224,7 +224,7 @@ func TestImageTransformFormat(t *testing.T) {
 
        imageGif, err := image.Resize("225x gif")
        c.Assert(err, qt.IsNil)
-       c.Assert(imageGif.RelPermalink(), qt.Equals, "/a/sunset_hu1431827106749674475.gif")
+       c.Assert(imageGif.RelPermalink(), qt.Equals, "/a/sunset_hu_f80842d4c3789345.gif")
        c.Assert(imageGif.ResourceType(), qt.Equals, "image")
        assertExtWidthHeight(imageGif, ".gif", 225, 141)
        c.Assert(imageGif.Name(), qt.Equals, "sunset.jpg")
@@ -247,7 +247,7 @@ func TestImagePermalinkPublishOrder(t *testing.T) {
                        }()
 
                        check1 := func(img images.ImageResource) {
-                               resizedLink := "/a/sunset_hu7919355342577096259.jpg"
+                               resizedLink := "/a/sunset_hu_3910bca82e28c9d6.jpg"
                                c.Assert(img.RelPermalink(), qt.Equals, resizedLink)
                                assertImageFile(c, spec.PublishFs, resizedLink, 100, 50)
                        }
@@ -288,12 +288,12 @@ func TestImageBugs(t *testing.T) {
                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/1234567890qwertyuiopasdfghjklzxcvbnm5to6eeeeee7via8eleph_hu9514381480012510326.jpg")
+               c.Assert(resized.RelPermalink(), qt.Equals, "/a/1234567890qwertyuiopasdfghjklzxcvbnm5to6eeeeee7via8eleph_hu_951d3980b18c52a9.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/1234567890qwertyuiopasdfghjklzxcvbnm5to6eeeeee7via8eleph_hu1776700126481066216.jpg")
+               c.Assert(resized.RelPermalink(), qt.Equals, "/a/1234567890qwertyuiopasdfghjklzxcvbnm5to6eeeeee7via8eleph_hu_1daa203572ecd6ec.jpg")
        })
 
        // Issue #6137
@@ -391,7 +391,7 @@ func TestImageResize8BitPNG(t *testing.T) {
        resized, err := image.Resize("800x")
        c.Assert(err, qt.IsNil)
        c.Assert(resized.MediaType().Type, qt.Equals, "image/png")
-       c.Assert(resized.RelPermalink(), qt.Equals, "/a/gohugoio_hu8582372628235034388.png")
+       c.Assert(resized.RelPermalink(), qt.Equals, "/a/gohugoio_hu_fe2b762e9cac406c.png")
        c.Assert(resized.Width(), qt.Equals, 800)
 }
 
index 9655e9a51cec198066522ac1ed54e52dc7238629..6fcd2e334ba6b01c0e083ac5b06bcd04ca907e32 100644 (file)
@@ -20,6 +20,7 @@ import (
        "strconv"
        "strings"
 
+       "github.com/gohugoio/hugo/common/hashing"
        "github.com/gohugoio/hugo/common/maps"
        "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/media"
@@ -37,6 +38,13 @@ const (
        ActionFill   = "fill"
 )
 
+var Actions = map[string]bool{
+       ActionResize: true,
+       ActionCrop:   true,
+       ActionFit:    true,
+       ActionFill:   true,
+}
+
 var (
        imageFormats = map[string]Format{
                ".jpg":  JPEG,
@@ -64,9 +72,9 @@ var (
        // Add or increment if changes to an image format's processing requires
        // re-generation.
        imageFormatsVersions = map[Format]int{
-               PNG:  3, // Fix transparency issue with 32 bit images.
-               WEBP: 2, // Fix transparency issue with 32 bit images.
-               GIF:  1, // Fix resize issue with animated GIFs when target != GIF.
+               PNG:  0,
+               WEBP: 0,
+               GIF:  0,
        }
 
        // Increment to mark all processed images as stale. Only use when absolutely needed.
@@ -84,6 +92,7 @@ var anchorPositions = map[string]gift.Anchor{
        strings.ToLower("BottomLeft"):  gift.BottomLeftAnchor,
        strings.ToLower("Bottom"):      gift.BottomAnchor,
        strings.ToLower("BottomRight"): gift.BottomRightAnchor,
+       smartCropIdentifier:            SmartCropAnchor,
 }
 
 // These encoding hints are currently only relevant for Webp.
@@ -176,7 +185,7 @@ func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, Ima
                        return i, nil, err
                }
 
-               if i.Imaging.Anchor != "" && i.Imaging.Anchor != smartCropIdentifier {
+               if i.Imaging.Anchor != "" {
                        anchor, found := anchorPositions[i.Imaging.Anchor]
                        if !found {
                                return i, nil, fmt.Errorf("invalid anchor value %q in imaging config", i.Anchor)
@@ -201,36 +210,34 @@ func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, Ima
        return ns, nil
 }
 
-func DecodeImageConfig(action string, options []string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal], sourceFormat Format) (ImageConfig, error) {
+func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal], sourceFormat Format) (ImageConfig, error) {
        var (
-               c   ImageConfig = GetDefaultImageConfig(action, defaults)
+               c   ImageConfig = GetDefaultImageConfig(defaults)
                err error
        )
 
-       action = strings.ToLower(action)
-
-       c.Action = action
-
-       if options == nil {
-               return c, errors.New("image options cannot be empty")
+       // Make to lower case, trim space and remove any empty strings.
+       n := 0
+       for _, s := range options {
+               s = strings.TrimSpace(s)
+               if s != "" {
+                       options[n] = strings.ToLower(s)
+                       n++
+               }
        }
+       options = options[:n]
 
        for _, part := range options {
-               part = strings.ToLower(part)
-
-               if part == smartCropIdentifier {
-                       c.AnchorStr = smartCropIdentifier
+               if _, ok := Actions[part]; ok {
+                       c.Action = part
                } else if pos, ok := anchorPositions[part]; ok {
                        c.Anchor = pos
-                       c.AnchorStr = part
                } else if filter, ok := imageFilters[part]; ok {
                        c.Filter = filter
-                       c.FilterStr = part
                } else if hint, ok := hints[part]; ok {
                        c.Hint = hint
                } else if part[0] == '#' {
-                       c.BgColorStr = part[1:]
-                       c.BgColor, err = hexStringToColorGo(c.BgColorStr)
+                       c.BgColor, err = hexStringToColorGo(part[1:])
                        if err != nil {
                                return c, err
                        }
@@ -291,8 +298,7 @@ func DecodeImageConfig(action string, options []string, defaults *config.ConfigN
                }
        }
 
-       if action != "" && c.FilterStr == "" {
-               c.FilterStr = defaults.Config.Imaging.ResampleFilter
+       if c.Action != "" && c.Filter == nil {
                c.Filter = defaults.Config.ResampleFilter
        }
 
@@ -300,8 +306,7 @@ func DecodeImageConfig(action string, options []string, defaults *config.ConfigN
                c.Hint = webpoptions.EncodingPresetPhoto
        }
 
-       if action != "" && c.AnchorStr == "" {
-               c.AnchorStr = defaults.Config.Imaging.Anchor
+       if c.Action != "" && c.Anchor == -1 {
                c.Anchor = defaults.Config.Anchor
        }
 
@@ -318,10 +323,23 @@ func DecodeImageConfig(action string, options []string, defaults *config.ConfigN
        if c.BgColor == nil && c.TargetFormat != sourceFormat {
                if sourceFormat.SupportsTransparency() && !c.TargetFormat.SupportsTransparency() {
                        c.BgColor = defaults.Config.BgColor
-                       c.BgColorStr = defaults.Config.Imaging.BgColor
                }
        }
 
+       if mainImageVersionNumber > 0 {
+               options = append(options, strconv.Itoa(mainImageVersionNumber))
+       }
+
+       if v, ok := imageFormatsVersions[sourceFormat]; ok && v > 0 {
+               options = append(options, strconv.Itoa(v))
+       }
+
+       if smartCropVersionNumber > 0 && c.Anchor == SmartCropAnchor {
+               options = append(options, strconv.Itoa(smartCropVersionNumber))
+       }
+
+       c.Key = hashing.HashStringHex(options)
+
        return c, nil
 }
 
@@ -350,8 +368,7 @@ type ImageConfig struct {
        // not support transparency.
        // When set per image operation, it's used even for formats that does support
        // transparency.
-       BgColor    color.Color
-       BgColorStr string
+       BgColor color.Color
 
        // Hint about what type of picture this is. Used to optimize encoding
        // when target is set to webp.
@@ -360,57 +377,15 @@ type ImageConfig struct {
        Width  int
        Height int
 
-       Filter    gift.Resampling
-       FilterStr string
+       Filter gift.Resampling
 
-       Anchor    gift.Anchor
-       AnchorStr string
+       Anchor gift.Anchor
 }
 
-func (i ImageConfig) GetKey(format Format) string {
-       if i.Key != "" {
-               return i.Action + "_" + i.Key
-       }
-
-       k := strconv.Itoa(i.Width) + "x" + strconv.Itoa(i.Height)
-       if i.Action != "" {
-               k += "_" + i.Action
-       }
-       // This slightly odd construct is here to preserve the old image keys.
-       if i.qualitySetForImage || i.TargetFormat.RequiresDefaultQuality() {
-               k += "_q" + strconv.Itoa(i.Quality)
-       }
-       if i.Rotate != 0 {
-               k += "_r" + strconv.Itoa(i.Rotate)
-       }
-       if i.BgColorStr != "" {
-               k += "_bg" + i.BgColorStr
-       }
-
-       if i.TargetFormat == WEBP {
-               k += "_h" + strconv.Itoa(int(i.Hint))
-       }
-
-       anchor := i.AnchorStr
-       if anchor == smartCropIdentifier {
-               anchor = anchor + strconv.Itoa(smartCropVersionNumber)
-       }
-
-       k += "_" + i.FilterStr
-
-       if i.Action == ActionFill || i.Action == ActionCrop {
-               k += "_" + anchor
-       }
-
-       if v, ok := imageFormatsVersions[format]; ok {
-               k += "_" + strconv.Itoa(v)
-       }
-
-       if mainImageVersionNumber > 0 {
-               k += "_" + strconv.Itoa(mainImageVersionNumber)
-       }
-
-       return k
+func (cfg ImageConfig) Reanchor(a gift.Anchor) ImageConfig {
+       cfg.Anchor = a
+       cfg.Key = hashing.HashStringHex(cfg.Key, "reanchor", a)
+       return cfg
 }
 
 type ImagingConfigInternal struct {
@@ -429,7 +404,7 @@ func (i *ImagingConfigInternal) Compile(externalCfg *ImagingConfig) error {
                return err
        }
 
-       if externalCfg.Anchor != "" && externalCfg.Anchor != smartCropIdentifier {
+       if externalCfg.Anchor != "" {
                anchor, found := anchorPositions[externalCfg.Anchor]
                if !found {
                        return fmt.Errorf("invalid anchor value %q in imaging config", i.Anchor)
index 6dd545f2cfead8bc83b761b664b3396b1e2f150c..d3c9827bd55e50e86ff19bb1c379afd374099eb1 100644 (file)
@@ -19,6 +19,7 @@ import (
        "testing"
 
        qt "github.com/frankban/quicktest"
+       "github.com/gohugoio/hugo/common/hashing"
 )
 
 func TestDecodeConfig(t *testing.T) {
@@ -106,7 +107,8 @@ func TestDecodeImageConfig(t *testing.T) {
                if err != nil {
                        t.Fatal(err)
                }
-               result, err := DecodeImageConfig(this.action, strings.Fields(this.in), cfg, PNG)
+               options := append([]string{this.action}, strings.Fields(this.in)...)
+               result, err := DecodeImageConfig(options, cfg, PNG)
                if b, ok := this.expect.(bool); ok && !b {
                        if err == nil {
                                t.Errorf("[%d] parseImageConfig didn't return an expected error", i)
@@ -115,15 +117,19 @@ func TestDecodeImageConfig(t *testing.T) {
                        if err != nil {
                                t.Fatalf("[%d] err: %s", i, err)
                        }
-                       if fmt.Sprint(result) != fmt.Sprint(this.expect) {
-                               t.Fatalf("[%d] got\n%v\n but expected\n%v", i, result, this.expect)
+                       expect := this.expect.(ImageConfig)
+                       expect.Key = hashing.HashStringHex(options)
+
+                       if fmt.Sprint(result) != fmt.Sprint(expect) {
+                               t.Fatalf("[%d] got\n%v\n but expected\n%v", i, result, expect)
                        }
                }
        }
 }
 
 func newImageConfig(action string, width, height, quality, rotate int, filter, anchor, bgColor string) ImageConfig {
-       var c ImageConfig = GetDefaultImageConfig(action, nil)
+       var c ImageConfig = GetDefaultImageConfig(nil)
+       c.Action = action
        c.TargetFormat = PNG
        c.Hint = 2
        c.Width = width
@@ -131,26 +137,20 @@ func newImageConfig(action string, width, height, quality, rotate int, filter, a
        c.Quality = quality
        c.qualitySetForImage = quality != 75
        c.Rotate = rotate
-       c.BgColorStr = bgColor
        c.BgColor, _ = hexStringToColorGo(bgColor)
+       c.Anchor = SmartCropAnchor
 
        if filter != "" {
                filter = strings.ToLower(filter)
                if v, ok := imageFilters[filter]; ok {
                        c.Filter = v
-                       c.FilterStr = filter
                }
        }
 
        if anchor != "" {
-               if anchor == smartCropIdentifier {
-                       c.AnchorStr = anchor
-               } else {
-                       anchor = strings.ToLower(anchor)
-                       if v, ok := anchorPositions[anchor]; ok {
-                               c.Anchor = v
-                               c.AnchorStr = anchor
-                       }
+               anchor = strings.ToLower(anchor)
+               if v, ok := anchorPositions[anchor]; ok {
+                       c.Anchor = v
                }
        }
 
index 64776affee598b488e23529b3ffcd5902913cd1d..9c2b9b46f9ec7586773d6449830d1092c5e356e1 100644 (file)
@@ -36,10 +36,11 @@ type Filters struct{}
 
 // Process creates a filter that processes an image using the given specification.
 func (*Filters) Process(spec any) gift.Filter {
+       specs := strings.ToLower(cast.ToString(spec))
        return filter{
-               Options: newFilterOpts(spec),
+               Options: newFilterOpts(specs),
                Filter: processFilter{
-                       spec: cast.ToString(spec),
+                       spec: specs,
                },
        }
 }
index 4d5285acdd2f79fabe609adbd2e44ad35a3af67f..c891b016844a85b355283568a5140bc73e1bc916 100644 (file)
@@ -217,7 +217,7 @@ func (p *ImageProcessor) FiltersFromConfig(src image.Image, conf ImageConfig) ([
        case "resize":
                filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter))
        case "crop":
-               if conf.AnchorStr == smartCropIdentifier {
+               if conf.Anchor == SmartCropAnchor {
                        bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter)
                        if err != nil {
                                return nil, err
@@ -232,7 +232,7 @@ func (p *ImageProcessor) FiltersFromConfig(src image.Image, conf ImageConfig) ([
                        filters = append(filters, gift.CropToSize(conf.Width, conf.Height, conf.Anchor))
                }
        case "fill":
-               if conf.AnchorStr == smartCropIdentifier {
+               if conf.Anchor == SmartCropAnchor {
                        bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter)
                        if err != nil {
                                return nil, err
@@ -329,12 +329,12 @@ func (p *ImageProcessor) doFilter(src image.Image, targetFormat Format, filters
        return dst, nil
 }
 
-func GetDefaultImageConfig(action string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) ImageConfig {
+func GetDefaultImageConfig(defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) ImageConfig {
        if defaults == nil {
                defaults = defaultImageConfig
        }
        return ImageConfig{
-               Action:  action,
+               Anchor:  -1, // The real values start at 0.
                Hint:    defaults.Config.Hint,
                Quality: defaults.Config.Imaging.Quality,
        }
index 526e3ba534c4a8dd942acc34036971b32d71dee2..c49de7bd191618475878a648df63ef78bd397767 100644 (file)
@@ -15,6 +15,7 @@ package images_test
 
 import (
        _ "image/jpeg"
+       "strings"
        "testing"
 
        "github.com/gohugoio/hugo/resources/images/imagetesting"
@@ -158,6 +159,76 @@ the last entry will win.
        imagetesting.RunGolden(opts)
 }
 
+// Issue 13272, 13273.
+func TestImagesGoldenFiltersMaskCacheIssues(t *testing.T) {
+       if imagetesting.SkipGoldenTests {
+               t.Skip("Skip golden test on this architecture")
+       }
+
+       // Will be used as the base folder for generated images.
+       name := "filters/mask2"
+
+       files := `
+-- hugo.toml --
+[caches]
+  [caches.images]
+    dir = ':cacheDir/golden_images'
+       maxAge = "30s"
+[imaging]
+  bgColor = '#33ff44'
+  hint = 'photo'
+  quality = 75
+  resampleFilter = 'Lanczos'
+-- assets/sunset.jpg --
+sourcefilename: ../testdata/sunset.jpg
+-- assets/mask.png --
+sourcefilename: ../testdata/mask.png
+
+-- layouts/index.html --
+Home.
+{{ $sunset := resources.Get "sunset.jpg" }}
+{{ $mask := resources.Get "mask.png" }}
+
+
+{{ template "mask" (dict "name" "green.jpg" "base" $sunset  "mask" $mask) }}
+
+{{ define "mask"}}
+{{ $ext := path.Ext .name }}
+{{ if lt (len (path.Ext .name)) 4 }}
+       {{ errorf "No extension in %q" .name }}
+{{ end }}
+{{ $format := strings.TrimPrefix "." $ext }}
+{{ $spec := .spec | default (printf "resize x300 %s" $format) }}
+{{ $filters := slice (images.Process $spec) (images.Mask .mask) }}
+{{ $name := printf "images/%s" .name  }}
+{{ $img := .base.Filter $filters }}
+{{ with $img | resources.Copy $name }}
+{{ .Publish }}
+{{ end }}
+{{ end }}
+`
+
+       tempDir := t.TempDir()
+
+       opts := imagetesting.DefaultGoldenOpts
+       opts.WorkingDir = tempDir
+       opts.T = t
+       opts.Name = name
+       opts.Files = files
+       opts.SkipAssertions = true
+
+       imagetesting.RunGolden(opts)
+
+       files = strings.Replace(files, "#33ff44", "#a83269", -1)
+       files = strings.Replace(files, "green", "pink", -1)
+       files = strings.Replace(files, "mask.png", "mask2.png", -1)
+       opts.Files = files
+       opts.SkipAssertions = false
+       opts.Rebuild = true
+
+       imagetesting.RunGolden(opts)
+}
+
 func TestImagesGoldenFiltersText(t *testing.T) {
        t.Parallel()
 
@@ -263,3 +334,74 @@ Home.
 
        imagetesting.RunGolden(opts)
 }
+
+func TestImagesGoldenMethods(t *testing.T) {
+       t.Parallel()
+
+       if imagetesting.SkipGoldenTests {
+               t.Skip("Skip golden test on this architecture")
+       }
+
+       // Will be used as the base folder for generated images.
+       name := "methods"
+
+       files := `
+-- hugo.toml --
+[imaging]
+  bgColor = '#ebcc34'
+  hint = 'photo'
+  quality = 75
+  resampleFilter = 'MitchellNetravali'
+-- assets/sunset.jpg --
+sourcefilename: ../testdata/sunset.jpg
+-- assets/gopher.png --
+sourcefilename: ../testdata/gopher-hero8.png
+
+-- layouts/index.html --
+Home.
+{{ $sunset := resources.Get "sunset.jpg" }}
+{{ $gopher := resources.Get "gopher.png" }}
+
+
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "resize"  "spec" "300x" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "resize" "spec" "x200" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "fill"  "spec" "90x120 left" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "fill"  "spec" "90x120 right" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "fit"  "spec" "200x200" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop"  "spec" "200x200" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop"  "spec" "350x400 center" ) }}
+ {{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop"  "spec" "350x400 smart" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop"  "spec" "350x400 center r90" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop"  "spec" "350x400 center q20" ) }}
+{{ template "invoke" (dict "copyFormat" "png" "base" $gopher "method" "resize"  "spec" "100x" ) }}
+{{ template "invoke" (dict "copyFormat" "png" "base" $gopher "method" "resize"  "spec" "100x #fc03ec" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $gopher "method" "resize"  "spec" "100x #03fc56 jpg" ) }}
+
+{{ define "invoke"}}
+{{ $spec := .spec }}
+{{ $name := printf "images/%s-%s-%s.%s" .method ((trim .base.Name "/") | lower | anchorize) ($spec | anchorize) .copyFormat  }}
+{{ $img := ""}}
+{{ if eq .method "resize" }}
+       {{ $img = .base.Resize $spec }}
+{{ else if eq .method "fill" }}
+       {{ $img = .base.Fill $spec }}
+{{ else if eq .method "fit" }}
+       {{ $img = .base.Fit $spec }}
+{{ else if eq .method "crop" }}
+       {{ $img = .base.Crop $spec }}
+{{ else }}
+       {{ errorf "Unknown method %q" .method }}
+{{ end }}
+{{ with $img | resources.Copy $name }}
+{{ .Publish }}
+{{ end }}
+{{ end }}
+`
+
+       opts := imagetesting.DefaultGoldenOpts
+       opts.T = t
+       opts.Name = name
+       opts.Files = files
+
+       imagetesting.RunGolden(opts)
+}
index 8e00751e0c2dbbb7e5a122c04aa4c5dbf0d079de..22a2317a100d9f80f344f061964ca478a0842f68 100644 (file)
@@ -63,8 +63,18 @@ type GoldenImageTestOpts struct {
        // Set to true to write golden files to disk.
        WriteFiles bool
 
+       // If not set, a temporary directory will be created.
+       WorkingDir string
+
        // Set to true to skip any assertions. Useful when adding new golden variants to a test.
        DevMode bool
+
+       // Set to skip any assertions.
+       SkipAssertions bool
+
+       // Whether this represents a rebuild of the same site.
+       // Setting this to true will keep the previous golden image set.
+       Rebuild bool
 }
 
 // To rebuild all Golden image tests, toggle WriteFiles=true and run:
@@ -78,7 +88,10 @@ var DefaultGoldenOpts = GoldenImageTestOpts{
 func RunGolden(opts GoldenImageTestOpts) *hugolib.IntegrationTestBuilder {
        opts.T.Helper()
 
-       c := hugolib.Test(opts.T, opts.Files, hugolib.TestOptWithOSFs()) // hugolib.TestOptWithPrintAndKeepTempDir(true))
+       c := hugolib.Test(opts.T, opts.Files, hugolib.TestOptWithConfig(func(conf *hugolib.IntegrationTestConfig) {
+               conf.NeedsOsFS = true
+               conf.WorkingDir = opts.WorkingDir
+       }))
        c.AssertFileContent("public/index.html", "Home.")
 
        outputDir := filepath.Join(c.H.Conf.WorkingDir(), "public", "images")
@@ -86,12 +99,18 @@ func RunGolden(opts GoldenImageTestOpts) *hugolib.IntegrationTestBuilder {
        goldenDir := filepath.Join(goldenBaseDir, filepath.FromSlash(opts.Name))
        if opts.WriteFiles {
                c.Assert(htesting.IsRealCI(), qt.IsFalse)
-               c.Assert(os.MkdirAll(goldenBaseDir, 0o777), qt.IsNil)
-               c.Assert(os.RemoveAll(goldenDir), qt.IsNil)
+               if !opts.Rebuild {
+                       c.Assert(os.MkdirAll(goldenBaseDir, 0o777), qt.IsNil)
+                       c.Assert(os.RemoveAll(goldenDir), qt.IsNil)
+               }
                c.Assert(hugio.CopyDir(hugofs.Os, outputDir, goldenDir, nil), qt.IsNil)
                return c
        }
 
+       if opts.SkipAssertions {
+               return c
+       }
+
        if opts.DevMode {
                c.Assert(htesting.IsRealCI(), qt.IsFalse)
                return c
index 864c6de0ae0ecdb279e2f1c8bb6a89f14ef03697..af45c241c18d8d2488afa607d800454bf6d924b9 100644 (file)
@@ -25,10 +25,10 @@ import (
 const (
        // Do not change.
        smartCropIdentifier = "smart"
-
-       // This is just a increment, starting on 1. If Smart Crop improves its cropping, we
+       SmartCropAnchor     = 1000
+       // This is just a increment, starting on 0. If Smart Crop improves its cropping, we
        // need a way to trigger a re-generation of the crops in the wild, so increment this.
-       smartCropVersionNumber = 1
+       smartCropVersionNumber = 0
 )
 
 func (p *ImageProcessor) newSmartCropAnalyzer(filter gift.Resampling) smartcrop.Analyzer {
diff --git a/resources/images/testdata/images_golden/filters/mask2/green.jpg b/resources/images/testdata/images_golden/filters/mask2/green.jpg
new file mode 100644 (file)
index 0000000..48a9dd0
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/mask2/green.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/mask2/pink.jpg b/resources/images/testdata/images_golden/filters/mask2/pink.jpg
new file mode 100644 (file)
index 0000000..640e41a
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/mask2/pink.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/crop-sunsetjpg-200x200.jpg b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-200x200.jpg
new file mode 100644 (file)
index 0000000..10569ab
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-200x200.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center-q20.jpg b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center-q20.jpg
new file mode 100644 (file)
index 0000000..1b316ff
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center-q20.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center-r90.jpg b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center-r90.jpg
new file mode 100644 (file)
index 0000000..9e3bc4b
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center-r90.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center.jpg b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center.jpg
new file mode 100644 (file)
index 0000000..aa89eed
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-smart.jpg b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-smart.jpg
new file mode 100644 (file)
index 0000000..7a20392
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-smart.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/fill-sunsetjpg-90x120-left.jpg b/resources/images/testdata/images_golden/methods/fill-sunsetjpg-90x120-left.jpg
new file mode 100644 (file)
index 0000000..39f1823
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/fill-sunsetjpg-90x120-left.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/fill-sunsetjpg-90x120-right.jpg b/resources/images/testdata/images_golden/methods/fill-sunsetjpg-90x120-right.jpg
new file mode 100644 (file)
index 0000000..4afcb6d
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/fill-sunsetjpg-90x120-right.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/fit-sunsetjpg-200x200.jpg b/resources/images/testdata/images_golden/methods/fit-sunsetjpg-200x200.jpg
new file mode 100644 (file)
index 0000000..75c6a30
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/fit-sunsetjpg-200x200.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-03fc56-jpg.jpg b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-03fc56-jpg.jpg
new file mode 100644 (file)
index 0000000..2e78ce4
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-03fc56-jpg.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-fc03ec.png b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-fc03ec.png
new file mode 100644 (file)
index 0000000..44af253
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-fc03ec.png differ
diff --git a/resources/images/testdata/images_golden/methods/resize-gopherpng-100x.png b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x.png
new file mode 100644 (file)
index 0000000..9bca47b
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x.png differ
diff --git a/resources/images/testdata/images_golden/methods/resize-sunsetjpg-300x.jpg b/resources/images/testdata/images_golden/methods/resize-sunsetjpg-300x.jpg
new file mode 100644 (file)
index 0000000..319385e
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/resize-sunsetjpg-300x.jpg differ
diff --git a/resources/images/testdata/images_golden/methods/resize-sunsetjpg-x200.jpg b/resources/images/testdata/images_golden/methods/resize-sunsetjpg-x200.jpg
new file mode 100644 (file)
index 0000000..77b2555
Binary files /dev/null and b/resources/images/testdata/images_golden/methods/resize-sunsetjpg-x200.jpg differ
index 29b9e5ddd8211cb84793fc2bec95b58d40d84ff7..6ef9bdae050b1529dc727b436399d98c0a86a163 100644 (file)
@@ -363,6 +363,7 @@ type genericResource struct {
        sd    ResourceSourceDescriptor
        paths internal.ResourcePaths
 
+       includeHashInKey     bool
        sourceFilenameIsHash bool
 
        h *resourceHash // A hash of the source content. Is only calculated in caching situations.
@@ -452,6 +453,10 @@ func (l *genericResource) Key() string {
                if l.spec.Cfg.IsMultihost() {
                        l.key = l.spec.Lang() + l.key
                }
+
+               if l.includeHashInKey && !l.sourceFilenameIsHash {
+                       l.key += fmt.Sprintf("_%d", l.hash())
+               }
        })
 
        return l.key
index 912a0d786f0acd35097af3546ece6d940adc5a9f..f1c30e0a20910c86c91c9cf9a9ef38cdcc38aafd 100644 (file)
@@ -183,29 +183,33 @@ func (r *Spec) NewResource(rd ResourceSourceDescriptor) (resource.Resource, erro
                TargetBasePaths: rd.TargetBasePaths,
        }
 
+       isImage := rd.MediaType.MainType == "image"
+       var imgFormat images.Format
+       if isImage {
+               imgFormat, isImage = images.ImageFormatFromMediaSubType(rd.MediaType.SubType)
+       }
+
        gr := &genericResource{
-               Staler:      &AtomicStaler{},
-               h:           &resourceHash{},
-               publishInit: &sync.Once{},
-               keyInit:     &sync.Once{},
-               paths:       rp,
-               spec:        r,
-               sd:          rd,
-               params:      rd.Params,
-               name:        rd.NameOriginal,
-               title:       rd.Title,
+               Staler:           &AtomicStaler{},
+               h:                &resourceHash{},
+               publishInit:      &sync.Once{},
+               keyInit:          &sync.Once{},
+               includeHashInKey: isImage,
+               paths:            rp,
+               spec:             r,
+               sd:               rd,
+               params:           rd.Params,
+               name:             rd.NameOriginal,
+               title:            rd.Title,
        }
 
-       if rd.MediaType.MainType == "image" {
-               imgFormat, ok := images.ImageFormatFromMediaSubType(rd.MediaType.SubType)
-               if ok {
-                       ir := &imageResource{
-                               Image:        images.NewImage(imgFormat, r.imaging, nil, gr),
-                               baseResource: gr,
-                       }
-                       ir.root = ir
-                       return newResourceAdapter(gr.spec, rd.LazyPublish, ir), nil
+       if isImage {
+               ir := &imageResource{
+                       Image:        images.NewImage(imgFormat, r.imaging, nil, gr),
+                       baseResource: gr,
                }
+               ir.root = ir
+               return newResourceAdapter(gr.spec, rd.LazyPublish, ir), nil
 
        }
 
index c26eb8fffab140564677870f46aa57eaaf0693dc..0c45b775a06704683597696752b7e2eb1ca9711f 100644 (file)
@@ -62,9 +62,9 @@ anigif: {{ $anigif.RelPermalink }}|{{ $anigif.Width }}|{{ $anigif.Height }}|{{ $
 
        assertImages := func() {
                b.AssertFileContent("public/index.html", `
              gif: /mybundle/pixel_hu14657638653019978294.gif|}|1|2|image/gif|
-               bmp: /mybundle/pixel_hu14705577916774115224.bmp|}|2|3|image/bmp|
-               anigif: /mybundle/giphy_hu3665406585348417395.gif|4|5|image/gif|
gif: /mybundle/pixel_hu_93429543fc146fce.gif|}|1|2|image/gif|
+bmp: /mybundle/pixel_hu_f9bf2acd6578e2c6.bmp|}|2|3|image/bmp|
+anigif: /mybundle/giphy_hu_652d28653068b48f.gif|4|5|image/gif|
                `)
        }
 
@@ -160,9 +160,9 @@ resize 2|RelPermalink: {{ $image.RelPermalink }}|MediaType: {{ $image.MediaType
        b := hugolib.Test(t, files)
 
        b.AssertFileContent("public/index.html",
-               "jpg|RelPermalink: /images/pixel_hu13683954895608450100.jpg|MediaType: image/jpeg|Width: 1|Height: 1|",
-               "resize 1|RelPermalink: /images/pixel_hu3453403302435331853.jpg|MediaType: image/jpeg|Width: 20|Height: 30|",
-               "resize 2|RelPermalink: /images/pixel_hu3453403302435331853.jpg|MediaType: image/jpeg|Width: 20|Height: 30|",
+               "jpg|RelPermalink: /images/pixel_hu_38c3f257174fc757.jpg|MediaType: image/jpeg|Width: 1|Height: 1|",
+               "resize 1|RelPermalink: /images/pixel_hu_b5c2a3d88991f65a.jpg|MediaType: image/jpeg|Width: 20|Height: 30|",
+               "resize 2|RelPermalink: /images/pixel_hu_b5c2a3d88991f65a.jpg|MediaType: image/jpeg|Width: 20|Height: 30|",
        )
 }
 
diff --git a/resources/testdata/mask2.png b/resources/testdata/mask2.png
new file mode 100644 (file)
index 0000000..b58a5e4
Binary files /dev/null and b/resources/testdata/mask2.png differ
index b036a44d7786bef5cfccd77148099059e75d2106..79d4841b1b2c106ffc53649354c83b26b47aa019 100644 (file)
@@ -386,22 +386,15 @@ func TestTransform(t *testing.T) {
                resizedPublished1, err := img.Resize("40x40")
                c.Assert(err, qt.IsNil)
                c.Assert(resizedPublished1.Height(), qt.Equals, 40)
-               c.Assert(resizedPublished1.RelPermalink(), qt.Equals, "/gopher.changed_hu16988682630555427117.png")
-               assertShouldExist(c, spec, "public/gopher.changed_hu16988682630555427117.png", true)
+               c.Assert(resizedPublished1.RelPermalink(), qt.Equals, "/gopher.changed_hu_85920388a7ff96fa.png")
+               assertShouldExist(c, spec, "public/gopher.changed_hu_85920388a7ff96fa.png", true)
 
                // Permalink called.
                resizedPublished2, err := img.Resize("30x30")
                c.Assert(err, qt.IsNil)
                c.Assert(resizedPublished2.Height(), qt.Equals, 30)
-               c.Assert(resizedPublished2.Permalink(), qt.Equals, "https://example.com/gopher.changed_hu14141325020798305104.png")
-               assertShouldExist(c, spec, "public/gopher.changed_hu14141325020798305104.png", true)
-
-               // Not published because none of RelPermalink or Permalink was called.
-               resizedNotPublished, err := img.Resize("50x50")
-               c.Assert(err, qt.IsNil)
-               c.Assert(resizedNotPublished.Height(), qt.Equals, 50)
-               // c.Assert(resized.RelPermalink(), qt.Equals, "/gopher.changed_hu2e827f5a78333ebc04166dd643235dea_1462_50x50_resize_linear_2.png")
-               assertShouldExist(c, spec, "public/gopher.changed_hu2e827f5a78333ebc04166dd643235dea_1462_50x50_resize_linear_3.png", false)
+               c.Assert(resizedPublished2.Permalink(), qt.Equals, "https://example.com/gopher.changed_hu_c8d8163c08643a7f.png")
+               assertShouldExist(c, spec, "public/gopher.changed_hu_c8d8163c08643a7f.png", true)
 
                assertNoDuplicateWrites(c, spec)
        })
index 563b3c45582e392398c9bd3fcbf6a4b4cdae039f..da3ff11685074ec877ab41d2b1c36840573a0866 100644 (file)
@@ -60,7 +60,7 @@ Copy3: {{ $copy3.RelPermalink}}|{{ $copy3.MediaType }}|{{ $copy3.Content | safeJ
 
        b.AssertFileContent("public/index.html", `
 Image Orig:  /blog/images/pixel.png|image/png|1|1|
-Image Copy1:  /blog/images/copy_hu2891316072287293157.png|image/png|3|4|
+Image Copy1:  /blog/images/copy_hu_1d9addfff177f388.png|image/png|3|4|
 Image Copy2:  /blog/images/copy2.png|image/png|3|4
 Image Copy3:  image/png|3|4|
 Orig: /blog/js/foo.js|text/javascript|let foo;|