From a043d3ec6323d9eb97db128c9fc710612708fa7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Thu, 4 Jun 2026 19:57:41 +0200 Subject: [PATCH] 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) --- resources/images/config.go | 35 +++++++++++++++++++++--- resources/images/config_test.go | 47 +++++++++++++++++++++++++++++++++ resources/images/image.go | 4 +-- 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/resources/images/config.go b/resources/images/config.go index f56c40930..ccc5e360d 100644 --- a/resources/images/config.go +++ b/resources/images/config.go @@ -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 diff --git a/resources/images/config_test.go b/resources/images/config_test.go index ef53b9208..9206c041d 100644 --- a/resources/images/config_test.go +++ b/resources/images/config_test.go @@ -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 diff --git a/resources/images/image.go b/resources/images/image.go index ae40b9b4c..5b1e6bb51 100644 --- a/resources/images/image.go +++ b/resources/images/image.go @@ -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,