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..c1f107b592f 100644 --- a/resources/image.go +++ b/resources/image.go @@ -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 } 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_extended_test.go b/resources/image_extended_test.go index e570c4a3d24..f481be9bb40 100644 --- a/resources/image_extended_test.go +++ b/resources/image_extended_test.go @@ -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) } 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..6fcd2e334ba 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, @@ -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) 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..c49de7bd191 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() @@ -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) +} 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..af45c241c18 100644 --- a/resources/images/smartcrop.go +++ b/resources/images/smartcrop.go @@ -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 index 00000000000..48a9dd0830f 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 index 00000000000..640e41ab156 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 index 00000000000..10569ab2c55 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 index 00000000000..1b316ff17c5 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 index 00000000000..9e3bc4bf570 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 index 00000000000..aa89eedf28d 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 index 00000000000..7a203920e1b 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 index 00000000000..39f1823f5db 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 index 00000000000..4afcb6decdb 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 index 00000000000..75c6a30b024 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 index 00000000000..2e78ce461c1 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 index 00000000000..44af253036f 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 index 00000000000..9bca47b147f 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 index 00000000000..319385e9a5a 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 index 00000000000..77b2555cb6e Binary files /dev/null and b/resources/images/testdata/images_golden/methods/resize-sunsetjpg-x200.jpg differ 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 00000000000..b58a5e4b071 Binary files /dev/null and b/resources/testdata/mask2.png differ diff --git a/resources/transform_test.go b/resources/transform_test.go index b036a44d778..79d4841b1b2 100644 --- a/resources/transform_test.go +++ b/resources/transform_test.go @@ -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) }) diff --git a/tpl/resources/resources_integration_test.go b/tpl/resources/resources_integration_test.go index 563b3c45582..da3ff116850 100644 --- a/tpl/resources/resources_integration_test.go +++ b/tpl/resources/resources_integration_test.go @@ -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;|