mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
images: Add quality setting per image format
Allow setting quality per output format via imaging.jpeg.quality, imaging.webp.quality and imaging.avif.quality. Each falls back to the global imaging.quality when unset, and a per-image qNN still wins. Fixes #14957 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -263,8 +263,9 @@ 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
|
||||
c ImageConfig = GetDefaultImageConfig(defaults)
|
||||
err error
|
||||
qualitySet bool
|
||||
)
|
||||
|
||||
// Make to lower case, trim space and remove any empty strings.
|
||||
@@ -304,6 +305,7 @@ 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 {
|
||||
@@ -367,10 +369,10 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin
|
||||
c.TargetFormat = sourceFormat
|
||||
}
|
||||
|
||||
if c.Quality <= 0 && c.TargetFormat.RequiresDefaultQuality() {
|
||||
// We need a quality setting for all JPEGs and WEBPs,
|
||||
// unless the user explicitly set quality.
|
||||
c.Quality = defaults.Config.Imaging.Quality
|
||||
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 == "" {
|
||||
@@ -512,10 +514,29 @@ type ImagingConfig struct {
|
||||
|
||||
Exif ExifConfig
|
||||
Meta MetaConfig
|
||||
Jpeg JpegConfig
|
||||
Webp WebpConfig
|
||||
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
|
||||
case WEBP:
|
||||
q = cfg.Webp.Quality
|
||||
case AVIF:
|
||||
q = cfg.Avif.Quality
|
||||
}
|
||||
if q > 0 {
|
||||
return q
|
||||
}
|
||||
return cfg.Quality
|
||||
}
|
||||
|
||||
var validMetaSources = map[string]bool{
|
||||
"exif": true,
|
||||
"iptc": true,
|
||||
@@ -585,6 +606,12 @@ func (cfg *ImagingConfig) init() error {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Avif.EncoderSpeed == 0 {
|
||||
cfg.Avif.EncoderSpeed = defaultAvifEncoderSpeed
|
||||
}
|
||||
@@ -631,8 +658,17 @@ type MetaConfig struct {
|
||||
Sources []string
|
||||
}
|
||||
|
||||
// JpegConfig holds JPEG-specific encoding configuration.
|
||||
type JpegConfig struct {
|
||||
// Quality setting (1-100). Falls back to the global imaging.quality if unset.
|
||||
Quality int
|
||||
}
|
||||
|
||||
// AvifConfig holds AVIF-specific encoding configuration.
|
||||
type AvifConfig struct {
|
||||
// Quality setting (1-100). Falls back to the global imaging.quality if unset.
|
||||
Quality int
|
||||
|
||||
// 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
|
||||
@@ -646,6 +682,10 @@ type AvifConfig struct {
|
||||
|
||||
// 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
|
||||
|
||||
// Hint about what type of image this is.
|
||||
// Valid values are "picture", "photo", "drawing", "icon", or "text".
|
||||
// Default is "photo".
|
||||
|
||||
@@ -109,6 +109,47 @@ func TestDecodeConfig(t *testing.T) {
|
||||
c.Assert(imagingConfig.Config.Imaging.Avif.EncoderSpeed, qt.Equals, 1)
|
||||
}
|
||||
|
||||
// See issue 14957.
|
||||
func TestImageConfigQualityPerFormat(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},
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
quality := func(opts ...string) int {
|
||||
conf, err := DecodeImageConfig(append([]string{"resize", "100x"}, opts...), cfg, JPEG)
|
||||
c.Assert(err, qt.IsNil)
|
||||
return conf.Quality
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// A per-image quality always wins.
|
||||
c.Assert(quality("webp", "q33"), qt.Equals, 33)
|
||||
c.Assert(quality("avif", "q33"), 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)
|
||||
}
|
||||
|
||||
// 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.*")
|
||||
}
|
||||
|
||||
func TestDecodeImageConfig(t *testing.T) {
|
||||
for i, this := range []struct {
|
||||
action string
|
||||
|
||||
@@ -408,7 +408,7 @@ 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
|
||||
return f == JPEG || f == WEBP || f == AVIF
|
||||
}
|
||||
|
||||
// SupportsTransparency reports whether it supports transparency in any form.
|
||||
|
||||
Reference in New Issue
Block a user