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
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
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.0 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 |