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
|
@ -22,7 +22,7 @@ import (
|
||||||
func DecodeNamespace[S, C any](configSource any, buildConfig func(any) (C, any, error)) (*ConfigNamespace[S, C], error) {
|
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).
|
// Calculate the hash of the input (not including any defaults applied later).
|
||||||
// This allows us to introduce new config options without breaking the hash.
|
// This allows us to introduce new config options without breaking the hash.
|
||||||
h := hashing.HashString(configSource)
|
h := hashing.HashStringHex(configSource)
|
||||||
|
|
||||||
// Build the config
|
// Build the config
|
||||||
c, ext, err := buildConfig(configSource)
|
c, ext, err := buildConfig(configSource)
|
||||||
|
|
|
@ -43,7 +43,7 @@ func TestNamespace(t *testing.T) {
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
c.Assert(ns, qt.Not(qt.IsNil))
|
c.Assert(ns, qt.Not(qt.IsNil))
|
||||||
c.Assert(ns.SourceStructure, qt.DeepEquals, map[string]interface{}{"foo": "bar"})
|
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.Config, qt.DeepEquals, &tstNsExt{Foo: "bar"})
|
||||||
c.Assert(ns.Signature(), qt.DeepEquals, []*tstNsExt(nil))
|
c.Assert(ns.Signature(), qt.DeepEquals, []*tstNsExt(nil))
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,9 +205,9 @@ title: mybundle-en
|
||||||
b.AssertFileExists("public/de/mybundle/pixel.png", true)
|
b.AssertFileExists("public/de/mybundle/pixel.png", true)
|
||||||
b.AssertFileExists("public/en/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
|
// 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) {
|
func TestMultihostResourceOneBaseURLWithSuPath(t *testing.T) {
|
||||||
|
|
|
@ -72,20 +72,20 @@ SUNSET2: {{ $resized2.RelPermalink }}/{{ $resized2.Width }}/Lat: {{ $resized2.Ex
|
||||||
|
|
||||||
b.Build(BuildCfg{})
|
b.Build(BuildCfg{})
|
||||||
|
|
||||||
b.AssertFileContent("public/index.html", "SUNSET FOR: en: /bundle/sunset_hu13235715490294913361.jpg/200/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_hu13235715490294913361.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_hu1573057890424052540.jpg/123/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_hu1573057890424052540.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
|
// 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")
|
"FocalLengthIn35mmFormat|uint16", "PENTAX")
|
||||||
|
|
||||||
b.AssertFileContent("resources/_gen/images/images/sunset_17710516992648092201.json",
|
b.AssertFileContent("resources/_gen/images/images/sunset_d209dcdc6b875e26.json",
|
||||||
"FocalLengthIn35mmFormat|uint16", "PENTAX")
|
"FocalLengthIn35mmFormat|uint16", "PENTAX")
|
||||||
|
|
||||||
b.AssertNoDuplicateWrites()
|
b.AssertNoDuplicateWrites()
|
||||||
|
|
|
@ -119,7 +119,7 @@ docs/p1/sub/mymixcasetext2.txt
|
||||||
"RelPermalink: /docs/p1/sub/mymixcasetext2.txt|Name: 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[]|",
|
"RelPermalink: /mydata.yaml|Name: sub/data1.yaml|Title: Sub data|Params: map[]|",
|
||||||
"Featured Image: /a/pixel.png|featured.png|",
|
"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
|
// Resource from string
|
||||||
"RelPermalink: /docs/p1/mytext.txt|Name: textresource|Title: My Text Resource|Params: map[param1:param1v]|",
|
"RelPermalink: /docs/p1/mytext.txt|Name: textresource|Title: My Text Resource|Params: map[param1:param1v]|",
|
||||||
// Dates
|
// Dates
|
||||||
|
|
|
@ -106,12 +106,12 @@ FAILED REMOTE ERROR DETAILS CONTENT: {{ with $failedImg }}{{ with .Err }}{{ with
|
||||||
b.AssertFileContent("public/index.html",
|
b.AssertFileContent("public/index.html",
|
||||||
fmt.Sprintf(`
|
fmt.Sprintf(`
|
||||||
SUNSET: /images/sunset.jpg|/images/sunset.a9bf1d944e19c0f382e0d8f51de690f7d0bc8fa97390c4242a86c3e5c0737e71.jpg|900|90587
|
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 first: sha256-od9YaHw8nMOL8mUy97Sy8sKwMV3N4hI3aVmZXATxH+8= /styles.min.a1df58687c3c9cc38bf26532f7b4b2f2c2b0315dcde212376959995c04f11fef.css
|
||||||
CSS integrity Data last: /styles2.min.1cfc52986836405d37f9998a63fd6dd8608e8c410e5e3db1daaa30f78bc273ba.css sha256-HPxSmGg2QF03+ZmKY/1t2GCOjEEOXj2x2qow94vCc7o=
|
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
|
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
|
REMOTE NOT FOUND: OK
|
||||||
LOCAL 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"|
|
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"|
|
||||||
|
|
|
@ -30,7 +30,6 @@ import (
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/cache/filecache"
|
"github.com/gohugoio/hugo/cache/filecache"
|
||||||
"github.com/gohugoio/hugo/common/hashing"
|
"github.com/gohugoio/hugo/common/hashing"
|
||||||
"github.com/gohugoio/hugo/common/hstrings"
|
|
||||||
"github.com/gohugoio/hugo/common/paths"
|
"github.com/gohugoio/hugo/common/paths"
|
||||||
|
|
||||||
"github.com/disintegration/gift"
|
"github.com/disintegration/gift"
|
||||||
|
@ -205,15 +204,12 @@ func (i *imageResource) cloneWithUpdates(u *transformationUpdate) (baseResource,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageActions = []string{images.ActionResize, images.ActionCrop, images.ActionFit, images.ActionFill}
|
|
||||||
|
|
||||||
// Process processes the image with the given spec.
|
// Process processes the image with the given spec.
|
||||||
// The spec can contain an optional action, one of "resize", "crop", "fit" or "fill".
|
// 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,
|
// 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.
|
// but it also supports e.g. format conversions without any resize action.
|
||||||
func (i *imageResource) Process(spec string) (images.ImageResource, error) {
|
func (i *imageResource) Process(spec string) (images.ImageResource, error) {
|
||||||
action, options := i.resolveActionOptions(spec)
|
return i.processActionSpec("", spec)
|
||||||
return i.processActionOptions(action, options)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize resizes the image to the specified width and height using the specified resampling
|
// 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) {
|
func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) {
|
||||||
var conf images.ImageConfig
|
var confMain images.ImageConfig
|
||||||
|
|
||||||
var gfilters []gift.Filter
|
var gfilters []gift.Filter
|
||||||
|
|
||||||
|
@ -251,47 +247,30 @@ func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) {
|
||||||
gfilters = append(gfilters, images.ToFilters(f)...)
|
gfilters = append(gfilters, images.ToFilters(f)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var options []string
|
||||||
targetFormat images.Format
|
|
||||||
configSet bool
|
|
||||||
)
|
|
||||||
for _, f := range gfilters {
|
for _, f := range gfilters {
|
||||||
f = images.UnwrapFilter(f)
|
f = images.UnwrapFilter(f)
|
||||||
if specProvider, ok := f.(images.ImageProcessSpecProvider); ok {
|
if specProvider, ok := f.(images.ImageProcessSpecProvider); ok {
|
||||||
action, options := i.resolveActionOptions(specProvider.ImageProcessSpec())
|
options = append(options, strings.Fields(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.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !configSet {
|
confMain, err := images.DecodeImageConfig(options, i.Proc.Cfg, i.Format)
|
||||||
conf = images.GetDefaultImageConfig("filter", i.Proc.Cfg)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
conf.Action = "filter"
|
confMain.Action = "filter"
|
||||||
conf.Key = hashing.HashString(gfilters)
|
confMain.Key = hashing.HashString(gfilters)
|
||||||
conf.TargetFormat = targetFormat
|
|
||||||
if conf.TargetFormat == 0 {
|
|
||||||
conf.TargetFormat = i.Format
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
var filters []gift.Filter
|
||||||
for _, f := range gfilters {
|
for _, f := range gfilters {
|
||||||
f = images.UnwrapFilter(f)
|
f = images.UnwrapFilter(f)
|
||||||
if specProvider, ok := f.(images.ImageProcessSpecProvider); ok {
|
if specProvider, ok := f.(images.ImageProcessSpecProvider); ok {
|
||||||
processSpec := specProvider.ImageProcessSpec()
|
options := strings.Fields(specProvider.ImageProcessSpec())
|
||||||
action, options := i.resolveActionOptions(processSpec)
|
conf, err := images.DecodeImageConfig(options, i.Proc.Cfg, i.Format)
|
||||||
conf, err := images.DecodeImageConfig(action, options, i.Proc.Cfg, i.Format)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
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) {
|
func (i *imageResource) processOptions(options []string) (images.ImageResource, error) {
|
||||||
conf, err := images.DecodeImageConfig(action, options, i.Proc.Cfg, i.Format)
|
conf, err := images.DecodeImageConfig(options, i.Proc.Cfg, i.Format)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -343,13 +310,12 @@ func (i *imageResource) processActionOptions(action string, options []string) (i
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if action == images.ActionFill {
|
if conf.Action == images.ActionFill {
|
||||||
if conf.Anchor == 0 && img.Width() == 0 || img.Height() == 0 {
|
if conf.Anchor == images.SmartCropAnchor && img.Width() == 0 || img.Height() == 0 {
|
||||||
// See https://github.com/gohugoio/hugo/issues/7955
|
// See https://github.com/gohugoio/hugo/issues/7955
|
||||||
// Smartcrop fails silently in some rare cases.
|
// Smartcrop fails silently in some rare cases.
|
||||||
// Fall back to a center fill.
|
// Fall back to a center fill.
|
||||||
conf.Anchor = gift.CenterAnchor
|
conf = conf.Reanchor(gift.CenterAnchor)
|
||||||
conf.AnchorStr = "center"
|
|
||||||
return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
|
return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
|
||||||
return i.Proc.ApplyFiltersFromConfig(src, conf)
|
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)
|
ci := i.clone(converted)
|
||||||
targetPath := i.relTargetPathFromConfig(conf)
|
targetPath := i.relTargetPathFromConfig(conf, i.getSpec().imaging.Cfg.SourceHash)
|
||||||
ci.setTargetPath(targetPath)
|
ci.setTargetPath(targetPath)
|
||||||
ci.Format = conf.TargetFormat
|
ci.Format = conf.TargetFormat
|
||||||
ci.setMediaType(conf.TargetFormat.MediaType())
|
ci.setMediaType(conf.TargetFormat.MediaType())
|
||||||
|
@ -485,26 +451,30 @@ func (i *imageResource) getImageMetaCacheTargetPath() string {
|
||||||
df := i.getResourcePaths()
|
df := i.getResourcePaths()
|
||||||
p1, _ := paths.FileAndExt(df.File)
|
p1, _ := paths.FileAndExt(df.File)
|
||||||
h := i.hash()
|
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)
|
df.File = fmt.Sprintf("%s_%s.json", p1, idStr)
|
||||||
return df.TargetPath()
|
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)
|
p1, p2 := paths.FileAndExt(i.getResourcePaths().File)
|
||||||
if conf.TargetFormat != i.Format {
|
if conf.TargetFormat != i.Format {
|
||||||
p2 = conf.TargetFormat.DefaultExtension()
|
p2 = conf.TargetFormat.DefaultExtension()
|
||||||
}
|
}
|
||||||
const prefix = "_hu"
|
|
||||||
huIdx := strings.LastIndex(p1, prefix)
|
// Do not change.
|
||||||
incomingID := "i"
|
const imageHashPrefix = "_hu_"
|
||||||
|
|
||||||
|
huIdx := strings.LastIndex(p1, imageHashPrefix)
|
||||||
|
incomingID := ""
|
||||||
if huIdx > -1 {
|
if huIdx > -1 {
|
||||||
incomingID = p1[huIdx+len(prefix):]
|
incomingID = p1[huIdx+len(imageHashPrefix):]
|
||||||
p1 = p1[:huIdx]
|
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 := 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
|
return rp
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ func (c *ImageCache) getOrCreate(
|
||||||
parent *imageResource, conf images.ImageConfig,
|
parent *imageResource, conf images.ImageConfig,
|
||||||
createImage func() (*imageResource, image.Image, error),
|
createImage func() (*imageResource, image.Image, error),
|
||||||
) (*resourceAdapter, error) {
|
) (*resourceAdapter, error) {
|
||||||
relTarget := parent.relTargetPathFromConfig(conf)
|
relTarget := parent.relTargetPathFromConfig(conf, parent.getSpec().imaging.Cfg.SourceHash)
|
||||||
relTargetPath := relTarget.TargetPath()
|
relTargetPath := relTarget.TargetPath()
|
||||||
memKey := relTargetPath
|
memKey := relTargetPath
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,6 @@ func TestImageResizeWebP(t *testing.T) {
|
||||||
resized, err := image.Resize("123x")
|
resized, err := image.Resize("123x")
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
c.Assert(image.MediaType(), qt.Equals, media.Builtin.WEBPType)
|
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)
|
c.Assert(resized.Width(), qt.Equals, 123)
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,28 +113,28 @@ func TestImageTransformBasic(t *testing.T) {
|
||||||
assertWidthHeight(resizedAndRotated, 125, 200)
|
assertWidthHeight(resizedAndRotated, 125, 200)
|
||||||
|
|
||||||
assertWidthHeight(resized, 300, 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")
|
fitted, err := resized.Fit("50x50")
|
||||||
c.Assert(err, qt.IsNil)
|
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)
|
assertWidthHeight(fitted, 50, 33)
|
||||||
|
|
||||||
// Check the MD5 key threshold
|
// Check the MD5 key threshold
|
||||||
fittedAgain, _ := fitted.Fit("10x20")
|
fittedAgain, _ := fitted.Fit("10x20")
|
||||||
fittedAgain, err = fittedAgain.Fit("10x20")
|
fittedAgain, err = fittedAgain.Fit("10x20")
|
||||||
c.Assert(err, qt.IsNil)
|
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)
|
assertWidthHeight(fittedAgain, 10, 7)
|
||||||
|
|
||||||
filled, err := image.Fill("200x100 bottomLeft")
|
filled, err := image.Fill("200x100 bottomLeft")
|
||||||
c.Assert(err, qt.IsNil)
|
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)
|
assertWidthHeight(filled, 200, 100)
|
||||||
|
|
||||||
smart, err := image.Fill("200x100 smart")
|
smart, err := image.Fill("200x100 smart")
|
||||||
c.Assert(err, qt.IsNil)
|
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)
|
assertWidthHeight(smart, 200, 100)
|
||||||
|
|
||||||
// Check cache
|
// Check cache
|
||||||
|
@ -144,12 +144,12 @@ func TestImageTransformBasic(t *testing.T) {
|
||||||
|
|
||||||
cropped, err := image.Crop("300x300 topRight")
|
cropped, err := image.Crop("300x300 topRight")
|
||||||
c.Assert(err, qt.IsNil)
|
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)
|
assertWidthHeight(cropped, 300, 300)
|
||||||
|
|
||||||
smartcropped, err := image.Crop("200x200 smart")
|
smartcropped, err := image.Crop("200x200 smart")
|
||||||
c.Assert(err, qt.IsNil)
|
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)
|
assertWidthHeight(smartcropped, 200, 200)
|
||||||
|
|
||||||
// Check cache
|
// Check cache
|
||||||
|
@ -216,7 +216,7 @@ func TestImageTransformFormat(t *testing.T) {
|
||||||
|
|
||||||
imagePng, err := image.Resize("450x png")
|
imagePng, err := image.Resize("450x png")
|
||||||
c.Assert(err, qt.IsNil)
|
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")
|
c.Assert(imagePng.ResourceType(), qt.Equals, "image")
|
||||||
assertExtWidthHeight(imagePng, ".png", 450, 281)
|
assertExtWidthHeight(imagePng, ".png", 450, 281)
|
||||||
c.Assert(imagePng.Name(), qt.Equals, "sunset.jpg")
|
c.Assert(imagePng.Name(), qt.Equals, "sunset.jpg")
|
||||||
|
@ -224,7 +224,7 @@ func TestImageTransformFormat(t *testing.T) {
|
||||||
|
|
||||||
imageGif, err := image.Resize("225x gif")
|
imageGif, err := image.Resize("225x gif")
|
||||||
c.Assert(err, qt.IsNil)
|
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")
|
c.Assert(imageGif.ResourceType(), qt.Equals, "image")
|
||||||
assertExtWidthHeight(imageGif, ".gif", 225, 141)
|
assertExtWidthHeight(imageGif, ".gif", 225, 141)
|
||||||
c.Assert(imageGif.Name(), qt.Equals, "sunset.jpg")
|
c.Assert(imageGif.Name(), qt.Equals, "sunset.jpg")
|
||||||
|
@ -247,7 +247,7 @@ func TestImagePermalinkPublishOrder(t *testing.T) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
check1 := func(img images.ImageResource) {
|
check1 := func(img images.ImageResource) {
|
||||||
resizedLink := "/a/sunset_hu7919355342577096259.jpg"
|
resizedLink := "/a/sunset_hu_3910bca82e28c9d6.jpg"
|
||||||
c.Assert(img.RelPermalink(), qt.Equals, resizedLink)
|
c.Assert(img.RelPermalink(), qt.Equals, resizedLink)
|
||||||
assertImageFile(c, spec.PublishFs, resizedLink, 100, 50)
|
assertImageFile(c, spec.PublishFs, resizedLink, 100, 50)
|
||||||
}
|
}
|
||||||
|
@ -288,12 +288,12 @@ func TestImageBugs(t *testing.T) {
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
c.Assert(resized, qt.Not(qt.IsNil))
|
c.Assert(resized, qt.Not(qt.IsNil))
|
||||||
c.Assert(resized.Width(), qt.Equals, 200)
|
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")
|
resized, err = resized.Resize("100x")
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
c.Assert(resized, qt.Not(qt.IsNil))
|
c.Assert(resized, qt.Not(qt.IsNil))
|
||||||
c.Assert(resized.Width(), qt.Equals, 100)
|
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
|
// Issue #6137
|
||||||
|
@ -391,7 +391,7 @@ func TestImageResize8BitPNG(t *testing.T) {
|
||||||
resized, err := image.Resize("800x")
|
resized, err := image.Resize("800x")
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
c.Assert(resized.MediaType().Type, qt.Equals, "image/png")
|
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)
|
c.Assert(resized.Width(), qt.Equals, 800)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/common/hashing"
|
||||||
"github.com/gohugoio/hugo/common/maps"
|
"github.com/gohugoio/hugo/common/maps"
|
||||||
"github.com/gohugoio/hugo/config"
|
"github.com/gohugoio/hugo/config"
|
||||||
"github.com/gohugoio/hugo/media"
|
"github.com/gohugoio/hugo/media"
|
||||||
|
@ -37,6 +38,13 @@ const (
|
||||||
ActionFill = "fill"
|
ActionFill = "fill"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var Actions = map[string]bool{
|
||||||
|
ActionResize: true,
|
||||||
|
ActionCrop: true,
|
||||||
|
ActionFit: true,
|
||||||
|
ActionFill: true,
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
imageFormats = map[string]Format{
|
imageFormats = map[string]Format{
|
||||||
".jpg": JPEG,
|
".jpg": JPEG,
|
||||||
|
@ -64,9 +72,9 @@ var (
|
||||||
// Add or increment if changes to an image format's processing requires
|
// Add or increment if changes to an image format's processing requires
|
||||||
// re-generation.
|
// re-generation.
|
||||||
imageFormatsVersions = map[Format]int{
|
imageFormatsVersions = map[Format]int{
|
||||||
PNG: 3, // Fix transparency issue with 32 bit images.
|
PNG: 0,
|
||||||
WEBP: 2, // Fix transparency issue with 32 bit images.
|
WEBP: 0,
|
||||||
GIF: 1, // Fix resize issue with animated GIFs when target != GIF.
|
GIF: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment to mark all processed images as stale. Only use when absolutely needed.
|
// 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("BottomLeft"): gift.BottomLeftAnchor,
|
||||||
strings.ToLower("Bottom"): gift.BottomAnchor,
|
strings.ToLower("Bottom"): gift.BottomAnchor,
|
||||||
strings.ToLower("BottomRight"): gift.BottomRightAnchor,
|
strings.ToLower("BottomRight"): gift.BottomRightAnchor,
|
||||||
|
smartCropIdentifier: SmartCropAnchor,
|
||||||
}
|
}
|
||||||
|
|
||||||
// These encoding hints are currently only relevant for Webp.
|
// 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
|
return i, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if i.Imaging.Anchor != "" && i.Imaging.Anchor != smartCropIdentifier {
|
if i.Imaging.Anchor != "" {
|
||||||
anchor, found := anchorPositions[i.Imaging.Anchor]
|
anchor, found := anchorPositions[i.Imaging.Anchor]
|
||||||
if !found {
|
if !found {
|
||||||
return i, nil, fmt.Errorf("invalid anchor value %q in imaging config", i.Anchor)
|
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
|
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 (
|
var (
|
||||||
c ImageConfig = GetDefaultImageConfig(action, defaults)
|
c ImageConfig = GetDefaultImageConfig(defaults)
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
action = strings.ToLower(action)
|
// Make to lower case, trim space and remove any empty strings.
|
||||||
|
n := 0
|
||||||
c.Action = action
|
for _, s := range options {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
if options == nil {
|
if s != "" {
|
||||||
return c, errors.New("image options cannot be empty")
|
options[n] = strings.ToLower(s)
|
||||||
|
n++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
options = options[:n]
|
||||||
|
|
||||||
for _, part := range options {
|
for _, part := range options {
|
||||||
part = strings.ToLower(part)
|
if _, ok := Actions[part]; ok {
|
||||||
|
c.Action = part
|
||||||
if part == smartCropIdentifier {
|
|
||||||
c.AnchorStr = smartCropIdentifier
|
|
||||||
} else if pos, ok := anchorPositions[part]; ok {
|
} else if pos, ok := anchorPositions[part]; ok {
|
||||||
c.Anchor = pos
|
c.Anchor = pos
|
||||||
c.AnchorStr = part
|
|
||||||
} else if filter, ok := imageFilters[part]; ok {
|
} else if filter, ok := imageFilters[part]; ok {
|
||||||
c.Filter = filter
|
c.Filter = filter
|
||||||
c.FilterStr = part
|
|
||||||
} else if hint, ok := hints[part]; ok {
|
} else if hint, ok := hints[part]; ok {
|
||||||
c.Hint = hint
|
c.Hint = hint
|
||||||
} else if part[0] == '#' {
|
} else if part[0] == '#' {
|
||||||
c.BgColorStr = part[1:]
|
c.BgColor, err = hexStringToColorGo(part[1:])
|
||||||
c.BgColor, err = hexStringToColorGo(c.BgColorStr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
@ -291,8 +298,7 @@ func DecodeImageConfig(action string, options []string, defaults *config.ConfigN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if action != "" && c.FilterStr == "" {
|
if c.Action != "" && c.Filter == nil {
|
||||||
c.FilterStr = defaults.Config.Imaging.ResampleFilter
|
|
||||||
c.Filter = defaults.Config.ResampleFilter
|
c.Filter = defaults.Config.ResampleFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,8 +306,7 @@ func DecodeImageConfig(action string, options []string, defaults *config.ConfigN
|
||||||
c.Hint = webpoptions.EncodingPresetPhoto
|
c.Hint = webpoptions.EncodingPresetPhoto
|
||||||
}
|
}
|
||||||
|
|
||||||
if action != "" && c.AnchorStr == "" {
|
if c.Action != "" && c.Anchor == -1 {
|
||||||
c.AnchorStr = defaults.Config.Imaging.Anchor
|
|
||||||
c.Anchor = defaults.Config.Anchor
|
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 c.BgColor == nil && c.TargetFormat != sourceFormat {
|
||||||
if sourceFormat.SupportsTransparency() && !c.TargetFormat.SupportsTransparency() {
|
if sourceFormat.SupportsTransparency() && !c.TargetFormat.SupportsTransparency() {
|
||||||
c.BgColor = defaults.Config.BgColor
|
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
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,8 +368,7 @@ type ImageConfig struct {
|
||||||
// not support transparency.
|
// not support transparency.
|
||||||
// When set per image operation, it's used even for formats that does support
|
// When set per image operation, it's used even for formats that does support
|
||||||
// transparency.
|
// transparency.
|
||||||
BgColor color.Color
|
BgColor color.Color
|
||||||
BgColorStr string
|
|
||||||
|
|
||||||
// Hint about what type of picture this is. Used to optimize encoding
|
// Hint about what type of picture this is. Used to optimize encoding
|
||||||
// when target is set to webp.
|
// when target is set to webp.
|
||||||
|
@ -360,57 +377,15 @@ type ImageConfig struct {
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
|
|
||||||
Filter gift.Resampling
|
Filter gift.Resampling
|
||||||
FilterStr string
|
|
||||||
|
|
||||||
Anchor gift.Anchor
|
Anchor gift.Anchor
|
||||||
AnchorStr string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i ImageConfig) GetKey(format Format) string {
|
func (cfg ImageConfig) Reanchor(a gift.Anchor) ImageConfig {
|
||||||
if i.Key != "" {
|
cfg.Anchor = a
|
||||||
return i.Action + "_" + i.Key
|
cfg.Key = hashing.HashStringHex(cfg.Key, "reanchor", a)
|
||||||
}
|
return cfg
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImagingConfigInternal struct {
|
type ImagingConfigInternal struct {
|
||||||
|
@ -429,7 +404,7 @@ func (i *ImagingConfigInternal) Compile(externalCfg *ImagingConfig) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if externalCfg.Anchor != "" && externalCfg.Anchor != smartCropIdentifier {
|
if externalCfg.Anchor != "" {
|
||||||
anchor, found := anchorPositions[externalCfg.Anchor]
|
anchor, found := anchorPositions[externalCfg.Anchor]
|
||||||
if !found {
|
if !found {
|
||||||
return fmt.Errorf("invalid anchor value %q in imaging config", i.Anchor)
|
return fmt.Errorf("invalid anchor value %q in imaging config", i.Anchor)
|
||||||
|
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
qt "github.com/frankban/quicktest"
|
qt "github.com/frankban/quicktest"
|
||||||
|
"github.com/gohugoio/hugo/common/hashing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDecodeConfig(t *testing.T) {
|
func TestDecodeConfig(t *testing.T) {
|
||||||
|
@ -106,7 +107,8 @@ func TestDecodeImageConfig(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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 b, ok := this.expect.(bool); ok && !b {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("[%d] parseImageConfig didn't return an expected error", i)
|
t.Errorf("[%d] parseImageConfig didn't return an expected error", i)
|
||||||
|
@ -115,15 +117,19 @@ func TestDecodeImageConfig(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("[%d] err: %s", i, err)
|
t.Fatalf("[%d] err: %s", i, err)
|
||||||
}
|
}
|
||||||
if fmt.Sprint(result) != fmt.Sprint(this.expect) {
|
expect := this.expect.(ImageConfig)
|
||||||
t.Fatalf("[%d] got\n%v\n but expected\n%v", i, result, this.expect)
|
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 {
|
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.TargetFormat = PNG
|
||||||
c.Hint = 2
|
c.Hint = 2
|
||||||
c.Width = width
|
c.Width = width
|
||||||
|
@ -131,26 +137,20 @@ func newImageConfig(action string, width, height, quality, rotate int, filter, a
|
||||||
c.Quality = quality
|
c.Quality = quality
|
||||||
c.qualitySetForImage = quality != 75
|
c.qualitySetForImage = quality != 75
|
||||||
c.Rotate = rotate
|
c.Rotate = rotate
|
||||||
c.BgColorStr = bgColor
|
|
||||||
c.BgColor, _ = hexStringToColorGo(bgColor)
|
c.BgColor, _ = hexStringToColorGo(bgColor)
|
||||||
|
c.Anchor = SmartCropAnchor
|
||||||
|
|
||||||
if filter != "" {
|
if filter != "" {
|
||||||
filter = strings.ToLower(filter)
|
filter = strings.ToLower(filter)
|
||||||
if v, ok := imageFilters[filter]; ok {
|
if v, ok := imageFilters[filter]; ok {
|
||||||
c.Filter = v
|
c.Filter = v
|
||||||
c.FilterStr = filter
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if anchor != "" {
|
if anchor != "" {
|
||||||
if anchor == smartCropIdentifier {
|
anchor = strings.ToLower(anchor)
|
||||||
c.AnchorStr = anchor
|
if v, ok := anchorPositions[anchor]; ok {
|
||||||
} else {
|
c.Anchor = v
|
||||||
anchor = strings.ToLower(anchor)
|
|
||||||
if v, ok := anchorPositions[anchor]; ok {
|
|
||||||
c.Anchor = v
|
|
||||||
c.AnchorStr = anchor
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,10 +36,11 @@ type Filters struct{}
|
||||||
|
|
||||||
// Process creates a filter that processes an image using the given specification.
|
// Process creates a filter that processes an image using the given specification.
|
||||||
func (*Filters) Process(spec any) gift.Filter {
|
func (*Filters) Process(spec any) gift.Filter {
|
||||||
|
specs := strings.ToLower(cast.ToString(spec))
|
||||||
return filter{
|
return filter{
|
||||||
Options: newFilterOpts(spec),
|
Options: newFilterOpts(specs),
|
||||||
Filter: processFilter{
|
Filter: processFilter{
|
||||||
spec: cast.ToString(spec),
|
spec: specs,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -217,7 +217,7 @@ func (p *ImageProcessor) FiltersFromConfig(src image.Image, conf ImageConfig) ([
|
||||||
case "resize":
|
case "resize":
|
||||||
filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter))
|
filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter))
|
||||||
case "crop":
|
case "crop":
|
||||||
if conf.AnchorStr == smartCropIdentifier {
|
if conf.Anchor == SmartCropAnchor {
|
||||||
bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter)
|
bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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))
|
filters = append(filters, gift.CropToSize(conf.Width, conf.Height, conf.Anchor))
|
||||||
}
|
}
|
||||||
case "fill":
|
case "fill":
|
||||||
if conf.AnchorStr == smartCropIdentifier {
|
if conf.Anchor == SmartCropAnchor {
|
||||||
bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter)
|
bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -329,12 +329,12 @@ func (p *ImageProcessor) doFilter(src image.Image, targetFormat Format, filters
|
||||||
return dst, nil
|
return dst, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDefaultImageConfig(action string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) ImageConfig {
|
func GetDefaultImageConfig(defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) ImageConfig {
|
||||||
if defaults == nil {
|
if defaults == nil {
|
||||||
defaults = defaultImageConfig
|
defaults = defaultImageConfig
|
||||||
}
|
}
|
||||||
return ImageConfig{
|
return ImageConfig{
|
||||||
Action: action,
|
Anchor: -1, // The real values start at 0.
|
||||||
Hint: defaults.Config.Hint,
|
Hint: defaults.Config.Hint,
|
||||||
Quality: defaults.Config.Imaging.Quality,
|
Quality: defaults.Config.Imaging.Quality,
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ package images_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/resources/images/imagetesting"
|
"github.com/gohugoio/hugo/resources/images/imagetesting"
|
||||||
|
@ -158,6 +159,76 @@ the last entry will win.
|
||||||
imagetesting.RunGolden(opts)
|
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) {
|
func TestImagesGoldenFiltersText(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
@ -263,3 +334,74 @@ Home.
|
||||||
|
|
||||||
imagetesting.RunGolden(opts)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -63,8 +63,18 @@ type GoldenImageTestOpts struct {
|
||||||
// Set to true to write golden files to disk.
|
// Set to true to write golden files to disk.
|
||||||
WriteFiles bool
|
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.
|
// Set to true to skip any assertions. Useful when adding new golden variants to a test.
|
||||||
DevMode bool
|
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:
|
// To rebuild all Golden image tests, toggle WriteFiles=true and run:
|
||||||
|
@ -78,7 +88,10 @@ var DefaultGoldenOpts = GoldenImageTestOpts{
|
||||||
func RunGolden(opts GoldenImageTestOpts) *hugolib.IntegrationTestBuilder {
|
func RunGolden(opts GoldenImageTestOpts) *hugolib.IntegrationTestBuilder {
|
||||||
opts.T.Helper()
|
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.")
|
c.AssertFileContent("public/index.html", "Home.")
|
||||||
|
|
||||||
outputDir := filepath.Join(c.H.Conf.WorkingDir(), "public", "images")
|
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))
|
goldenDir := filepath.Join(goldenBaseDir, filepath.FromSlash(opts.Name))
|
||||||
if opts.WriteFiles {
|
if opts.WriteFiles {
|
||||||
c.Assert(htesting.IsRealCI(), qt.IsFalse)
|
c.Assert(htesting.IsRealCI(), qt.IsFalse)
|
||||||
c.Assert(os.MkdirAll(goldenBaseDir, 0o777), qt.IsNil)
|
if !opts.Rebuild {
|
||||||
c.Assert(os.RemoveAll(goldenDir), qt.IsNil)
|
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)
|
c.Assert(hugio.CopyDir(hugofs.Os, outputDir, goldenDir, nil), qt.IsNil)
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.SkipAssertions {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
if opts.DevMode {
|
if opts.DevMode {
|
||||||
c.Assert(htesting.IsRealCI(), qt.IsFalse)
|
c.Assert(htesting.IsRealCI(), qt.IsFalse)
|
||||||
return c
|
return c
|
||||||
|
|
|
@ -25,10 +25,10 @@ import (
|
||||||
const (
|
const (
|
||||||
// Do not change.
|
// Do not change.
|
||||||
smartCropIdentifier = "smart"
|
smartCropIdentifier = "smart"
|
||||||
|
SmartCropAnchor = 1000
|
||||||
// This is just a increment, starting on 1. If Smart Crop improves its cropping, we
|
// 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.
|
// 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 {
|
func (p *ImageProcessor) newSmartCropAnalyzer(filter gift.Resampling) smartcrop.Analyzer {
|
||||||
|
|
BIN
resources/images/testdata/images_golden/filters/mask2/green.jpg
vendored
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
resources/images/testdata/images_golden/filters/mask2/pink.jpg
vendored
Normal file
After Width: | Height: | Size: 6 KiB |
BIN
resources/images/testdata/images_golden/methods/crop-sunsetjpg-200x200.jpg
vendored
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center-q20.jpg
vendored
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center-r90.jpg
vendored
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-center.jpg
vendored
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
resources/images/testdata/images_golden/methods/crop-sunsetjpg-350x400-smart.jpg
vendored
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
resources/images/testdata/images_golden/methods/fill-sunsetjpg-90x120-left.jpg
vendored
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
resources/images/testdata/images_golden/methods/fill-sunsetjpg-90x120-right.jpg
vendored
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
resources/images/testdata/images_golden/methods/fit-sunsetjpg-200x200.jpg
vendored
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
resources/images/testdata/images_golden/methods/resize-gopherpng-100x-03fc56-jpg.jpg
vendored
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
resources/images/testdata/images_golden/methods/resize-gopherpng-100x-fc03ec.png
vendored
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
resources/images/testdata/images_golden/methods/resize-gopherpng-100x.png
vendored
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
resources/images/testdata/images_golden/methods/resize-sunsetjpg-300x.jpg
vendored
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
resources/images/testdata/images_golden/methods/resize-sunsetjpg-x200.jpg
vendored
Normal file
After Width: | Height: | Size: 5.3 KiB |
|
@ -363,6 +363,7 @@ type genericResource struct {
|
||||||
sd ResourceSourceDescriptor
|
sd ResourceSourceDescriptor
|
||||||
paths internal.ResourcePaths
|
paths internal.ResourcePaths
|
||||||
|
|
||||||
|
includeHashInKey bool
|
||||||
sourceFilenameIsHash bool
|
sourceFilenameIsHash bool
|
||||||
|
|
||||||
h *resourceHash // A hash of the source content. Is only calculated in caching situations.
|
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() {
|
if l.spec.Cfg.IsMultihost() {
|
||||||
l.key = l.spec.Lang() + l.key
|
l.key = l.spec.Lang() + l.key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if l.includeHashInKey && !l.sourceFilenameIsHash {
|
||||||
|
l.key += fmt.Sprintf("_%d", l.hash())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return l.key
|
return l.key
|
||||||
|
|
|
@ -183,29 +183,33 @@ func (r *Spec) NewResource(rd ResourceSourceDescriptor) (resource.Resource, erro
|
||||||
TargetBasePaths: rd.TargetBasePaths,
|
TargetBasePaths: rd.TargetBasePaths,
|
||||||
}
|
}
|
||||||
|
|
||||||
gr := &genericResource{
|
isImage := rd.MediaType.MainType == "image"
|
||||||
Staler: &AtomicStaler{},
|
var imgFormat images.Format
|
||||||
h: &resourceHash{},
|
if isImage {
|
||||||
publishInit: &sync.Once{},
|
imgFormat, isImage = images.ImageFormatFromMediaSubType(rd.MediaType.SubType)
|
||||||
keyInit: &sync.Once{},
|
|
||||||
paths: rp,
|
|
||||||
spec: r,
|
|
||||||
sd: rd,
|
|
||||||
params: rd.Params,
|
|
||||||
name: rd.NameOriginal,
|
|
||||||
title: rd.Title,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if rd.MediaType.MainType == "image" {
|
gr := &genericResource{
|
||||||
imgFormat, ok := images.ImageFormatFromMediaSubType(rd.MediaType.SubType)
|
Staler: &AtomicStaler{},
|
||||||
if ok {
|
h: &resourceHash{},
|
||||||
ir := &imageResource{
|
publishInit: &sync.Once{},
|
||||||
Image: images.NewImage(imgFormat, r.imaging, nil, gr),
|
keyInit: &sync.Once{},
|
||||||
baseResource: gr,
|
includeHashInKey: isImage,
|
||||||
}
|
paths: rp,
|
||||||
ir.root = ir
|
spec: r,
|
||||||
return newResourceAdapter(gr.spec, rd.LazyPublish, ir), nil
|
sd: rd,
|
||||||
|
params: rd.Params,
|
||||||
|
name: rd.NameOriginal,
|
||||||
|
title: rd.Title,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,9 +62,9 @@ anigif: {{ $anigif.RelPermalink }}|{{ $anigif.Width }}|{{ $anigif.Height }}|{{ $
|
||||||
|
|
||||||
assertImages := func() {
|
assertImages := func() {
|
||||||
b.AssertFileContent("public/index.html", `
|
b.AssertFileContent("public/index.html", `
|
||||||
gif: /mybundle/pixel_hu14657638653019978294.gif|}|1|2|image/gif|
|
gif: /mybundle/pixel_hu_93429543fc146fce.gif|}|1|2|image/gif|
|
||||||
bmp: /mybundle/pixel_hu14705577916774115224.bmp|}|2|3|image/bmp|
|
bmp: /mybundle/pixel_hu_f9bf2acd6578e2c6.bmp|}|2|3|image/bmp|
|
||||||
anigif: /mybundle/giphy_hu3665406585348417395.gif|4|5|image/gif|
|
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 := hugolib.Test(t, files)
|
||||||
|
|
||||||
b.AssertFileContent("public/index.html",
|
b.AssertFileContent("public/index.html",
|
||||||
"jpg|RelPermalink: /images/pixel_hu13683954895608450100.jpg|MediaType: image/jpeg|Width: 1|Height: 1|",
|
"jpg|RelPermalink: /images/pixel_hu_38c3f257174fc757.jpg|MediaType: image/jpeg|Width: 1|Height: 1|",
|
||||||
"resize 1|RelPermalink: /images/pixel_hu3453403302435331853.jpg|MediaType: image/jpeg|Width: 20|Height: 30|",
|
"resize 1|RelPermalink: /images/pixel_hu_b5c2a3d88991f65a.jpg|MediaType: image/jpeg|Width: 20|Height: 30|",
|
||||||
"resize 2|RelPermalink: /images/pixel_hu3453403302435331853.jpg|MediaType: image/jpeg|Width: 20|Height: 30|",
|
"resize 2|RelPermalink: /images/pixel_hu_b5c2a3d88991f65a.jpg|MediaType: image/jpeg|Width: 20|Height: 30|",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
BIN
resources/testdata/mask2.png
vendored
Normal file
After Width: | Height: | Size: 36 KiB |
|
@ -386,22 +386,15 @@ func TestTransform(t *testing.T) {
|
||||||
resizedPublished1, err := img.Resize("40x40")
|
resizedPublished1, err := img.Resize("40x40")
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
c.Assert(resizedPublished1.Height(), qt.Equals, 40)
|
c.Assert(resizedPublished1.Height(), qt.Equals, 40)
|
||||||
c.Assert(resizedPublished1.RelPermalink(), qt.Equals, "/gopher.changed_hu16988682630555427117.png")
|
c.Assert(resizedPublished1.RelPermalink(), qt.Equals, "/gopher.changed_hu_85920388a7ff96fa.png")
|
||||||
assertShouldExist(c, spec, "public/gopher.changed_hu16988682630555427117.png", true)
|
assertShouldExist(c, spec, "public/gopher.changed_hu_85920388a7ff96fa.png", true)
|
||||||
|
|
||||||
// Permalink called.
|
// Permalink called.
|
||||||
resizedPublished2, err := img.Resize("30x30")
|
resizedPublished2, err := img.Resize("30x30")
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
c.Assert(resizedPublished2.Height(), qt.Equals, 30)
|
c.Assert(resizedPublished2.Height(), qt.Equals, 30)
|
||||||
c.Assert(resizedPublished2.Permalink(), qt.Equals, "https://example.com/gopher.changed_hu14141325020798305104.png")
|
c.Assert(resizedPublished2.Permalink(), qt.Equals, "https://example.com/gopher.changed_hu_c8d8163c08643a7f.png")
|
||||||
assertShouldExist(c, spec, "public/gopher.changed_hu14141325020798305104.png", true)
|
assertShouldExist(c, spec, "public/gopher.changed_hu_c8d8163c08643a7f.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)
|
|
||||||
|
|
||||||
assertNoDuplicateWrites(c, spec)
|
assertNoDuplicateWrites(c, spec)
|
||||||
})
|
})
|
||||||
|
|
|
@ -60,7 +60,7 @@ Copy3: {{ $copy3.RelPermalink}}|{{ $copy3.MediaType }}|{{ $copy3.Content | safeJ
|
||||||
|
|
||||||
b.AssertFileContent("public/index.html", `
|
b.AssertFileContent("public/index.html", `
|
||||||
Image Orig: /blog/images/pixel.png|image/png|1|1|
|
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 Copy2: /blog/images/copy2.png|image/png|3|4
|
||||||
Image Copy3: image/png|3|4|
|
Image Copy3: image/png|3|4|
|
||||||
Orig: /blog/js/foo.js|text/javascript|let foo;|
|
Orig: /blog/js/foo.js|text/javascript|let foo;|
|
||||||
|
|