From 843c1dce52647fab4229da53a1cc663dd51d6ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Thu, 16 Jan 2025 19:15:30 +0100 Subject: [PATCH] resources: Fix 2 image file cache key issues * 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 --- config/namespace.go | 2 +- config/namespace_test.go | 2 +- hugolib/hugo_sites_multihost_test.go | 4 +- hugolib/image_test.go | 16 +-- .../pagesfromgotmpl_integration_test.go | 2 +- hugolib/resource_chain_test.go | 4 +- resources/image.go | 110 +++++++++--------- resources/image_cache.go | 2 +- resources/image_test.go | 26 ++--- resources/images/config.go | 109 ++++++----------- resources/images/config_test.go | 28 ++--- resources/images/filters.go | 5 +- resources/images/image.go | 8 +- .../images/images_golden_integration_test.go | 71 +++++++++++ resources/images/imagetesting/testing.go | 25 +++- resources/images/smartcrop.go | 2 +- .../images_golden/filters/mask2/green.jpg | Bin 0 -> 8811 bytes .../images_golden/filters/mask2/pink.jpg | Bin 0 -> 6147 bytes resources/resource.go | 5 + resources/resource_spec.go | 42 ++++--- resources/resources_integration_test.go | 12 +- resources/testdata/mask2.png | Bin 0 -> 36823 bytes resources/transform_test.go | 15 +-- tpl/resources/resources_integration_test.go | 2 +- 24 files changed, 275 insertions(+), 217 deletions(-) create mode 100644 resources/images/testdata/images_golden/filters/mask2/green.jpg create mode 100644 resources/images/testdata/images_golden/filters/mask2/pink.jpg create mode 100644 resources/testdata/mask2.png diff --git a/config/namespace.go b/config/namespace.go index 46b5014c34c..e41b56e2d9e 100644 --- a/config/namespace.go +++ b/config/namespace.go @@ -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) diff --git a/config/namespace_test.go b/config/namespace_test.go index 5eacdeac7dd..df27ae05cf0 100644 --- a/config/namespace_test.go +++ b/config/namespace_test.go @@ -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)) } diff --git a/hugolib/hugo_sites_multihost_test.go b/hugolib/hugo_sites_multihost_test.go index 39504202b2b..37f7ab92791 100644 --- a/hugolib/hugo_sites_multihost_test.go +++ b/hugolib/hugo_sites_multihost_test.go @@ -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) { diff --git a/hugolib/image_test.go b/hugolib/image_test.go index 7dcd9fc26ab..09a5b841e90 100644 --- a/hugolib/image_test.go +++ b/hugolib/image_test.go @@ -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() diff --git a/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go b/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go index b033aad2b65..64ee7039731 100644 --- a/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go +++ b/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go @@ -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 diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go index 669114c8a4a..942873ae4c9 100644 --- a/hugolib/resource_chain_test.go +++ b/hugolib/resource_chain_test.go @@ -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+8= /styles.min.a1df58687c3c9cc38bf26532f7b4b2f2c2b0315dcde212376959995c04f11fef.css CSS integrity Data last: /styles2.min.1cfc52986836405d37f9998a63fd6dd8608e8c410e5e3db1daaa30f78bc273ba.css sha256-HPxSmGg2QF03+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 : error calling GetRemote: Get "gopher://example.org": unsupported protocol scheme "gopher"| diff --git a/resources/image.go b/resources/image.go index 686f70e274d..ed5f74d77c1 100644 --- a/resources/image.go +++ b/resources/image.go @@ -205,6 +205,7 @@ func (i *imageResource) cloneWithUpdates(u *transformationUpdate) (baseResource, }, nil } +// TODO1 remove var imageActions = []string{images.ActionResize, images.ActionCrop, images.ActionFit, images.ActionFill} // Process processes the image with the given spec. @@ -212,8 +213,7 @@ var imageActions = []string{images.ActionResize, images.ActionCrop, images.Actio // 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 +243,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 +251,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 +296,39 @@ 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 { +// TODO1 remove me. +func (i *imageResource) resolveActionTargetFormatOptions(spec string) (string, images.Format, []string) { + var ( + action string + targetFormat images.Format + options = strings.Fields(spec) + ) + n := 0 + for _, p := range options { + var remove bool if hstrings.InSlicEqualFold(imageActions, p) { action = p - options = append(options[:i], options[i+1:]...) - break + remove = true + } else if f, ok := images.ImageFormatFromExt("." + p); ok { + targetFormat = f + remove = true + } + if !remove { + options[n] = p + n++ } } - return action, options + options = options[:n] + return action, targetFormat, 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 +340,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 +413,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 +481,32 @@ 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)) + if conf.Key == "" { + panic("conf.Key not set") // TODO1 remove me. + } + 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 } diff --git a/resources/image_cache.go b/resources/image_cache.go index d824c5d1a06..1fc7226095e 100644 --- a/resources/image_cache.go +++ b/resources/image_cache.go @@ -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 diff --git a/resources/image_test.go b/resources/image_test.go index 5639d457e07..1ba5a149a32 100644 --- a/resources/image_test.go +++ b/resources/image_test.go @@ -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) } diff --git a/resources/images/config.go b/resources/images/config.go index 9655e9a51ce..80545ecdac1 100644 --- a/resources/images/config.go +++ b/resources/images/config.go @@ -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, @@ -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,36 @@ 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) + c.Key = hashing.HashStringHex(options) - if part == smartCropIdentifier { - c.AnchorStr = smartCropIdentifier + for _, part := range options { + 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 +300,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 +308,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,7 +325,6 @@ 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 } } @@ -350,8 +356,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 +365,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 +392,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) diff --git a/resources/images/config_test.go b/resources/images/config_test.go index 6dd545f2cfe..d3c9827bd55 100644 --- a/resources/images/config_test.go +++ b/resources/images/config_test.go @@ -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 } } diff --git a/resources/images/filters.go b/resources/images/filters.go index 64776affee5..9c2b9b46f9e 100644 --- a/resources/images/filters.go +++ b/resources/images/filters.go @@ -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, }, } } diff --git a/resources/images/image.go b/resources/images/image.go index 4d5285acdd2..c891b016844 100644 --- a/resources/images/image.go +++ b/resources/images/image.go @@ -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, } diff --git a/resources/images/images_golden_integration_test.go b/resources/images/images_golden_integration_test.go index 526e3ba534c..4a8f6a0a0d3 100644 --- a/resources/images/images_golden_integration_test.go +++ b/resources/images/images_golden_integration_test.go @@ -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() diff --git a/resources/images/imagetesting/testing.go b/resources/images/imagetesting/testing.go index 8e00751e0c2..22a2317a100 100644 --- a/resources/images/imagetesting/testing.go +++ b/resources/images/imagetesting/testing.go @@ -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 diff --git a/resources/images/smartcrop.go b/resources/images/smartcrop.go index 864c6de0ae0..aa6cf9a87ef 100644 --- a/resources/images/smartcrop.go +++ b/resources/images/smartcrop.go @@ -25,7 +25,7 @@ import ( const ( // Do not change. smartCropIdentifier = "smart" - + SmartCropAnchor = 1000 // This is just a increment, starting on 1. 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 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 index 0000000000000000000000000000000000000000..48a9dd0830f7437a20f7acc07c45e8ed2fc8c7c9 GIT binary patch literal 8811 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 R3@w50UuVew>-ERX{{nqf%KZQU literal 0 HcmV?d00001 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 index 0000000000000000000000000000000000000000..640e41ab156598b15259c5fd999e745376b69f9f GIT binary patch literal 6147 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 literal 0 HcmV?d00001 diff --git a/resources/resource.go b/resources/resource.go index 29b9e5ddd82..6ef9bdae050 100644 --- a/resources/resource.go +++ b/resources/resource.go @@ -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 diff --git a/resources/resource_spec.go b/resources/resource_spec.go index 912a0d786f0..f1c30e0a209 100644 --- a/resources/resource_spec.go +++ b/resources/resource_spec.go @@ -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 } diff --git a/resources/resources_integration_test.go b/resources/resources_integration_test.go index c26eb8fffab..0c45b775a06 100644 --- a/resources/resources_integration_test.go +++ b/resources/resources_integration_test.go @@ -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 index 0000000000000000000000000000000000000000..b58a5e4b071b372bba6eac8f0e02d2798cb7ccad GIT binary patch literal 36823 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 JsWJA#{SQ