images: Deprecate Imaging.Compression and move it down to webp and avif configs

Also clean up and simplify the image config handling.

Closes #14998
This commit is contained in:
Bjørn Erik Pedersen
2026-06-05 19:04:12 +02:00
parent 98ad9b3c03
commit cf18b827e2
5 changed files with 255 additions and 347 deletions
+7 -1
View File
@@ -90,7 +90,13 @@ var allDecoderSetups = map[string]decodeWeight{
decode: func(d decodeWeight, p decodeConfig) error {
m := p.p.GetStringMap(d.key)
if _, found := m["quality"]; found {
hugo.DeprecateWithLogger("project config key imaging.quality", "Set the quality per format instead with imaging.jpeg.quality, imaging.webp.quality and/or imaging.avif.quality.", "v0.164.0", p.logger.Logger())
hugo.DeprecateWithLogger("project config key imaging.quality", "Set the quality per format instead with imaging.jpeg.quality, imaging.webp.quality and/or imaging.avif.quality.", "v0.163.0", p.logger.Logger())
}
if _, found := m["compression"]; found {
hugo.DeprecateWithLogger("project config key imaging.compression", "Set the compression type per format instead with imaging.webp.compression and/or imaging.avif.compression.", "v0.163.0", p.logger.Logger())
}
if _, found := m["hint"]; found {
hugo.DeprecateWithLogger("project config key imaging.hint", "Set the hint per format instead with imaging.webp.hint and/or imaging.avif.hint.", "v0.163.0", p.logger.Logger())
}
var err error
p.c.Imaging, err = images.DecodeConfig(m)
+199 -160
View File
@@ -169,8 +169,6 @@ func ImageFormatFromMediaSubType(sub string) (Format, ImageResourceType) {
}
const (
defaultJPEGQuality = 75
defaultAVIFQuality = 60
defaultResampleFilter = "box"
defaultBgColor = "#ffffff"
defaultHint = "photo"
@@ -180,32 +178,6 @@ const (
defaultAvifEncoderSpeed = 10
)
var (
defaultImaging = map[string]any{
"resampleFilter": defaultResampleFilter,
"bgColor": defaultBgColor,
"hint": defaultHint,
"compression": defaultCompression,
"webp": map[string]any{
"useSharpYuv": defaultWebpUseSharpYuv,
"method": defaultWebpMethod,
},
"avif": map[string]any{
"encoderSpeed": defaultAvifEncoderSpeed,
},
}
defaultImageConfig *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]
)
func init() {
var err error
defaultImageConfig, err = DecodeConfig(defaultImaging)
if err != nil {
panic(err)
}
}
func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, ImagingConfigInternal], error) {
if in == nil {
in = make(map[string]any)
@@ -216,25 +188,24 @@ func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, Ima
if err != nil {
return ImagingConfigInternal{}, nil, err
}
_, qualityExplicit := m["quality"]
// Merge in the defaults.
hmaps.MergeShallow(m, defaultImaging)
// Deep merge webp defaults.
if webp, ok := m["webp"].(map[string]any); ok {
hmaps.MergeShallow(webp, defaultImaging["webp"].(map[string]any))
i := ImagingConfigInternal{
Imaging: ImagingConfig{
BgColor: defaultBgColor,
ResampleFilter: defaultResampleFilter,
Webp: WebpConfig{
UseSharpYuv: defaultWebpUseSharpYuv,
Method: defaultWebpMethod,
},
Avif: AvifConfig{
EncoderSpeed: defaultAvifEncoderSpeed,
},
},
}
// Deep merge avif defaults.
if avif, ok := m["avif"].(map[string]any); ok {
hmaps.MergeShallow(avif, defaultImaging["avif"].(map[string]any))
}
var i ImagingConfigInternal
if err := mapstructure.Decode(m, &i.Imaging); err != nil {
return i, nil, err
}
i.Imaging.qualityExplicit = qualityExplicit
if err := i.Imaging.init(); err != nil {
return i, nil, err
@@ -272,9 +243,8 @@ func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, Ima
func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal], sourceFormat Format) (ImageConfig, error) {
var (
c ImageConfig = GetDefaultImageConfig(defaults)
err error
qualitySet bool
c ImageConfig = newImageConfig()
err error
)
// Make to lower case, trim space and remove any empty strings.
@@ -314,7 +284,6 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin
if c.Quality < 1 || c.Quality > 100 {
return c, errors.New("quality ranges from 1 to 100 inclusive")
}
qualitySet = true
} else if part[0] == 'r' {
c.Rotate, err = strconv.Atoi(part[1:])
if err != nil {
@@ -361,37 +330,8 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin
}
}
if c.Action != "" && c.Filter == nil {
c.Filter = defaults.Config.ResampleFilter
}
if c.Action != "" && c.Anchor == -1 {
c.Anchor = defaults.Config.Anchor
}
// default to the source format
if c.TargetFormat == 0 {
c.TargetFormat = sourceFormat
}
if c.Hint == "" {
c.Hint = defaults.Config.Imaging.hintFor(c.TargetFormat)
}
if !qualitySet && c.TargetFormat.RequiresDefaultQuality() {
// Apply the per-format (or global) default quality unless the user
// explicitly set a quality for this image operation.
c.Quality = defaults.Config.Imaging.qualityFor(c.TargetFormat)
}
if c.Compression == "" {
c.Compression = defaults.Config.Imaging.Compression
}
if c.BgColor == nil && c.TargetFormat != sourceFormat {
if sourceFormat.SupportsTransparency() && !c.TargetFormat.SupportsTransparency() {
c.BgColor = defaults.Config.BgColor
}
if err := c.init(defaults.Config, sourceFormat); err != nil {
return c, err
}
if mainImageVersionNumber > 0 {
@@ -417,6 +357,7 @@ type ImageConfig struct {
// This defines the output format of the output image. It defaults to the source format.
TargetFormat Format
// Optional image processing action to perform ("resize", "crop", "fit", "fill"). When empty, the operation may still perform e.g. format conversion.
Action string
// If set, this will be used as the key in filenames etc.
@@ -439,6 +380,9 @@ type ImageConfig struct {
// transparency.
BgColor color.Color
Filter gift.Resampling
Anchor gift.Anchor
// Hint about what type of picture this is. Used to optimize encoding
// when target is webp (preset) or avif (chroma subsampling).
Hint string
@@ -454,16 +398,54 @@ type ImageConfig struct {
Width int
Height int
Filter gift.Resampling
Anchor gift.Anchor
}
func (cfg ImageConfig) Reanchor(a gift.Anchor) ImageConfig {
cfg.Anchor = a
cfg.Key = hashing.HashStringHex(cfg.Key, "reanchor", a)
return cfg
func (c *ImageConfig) init(defaults ImagingConfigInternal, sourceFormat Format) error {
if c.Action != "" && c.Anchor == -1 {
c.Anchor = defaults.Anchor
}
if c.TargetFormat == 0 {
c.TargetFormat = sourceFormat
}
if c.Filter == nil {
c.Filter = defaults.ResampleFilter
}
if c.Anchor == -1 {
c.Anchor = defaults.Anchor
}
if c.Hint == "" {
c.Hint = defaults.Imaging.hintFor(c.TargetFormat)
}
if c.Quality == 0 {
c.Quality = defaults.Imaging.qualityFor(c.TargetFormat)
}
if c.Compression == "" {
c.Compression = defaults.Imaging.compressionFor(c.TargetFormat)
}
if c.TargetFormat == AVIF {
c.EncoderSpeed = defaults.Imaging.Avif.EncoderSpeed
}
if c.TargetFormat == WEBP {
c.Method = defaults.Imaging.Webp.Method
c.UseSharpYuv = defaults.Imaging.Webp.UseSharpYuv
}
if c.BgColor == nil && c.TargetFormat != sourceFormat {
if sourceFormat.SupportsTransparency() && !c.TargetFormat.SupportsTransparency() {
c.BgColor = defaults.BgColor
}
}
return nil
}
func (c ImageConfig) Reanchor(a gift.Anchor) ImageConfig {
c.Anchor = a
c.Key = hashing.HashStringHex(c.Key, "reanchor", a)
return c
}
type ImagingConfigInternal struct {
@@ -504,18 +486,15 @@ type ImagingConfig struct {
// Default image quality setting (1-100). Used as the fallback for JPEG,
// WebP and AVIF when no per-format quality is set. When left unset, JPEG
// and WebP default to 75 and AVIF to 60 (its scale differs perceptually).
// Deprecated in v0.164.0: set the quality per format instead, see
// Deprecated in v0.163.0: set the quality per format instead, see
// imaging.jpeg.quality, imaging.webp.quality and imaging.avif.quality.
Quality int
// Whether Quality was explicitly set in the config. When false, AVIF uses
// its own lower default instead of the global Quality.
qualityExplicit bool
Quality int `json:"-"`
// Compression method to use.
// One of "lossy" or "lossless".
// Note that lossless is currently only supported for WebP and AVIF.
Compression string
// Deprecated in v0.163.0: set the compression method per format instead, see imaging.webp.compression and imaging.avif.compression.
Compression string `json:"-"`
// Resample filter to use in resize operations.
ResampleFilter string
@@ -540,33 +519,37 @@ type ImagingConfig struct {
Avif AvifConfig
}
// qualityFor returns the configured quality for the given target format,
// falling back to the global Quality when no per-format quality is set.
func (cfg *ImagingConfig) qualityFor(f Format) int {
var q int
switch f {
case JPEG:
q = cfg.Jpeg.Quality
return cfg.Jpeg.Quality
case WEBP:
q = cfg.Webp.Quality
return cfg.Webp.Quality
case AVIF:
q = cfg.Avif.Quality
return cfg.Avif.Quality
}
if q > 0 {
return q
return 0
}
func (cfg *ImagingConfig) compressionFor(f Format) string {
switch f {
case WEBP:
return cfg.Webp.Compression
case AVIF:
return cfg.Avif.Compression
}
if f == AVIF && !cfg.qualityExplicit {
return defaultAVIFQuality
}
return cfg.Quality
return ""
}
// hintFor returns the configured hint for the given target format.
func (cfg *ImagingConfig) hintFor(f Format) string {
if f == AVIF {
switch f {
case WEBP:
return cfg.Webp.Hint
case AVIF:
return cfg.Avif.Hint
}
return cfg.Webp.Hint
return ""
}
var validMetaSources = map[string]bool{
@@ -576,18 +559,23 @@ var validMetaSources = map[string]bool{
}
func (cfg *ImagingConfig) init() error {
if cfg.Quality == 0 && !cfg.qualityExplicit {
cfg.Quality = defaultJPEGQuality
}
if cfg.Quality < 1 || cfg.Quality > 100 {
return errors.New("image quality must be a number between 1 and 100")
}
cfg.BgColor = strings.ToLower(strings.TrimPrefix(cfg.BgColor, "#"))
cfg.Anchor = strings.ToLower(cfg.Anchor)
cfg.ResampleFilter = strings.ToLower(cfg.ResampleFilter)
cfg.Hint = strings.ToLower(cfg.Hint)
cfg.Compression = strings.ToLower(cfg.Compression)
if err := cfg.Jpeg.init(cfg); err != nil {
return fmt.Errorf("invalid jpeg config: %w", err)
}
if err := cfg.Webp.init(cfg); err != nil {
return fmt.Errorf("invalid webp config: %w", err)
}
if err := cfg.Avif.init(cfg); err != nil {
return fmt.Errorf("invalid avif config: %w", err)
}
if cfg.Quality < 0 || cfg.Quality > 100 {
return fmt.Errorf("imaging.quality must be between 1 and 100 inclusive, got %d", cfg.Quality)
}
if cfg.Anchor == "" {
cfg.Anchor = smartCropIdentifier
@@ -621,53 +609,6 @@ func (cfg *ImagingConfig) init() error {
}
}
// WebP config with backwards compatibility for root-level Hint.
cfg.Webp.Hint = strings.ToLower(cfg.Webp.Hint)
if cfg.Webp.Hint == "" {
// Fall back to root-level hint for backwards compatibility.
if cfg.Hint != "" {
cfg.Webp.Hint = cfg.Hint
} else {
cfg.Webp.Hint = defaultHint
}
}
if cfg.Webp.Hint != "" && !hints[cfg.Webp.Hint] {
return fmt.Errorf("invalid webp hint %q; must be one of picture, photo, drawing, icon, or text", cfg.Webp.Hint)
}
if cfg.Webp.Method == 0 {
cfg.Webp.Method = defaultWebpMethod
}
if cfg.Webp.Method < 0 || cfg.Webp.Method > 6 {
return fmt.Errorf("webp method must be between 0 and 6, got %d", cfg.Webp.Method)
}
for name, q := range map[string]int{"jpeg": cfg.Jpeg.Quality, "webp": cfg.Webp.Quality, "avif": cfg.Avif.Quality} {
if q != 0 && (q < 1 || q > 100) {
return fmt.Errorf("%s quality must be a number between 1 and 100", name)
}
}
// AVIF config with backwards compatibility for root-level Hint.
cfg.Avif.Hint = strings.ToLower(cfg.Avif.Hint)
if cfg.Avif.Hint == "" {
// Fall back to root-level hint for backwards compatibility.
if cfg.Hint != "" {
cfg.Avif.Hint = cfg.Hint
} else {
cfg.Avif.Hint = defaultHint
}
}
if !hints[cfg.Avif.Hint] {
return fmt.Errorf("invalid avif hint %q; must be one of picture, photo, drawing, icon, or text", cfg.Avif.Hint)
}
if cfg.Avif.EncoderSpeed == 0 {
cfg.Avif.EncoderSpeed = defaultAvifEncoderSpeed
}
if cfg.Avif.EncoderSpeed < 1 || cfg.Avif.EncoderSpeed > 10 {
return fmt.Errorf("avif encoderSpeed must be between 1 and 10, got %d", cfg.Avif.EncoderSpeed)
}
return nil
}
@@ -713,11 +654,28 @@ type JpegConfig struct {
Quality int
}
func (c *JpegConfig) init(ic *ImagingConfig) error {
if c.Quality == 0 {
c.Quality = ic.Quality
}
if c.Quality == 0 {
c.Quality = 75
}
if c.Quality < 1 || c.Quality > 100 {
return fmt.Errorf("imaging.jpeg.quality must be between 1 and 100 inclusive, got %d", c.Quality)
}
return nil
}
// AvifConfig holds AVIF-specific encoding configuration.
type AvifConfig struct {
// Quality setting (1-100). Falls back to the global imaging.quality if unset.
Quality int
// Compression method to use.
// One of "lossy" or "lossless".
Compression string
// Hint about what type of image this is. Used for chroma subsampling.
// Valid values are "picture", "photo", "drawing", "icon", or "text".
// Default is "photo".
@@ -730,16 +688,59 @@ type AvifConfig struct {
// build time.
// We recommend sticking with the default of 10 unless you have a specific reason to change it,
// and to stay above 5 to avoid very long build times and timeouts.
// 0 is treated as unset and falls back to the default.
EncoderSpeed int
}
func (c *AvifConfig) init(ic *ImagingConfig) error {
if c.Hint == "" {
c.Hint = ic.Hint
}
if c.Compression == "" {
c.Compression = ic.Compression
}
if c.Quality == 0 {
c.Quality = ic.Quality
}
if c.Hint == "" {
c.Hint = defaultHint
}
if c.Compression == "" {
c.Compression = defaultCompression
}
if c.Quality == 0 {
c.Quality = 60
}
c.Hint = strings.ToLower(c.Hint)
c.Compression = strings.ToLower(c.Compression)
if c.Hint != "" && !hints[c.Hint] {
return fmt.Errorf("imaging.avif.hint must be one of picture, photo, drawing, icon, or text, got %q", c.Hint)
}
if c.EncoderSpeed < 1 || c.EncoderSpeed > 10 {
return fmt.Errorf("imaging.avif.encoderSpeed must be between 1 and 10, got %d", c.EncoderSpeed)
}
if c.Compression != "" && !compressionMethods[c.Compression] {
return fmt.Errorf("imaging.avif.compression must be one of lossy or lossless, got %q", c.Compression)
}
if c.Quality < 1 || c.Quality > 100 {
return fmt.Errorf("imaging.avif.quality must be between 1 and 100 inclusive, got %d", c.Quality)
}
return nil
}
// WebpConfig holds WebP-specific encoding configuration.
type WebpConfig struct {
// Quality setting (1-100). Falls back to the global imaging.quality if unset.
// Only relevant for lossy encoding.
Quality int
// Compression method to use.
// One of "lossy" or "lossless".
Compression string
// Hint about what type of image this is.
// Valid values are "picture", "photo", "drawing", "icon", or "text".
// Default is "photo".
@@ -753,3 +754,41 @@ type WebpConfig struct {
// Default is 2.
Method int
}
func (c *WebpConfig) init(ic *ImagingConfig) error {
if c.Hint == "" {
c.Hint = ic.Hint
}
if c.Compression == "" {
c.Compression = ic.Compression
}
if c.Quality == 0 {
c.Quality = ic.Quality
}
if c.Hint == "" {
c.Hint = defaultHint
}
if c.Compression == "" {
c.Compression = defaultCompression
}
if c.Quality == 0 {
c.Quality = 75
}
c.Hint = strings.ToLower(c.Hint)
c.Compression = strings.ToLower(c.Compression)
if c.Hint != "" && !hints[c.Hint] {
return fmt.Errorf("imaging.webp.hint must be one of picture, photo, drawing, icon, or text, got %q", c.Hint)
}
if c.Method < 0 || c.Method > 6 {
return fmt.Errorf("imaging.webp.method must be between 0 and 6, got %d", c.Method)
}
if c.Compression != "" && !compressionMethods[c.Compression] {
return fmt.Errorf("imaging.webp.compression must be one of lossy or lossless, got %q", c.Compression)
}
if c.Quality < 1 || c.Quality > 100 {
return fmt.Errorf("imaging.webp.quality must be between 1 and 100 inclusive, got %d", c.Quality)
}
return nil
}
+40 -153
View File
@@ -18,8 +18,6 @@ import (
"strings"
"testing"
"github.com/gohugoio/hugo/config"
qt "github.com/frankban/quicktest"
)
@@ -97,13 +95,6 @@ func TestDecodeConfig(t *testing.T) {
})
c.Assert(err, qt.ErrorMatches, ".*encoderSpeed must be between.*")
// AVIF: minimum is 1; 0 is treated as unset and falls back to the default.
imagingConfig, err = DecodeConfig(map[string]any{
"avif": map[string]any{"encoderSpeed": 0},
})
c.Assert(err, qt.IsNil)
c.Assert(imagingConfig.Config.Imaging.Avif.EncoderSpeed, qt.Equals, defaultAvifEncoderSpeed)
imagingConfig, err = DecodeConfig(map[string]any{
"avif": map[string]any{"encoderSpeed": 1},
})
@@ -111,163 +102,58 @@ func TestDecodeConfig(t *testing.T) {
c.Assert(imagingConfig.Config.Imaging.Avif.EncoderSpeed, qt.Equals, 1)
}
// See issue 14992.
func TestImageConfigHintPerFormat(t *testing.T) {
c := qt.New(t)
// Default: both WebP and AVIF hint default to "photo".
cfg, err := DecodeConfig(map[string]any{})
c.Assert(err, qt.IsNil)
c.Assert(cfg.Config.Imaging.Webp.Hint, qt.Equals, "photo")
c.Assert(cfg.Config.Imaging.Avif.Hint, qt.Equals, "photo")
// Per-format hint from config.
cfg, err = DecodeConfig(map[string]any{
"webp": map[string]any{"hint": "drawing"},
"avif": map[string]any{"hint": "icon"},
})
c.Assert(err, qt.IsNil)
c.Assert(cfg.Config.Imaging.Webp.Hint, qt.Equals, "drawing")
c.Assert(cfg.Config.Imaging.Avif.Hint, qt.Equals, "icon")
// Root-level hint applies to both formats for backwards compatibility.
cfg, err = DecodeConfig(map[string]any{"hint": "text"})
c.Assert(err, qt.IsNil)
c.Assert(cfg.Config.Imaging.Webp.Hint, qt.Equals, "text")
c.Assert(cfg.Config.Imaging.Avif.Hint, qt.Equals, "text")
// Invalid AVIF hint is rejected.
_, err = DecodeConfig(map[string]any{"avif": map[string]any{"hint": "nope"}})
c.Assert(err, qt.ErrorMatches, ".*invalid avif hint.*")
// Per-image config picks the hint for the target format.
cfg, err = DecodeConfig(map[string]any{
"webp": map[string]any{"hint": "drawing"},
"avif": map[string]any{"hint": "icon"},
})
c.Assert(err, qt.IsNil)
hint := func(f Format, opts ...string) string {
conf, err := DecodeImageConfig(append([]string{"resize", "100x"}, opts...), cfg, f)
c.Assert(err, qt.IsNil)
return conf.Hint
}
c.Assert(hint(WEBP), qt.Equals, "drawing")
c.Assert(hint(AVIF), qt.Equals, "icon")
// A per-image hint always wins.
c.Assert(hint(AVIF, "photo"), qt.Equals, "photo")
}
// See issue 14957.
func TestImageConfigQualityPerFormat(t *testing.T) {
func TestImageConfigPerFormat(t *testing.T) {
c := qt.New(t)
cfg, err := DecodeConfig(map[string]any{
"quality": 80,
"webp": map[string]any{"quality": 70},
"avif": map[string]any{"quality": 55},
"quality": 80,
"compression": "lossy",
"hint": "text",
"jpeg": map[string]any{"quality": 80},
"webp": map[string]any{"quality": 70, "hint": "picture", "compression": "lossless"},
"avif": map[string]any{"quality": 55},
})
c.Assert(err, qt.IsNil)
quality := func(opts ...string) int {
conf := func(opts ...string) ImageConfig {
conf, err := DecodeImageConfig(append([]string{"resize", "100x"}, opts...), cfg, JPEG)
c.Assert(err, qt.IsNil)
return conf.Quality
return conf
}
// Per-format quality from config.
c.Assert(quality("webp"), qt.Equals, 70)
c.Assert(quality("avif"), qt.Equals, 55)
// JPEG has no per-format override, so it falls back to the global quality.
c.Assert(quality("jpg"), qt.Equals, 80)
c.Assert(conf("jpg").Quality, qt.Equals, 80)
c.Assert(conf("avif").Quality, qt.Equals, 55)
c.Assert(conf("webp").Quality, qt.Equals, 70)
// A per-image quality always wins.
c.Assert(quality("webp", "q33"), qt.Equals, 33)
c.Assert(quality("avif", "q33"), qt.Equals, 33)
c.Assert(conf("webp", "q33").Quality, qt.Equals, 33)
c.Assert(conf("avif", "q33").Quality, qt.Equals, 33)
// Without per-format config, all formats fall back to the global quality.
cfg2, err := DecodeConfig(map[string]any{"quality": 80})
c.Assert(err, qt.IsNil)
for _, f := range []string{"jpg", "webp", "avif"} {
conf, err := DecodeImageConfig([]string{"resize", "100x", f}, cfg2, JPEG)
c.Assert(err, qt.IsNil)
c.Assert(conf.Quality, qt.Equals, 80)
}
c.Assert(conf("webp").Hint, qt.Equals, "picture")
c.Assert(conf("avif").Hint, qt.Equals, "text")
c.Assert(conf("jpeg").Hint, qt.Equals, "")
// Out-of-range per-format quality is rejected.
_, err = DecodeConfig(map[string]any{"avif": map[string]any{"quality": 123}})
c.Assert(err, qt.ErrorMatches, ".*quality must be.*")
}
// See issue 14979.
func TestImageConfigAvifDefaultQuality(t *testing.T) {
c := qt.New(t)
quality := func(cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal], opts ...string) int {
conf, err := DecodeImageConfig(append([]string{"resize", "100x"}, opts...), cfg, JPEG)
c.Assert(err, qt.IsNil)
return conf.Quality
}
// With nothing configured, AVIF defaults to 60 while JPEG/WebP stay at 75.
cfg, err := DecodeConfig(map[string]any{})
c.Assert(err, qt.IsNil)
c.Assert(quality(cfg, "avif"), qt.Equals, 60)
c.Assert(quality(cfg, "jpg"), qt.Equals, 75)
c.Assert(quality(cfg, "webp"), qt.Equals, 75)
// An explicit global quality applies to AVIF too.
cfg, err = DecodeConfig(map[string]any{"quality": 80})
c.Assert(err, qt.IsNil)
c.Assert(quality(cfg, "avif"), qt.Equals, 80)
// An explicit per-format AVIF quality wins over the default.
cfg, err = DecodeConfig(map[string]any{"avif": map[string]any{"quality": 90}})
c.Assert(err, qt.IsNil)
c.Assert(quality(cfg, "avif"), qt.Equals, 90)
// A per-image quality still wins.
c.Assert(quality(cfg, "avif", "q33"), qt.Equals, 33)
}
// See issue 14990.
func TestImageConfigFormatVersionNumber(t *testing.T) {
c := qt.New(t)
cfg, err := DecodeConfig(map[string]any{})
c.Assert(err, qt.IsNil)
key := func(f string) string {
conf, err := DecodeImageConfig([]string{"resize", "100x", f}, cfg, JPEG)
c.Assert(err, qt.IsNil)
return conf.Key
}
avifBefore, jpgBefore := key("avif"), key("jpg")
orig := formatVersionNumbers[AVIF]
defer func() { formatVersionNumbers[AVIF] = orig }()
formatVersionNumbers[AVIF] = orig + 1
// Bumping the AVIF version invalidates AVIF images only.
c.Assert(key("avif"), qt.Not(qt.Equals), avifBefore)
c.Assert(key("jpg"), qt.Equals, jpgBefore)
c.Assert(conf("webp").Compression, qt.Equals, "lossless")
c.Assert(conf("avif").Compression, qt.Equals, "lossy")
c.Assert(conf("jpeg").Compression, qt.Equals, "")
}
func TestDecodeImageConfig(t *testing.T) {
c := qt.New(t)
for i, this := range []struct {
action string
in string
expect any
}{
{"resize", "300x400", newImageConfig("resize", 300, 400, 75, 0, "box", "smart", "")},
{"resize", "300x400 #fff", newImageConfig("resize", 300, 400, 75, 0, "box", "smart", "fff")},
{"resize", "100x200 bottomRight", newImageConfig("resize", 100, 200, 75, 0, "box", "BottomRight", "")},
{"resize", "10x20 topleft Lanczos", newImageConfig("resize", 10, 20, 75, 0, "Lanczos", "topleft", "")},
{"resize", "linear left 10x r180", newImageConfig("resize", 10, 0, 75, 180, "linear", "left", "")},
{"resize", "x20 riGht Cosine q95", newImageConfig("resize", 0, 20, 95, 0, "cosine", "right", "")},
{"crop", "300x400", newImageConfig("crop", 300, 400, 75, 0, "box", "smart", "")},
{"fill", "300x400", newImageConfig("fill", 300, 400, 75, 0, "box", "smart", "")},
{"fit", "300x400", newImageConfig("fit", 300, 400, 75, 0, "box", "smart", "")},
{"resize", "300x400", newTestImageConfig("resize", 300, 400, 75, 0, "box", "smart", "")},
{"resize", "300x400 #fff", newTestImageConfig("resize", 300, 400, 75, 0, "box", "smart", "fff")},
{"resize", "100x200 bottomRight", newTestImageConfig("resize", 100, 200, 75, 0, "box", "BottomRight", "")},
{"resize", "10x20 topleft Lanczos", newTestImageConfig("resize", 10, 20, 75, 0, "Lanczos", "topleft", "")},
{"resize", "linear left 10x r180", newTestImageConfig("resize", 10, 0, 75, 180, "linear", "left", "")},
{"resize", "x20 riGht Cosine q95", newTestImageConfig("resize", 0, 20, 95, 0, "cosine", "right", "")},
{"crop", "300x400", newTestImageConfig("crop", 300, 400, 75, 0, "box", "smart", "")},
{"fill", "300x400", newTestImageConfig("fill", 300, 400, 75, 0, "box", "smart", "")},
{"fit", "300x400", newTestImageConfig("fit", 300, 400, 75, 0, "box", "smart", "")},
{"resize", "", false},
{"resize", "foo", false},
@@ -282,7 +168,7 @@ func TestDecodeImageConfig(t *testing.T) {
t.Fatal(err)
}
options := append([]string{this.action}, strings.Fields(this.in)...)
result, err := DecodeImageConfig(options, cfg, PNG)
result, err := DecodeImageConfig(options, cfg, WEBP)
if b, ok := this.expect.(bool); ok && !b {
if err == nil {
t.Errorf("[%d] parseImageConfig didn't return an expected error", i)
@@ -294,24 +180,25 @@ func TestDecodeImageConfig(t *testing.T) {
expect := this.expect.(ImageConfig)
result.Key = ""
if fmt.Sprint(result) != fmt.Sprint(expect) {
t.Fatalf("[%d] got\n%v\n but expected\n%v", i, result, expect)
}
c.Assert(fmt.Sprint(result), qt.Equals, fmt.Sprint(expect))
}
}
}
func newImageConfig(action string, width, height, quality, rotate int, filter, anchor, bgColor string) ImageConfig {
var c ImageConfig = GetDefaultImageConfig(nil)
func newTestImageConfig(action string, width, height, quality, rotate int, filter, anchor, bgColor string) ImageConfig {
var c ImageConfig = newImageConfig()
c.Action = action
c.TargetFormat = PNG
c.Hint = defaultHint // Resolved per target format in DecodeImageConfig.
c.TargetFormat = WEBP
c.Hint = defaultHint
c.Compression = defaultCompression
c.Width = width
c.Height = height
c.Quality = quality
c.Rotate = rotate
c.BgColor, _ = hexStringToColorGo(bgColor)
c.Anchor = SmartCropAnchor
c.Method = defaultWebpMethod
if filter != "" {
filter = strings.ToLower(filter)
+1 -16
View File
@@ -342,18 +342,9 @@ func (p *ImageProcessor) doFilter(src image.Image, targetFormat Format, filters
return dst, nil
}
func GetDefaultImageConfig(defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) ImageConfig {
if defaults == nil {
defaults = defaultImageConfig
}
func newImageConfig() ImageConfig {
return ImageConfig{
Anchor: -1, // The real values start at 0.
// Hint is resolved per target format in DecodeImageConfig.
Quality: defaults.Config.Imaging.Quality,
Compression: defaults.Config.Imaging.Compression,
UseSharpYuv: defaults.Config.Imaging.Webp.UseSharpYuv,
Method: defaults.Config.Imaging.Webp.Method,
EncoderSpeed: defaults.Config.Imaging.Avif.EncoderSpeed,
}
}
@@ -405,12 +396,6 @@ func (f Format) ToImageMetaImageFormatFormat() imagemeta.ImageFormat {
}
}
// RequiresDefaultQuality returns if the default quality needs to be applied to
// images of this format.
func (f Format) RequiresDefaultQuality() bool {
return f == JPEG || f == WEBP || f == AVIF
}
// SupportsTransparency reports whether it supports transparency in any form.
func (f Format) SupportsTransparency() bool {
return f != JPEG
+8 -17
View File
@@ -105,15 +105,15 @@ CropSmart: {{ .Width }}x{{ .Height }}|
b.AssertFileContent("public/index.html", "Original: 900x562|CropTopLeft: 900x561|CropSmart: 900x561|")
}
// The global imaging.quality was deprecated in v0.164.0 in favour of the
// per-format quality settings. See issue 14979.
func TestImagingQualityDeprecated(t *testing.T) {
func TestImagingGlobalsDeprecated(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
[imaging]
quality = 70
hint = "picture"
compression = "lossless"
-- layouts/home.html --
Home.
`
@@ -122,20 +122,11 @@ Home.
cfg.LogLevel = logg.LevelInfo
}))
b.AssertLogContains("project config key imaging.quality was deprecated in Hugo v0.164.0")
// The per-format settings should not trigger the deprecation.
files = `
-- hugo.toml --
[imaging.jpeg]
quality = 70
-- layouts/home.html --
Home.
`
b = hugolib.Test(t, files, hugolib.TestOptWithConfig(func(cfg *hugolib.IntegrationTestConfig) {
cfg.LogLevel = logg.LevelInfo
}))
b.AssertLogContains("! project config key imaging.quality was deprecated")
b.AssertLogContains(
"project config key imaging.quality was deprecated in Hugo v0.163.0",
"project config key imaging.hint was deprecated in Hugo v0.163.0",
"project config key imaging.compression was deprecated in Hugo v0.163.0",
)
}
func BenchmarkImageResize(b *testing.B) {