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,