From cf18b827e2bebe95f87b36bff9937218348a55ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Fri, 5 Jun 2026 19:04:12 +0200 Subject: [PATCH] images: Deprecate Imaging.Compression and move it down to webp and avif configs Also clean up and simplify the image config handling. Closes #14998 --- config/allconfig/alldecoders.go | 8 +- resources/images/config.go | 359 +++++++++++--------- resources/images/config_test.go | 193 +++-------- resources/images/image.go | 17 +- resources/images/images_integration_test.go | 25 +- 5 files changed, 255 insertions(+), 347 deletions(-) diff --git a/config/allconfig/alldecoders.go b/config/allconfig/alldecoders.go index cac0b7c59..619eea262 100644 --- a/config/allconfig/alldecoders.go +++ b/config/allconfig/alldecoders.go @@ -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) diff --git a/resources/images/config.go b/resources/images/config.go index a9e60cac6..4cbd7e629 100644 --- a/resources/images/config.go +++ b/resources/images/config.go @@ -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 +} diff --git a/resources/images/config_test.go b/resources/images/config_test.go index ad6cfbd00..2adc1fe37 100644 --- a/resources/images/config_test.go +++ b/resources/images/config_test.go @@ -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) diff --git a/resources/images/image.go b/resources/images/image.go index 5b1e6bb51..a0d9a77e4 100644 --- a/resources/images/image.go +++ b/resources/images/image.go @@ -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 diff --git a/resources/images/images_integration_test.go b/resources/images/images_integration_test.go index 248ea6ef9..172449d21 100644 --- a/resources/images/images_integration_test.go +++ b/resources/images/images_integration_test.go @@ -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) {