images: Add a per-format AVIF hint setting

The hint setting controls WebP encoding (preset) and AVIF encoding
(chroma subsampling), but only lived on imaging.webp. Add imaging.avif.hint
so it shows up under the AVIF section in the docs, with the same root-level
backwards compatibility as imaging.webp.hint. Per-image hint now resolves
from the target format.

Fixes #14992

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bjørn Erik Pedersen
2026-06-04 19:57:41 +02:00
parent 77a1147056
commit a043d3ec63
3 changed files with 80 additions and 6 deletions
+31 -4
View File
@@ -358,10 +358,6 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin
c.Filter = defaults.Config.ResampleFilter
}
if c.Hint == "" {
c.Hint = defaults.Config.Imaging.Webp.Hint
}
if c.Action != "" && c.Anchor == -1 {
c.Anchor = defaults.Config.Anchor
}
@@ -371,6 +367,10 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin
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.
@@ -550,6 +550,14 @@ func (cfg *ImagingConfig) qualityFor(f Format) int {
return cfg.Quality
}
// hintFor returns the configured hint for the given target format.
func (cfg *ImagingConfig) hintFor(f Format) string {
if f == AVIF {
return cfg.Avif.Hint
}
return cfg.Webp.Hint
}
var validMetaSources = map[string]bool{
"exif": true,
"iptc": true,
@@ -628,6 +636,20 @@ func (cfg *ImagingConfig) init() error {
}
}
// 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
}
@@ -685,6 +707,11 @@ type AvifConfig struct {
// Quality setting (1-100). Falls back to the global imaging.quality if unset.
Quality int
// Hint about what type of image this is. Used for chroma subsampling.
// Valid values are "picture", "photo", "drawing", "icon", or "text".
// Default is "photo".
Hint string
// Encoder quality/speed trade-off, 1 (slowest, best quality / smallest
// files) to 10 (fastest). Default is 10 — fast enough for incremental
// builds with quality indistinguishable from slower settings at typical
+47
View File
@@ -111,6 +111,52 @@ 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) {
c := qt.New(t)
@@ -235,6 +281,7 @@ func newImageConfig(action string, width, height, quality, rotate int, filter, a
var c ImageConfig = GetDefaultImageConfig(nil)
c.Action = action
c.TargetFormat = PNG
c.Hint = defaultHint // Resolved per target format in DecodeImageConfig.
c.Width = width
c.Height = height
c.Quality = quality
+2 -2
View File
@@ -347,8 +347,8 @@ func GetDefaultImageConfig(defaults *config.ConfigNamespace[ImagingConfig, Imagi
defaults = defaultImageConfig
}
return ImageConfig{
Anchor: -1, // The real values start at 0.
Hint: defaults.Config.Imaging.Webp.Hint,
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,