mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
03b4b54220
AVIF's quality scale is not perceptually comparable to JPEG/WebP: libavif anchors its "good" default at 60, where Hugo's universal 75 produced visibly higher quality and larger files. AVIF now defaults to 60 while JPEG and WebP keep 75. An explicit imaging.quality, imaging.avif.quality or a per-image qNN still wins. Fixes #14979 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
261 lines
8.0 KiB
Go
261 lines
8.0 KiB
Go
// Copyright 2019 The Hugo Authors. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package images
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gohugoio/hugo/config"
|
|
|
|
qt "github.com/frankban/quicktest"
|
|
)
|
|
|
|
func TestDecodeConfig(t *testing.T) {
|
|
c := qt.New(t)
|
|
m := map[string]any{
|
|
"quality": 42,
|
|
"resampleFilter": "NearestNeighbor",
|
|
"anchor": "topLeft",
|
|
}
|
|
|
|
imagingConfig, err := DecodeConfig(m)
|
|
|
|
c.Assert(err, qt.IsNil)
|
|
conf := imagingConfig.Config
|
|
c.Assert(conf.Imaging.Quality, qt.Equals, 42)
|
|
c.Assert(conf.Imaging.ResampleFilter, qt.Equals, "nearestneighbor")
|
|
c.Assert(conf.Imaging.Anchor, qt.Equals, "topleft")
|
|
|
|
m = map[string]any{}
|
|
|
|
imagingConfig, err = DecodeConfig(m)
|
|
c.Assert(err, qt.IsNil)
|
|
conf = imagingConfig.Config
|
|
c.Assert(conf.Imaging.ResampleFilter, qt.Equals, "box")
|
|
c.Assert(conf.Imaging.Anchor, qt.Equals, "smart")
|
|
|
|
_, err = DecodeConfig(map[string]any{
|
|
"quality": 123,
|
|
})
|
|
c.Assert(err, qt.Not(qt.IsNil))
|
|
|
|
_, err = DecodeConfig(map[string]any{
|
|
"resampleFilter": "asdf",
|
|
})
|
|
c.Assert(err, qt.Not(qt.IsNil))
|
|
|
|
_, err = DecodeConfig(map[string]any{
|
|
"anchor": "asdf",
|
|
})
|
|
c.Assert(err, qt.Not(qt.IsNil))
|
|
|
|
imagingConfig, err = DecodeConfig(map[string]any{
|
|
"anchor": "Smart",
|
|
})
|
|
conf = imagingConfig.Config
|
|
c.Assert(err, qt.IsNil)
|
|
c.Assert(conf.Imaging.Anchor, qt.Equals, "smart")
|
|
|
|
imagingConfig, err = DecodeConfig(map[string]any{
|
|
"exif": map[string]any{
|
|
"disableLatLong": true,
|
|
},
|
|
})
|
|
c.Assert(err, qt.IsNil)
|
|
conf = imagingConfig.Config
|
|
c.Assert(conf.Imaging.Exif.DisableLatLong, qt.Equals, true)
|
|
c.Assert(conf.Imaging.Exif.ExcludeFields, qt.Equals, "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance")
|
|
|
|
// AVIF: default is speed 10.
|
|
imagingConfig, err = DecodeConfig(map[string]any{})
|
|
c.Assert(err, qt.IsNil)
|
|
c.Assert(imagingConfig.Config.Imaging.Avif.EncoderSpeed, qt.Equals, defaultAvifEncoderSpeed)
|
|
|
|
// AVIF: override via config.
|
|
imagingConfig, err = DecodeConfig(map[string]any{
|
|
"avif": map[string]any{"encoderSpeed": 5},
|
|
})
|
|
c.Assert(err, qt.IsNil)
|
|
c.Assert(imagingConfig.Config.Imaging.Avif.EncoderSpeed, qt.Equals, 5)
|
|
|
|
// AVIF: out-of-range rejected.
|
|
_, err = DecodeConfig(map[string]any{
|
|
"avif": map[string]any{"encoderSpeed": 11},
|
|
})
|
|
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},
|
|
})
|
|
c.Assert(err, qt.IsNil)
|
|
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.*")
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
func TestDecodeImageConfig(t *testing.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", "", false},
|
|
{"resize", "foo", false},
|
|
{"crop", "100x", false},
|
|
{"fill", "100x", false},
|
|
{"fit", "100x", false},
|
|
{"foo", "100x", false},
|
|
} {
|
|
|
|
cfg, err := DecodeConfig(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
options := append([]string{this.action}, strings.Fields(this.in)...)
|
|
result, err := DecodeImageConfig(options, cfg, PNG)
|
|
if b, ok := this.expect.(bool); ok && !b {
|
|
if err == nil {
|
|
t.Errorf("[%d] parseImageConfig didn't return an expected error", i)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("[%d] err: %s", i, err)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func newImageConfig(action string, width, height, quality, rotate int, filter, anchor, bgColor string) ImageConfig {
|
|
var c ImageConfig = GetDefaultImageConfig(nil)
|
|
c.Action = action
|
|
c.TargetFormat = PNG
|
|
c.Width = width
|
|
c.Height = height
|
|
c.Quality = quality
|
|
c.Rotate = rotate
|
|
c.BgColor, _ = hexStringToColorGo(bgColor)
|
|
c.Anchor = SmartCropAnchor
|
|
|
|
if filter != "" {
|
|
filter = strings.ToLower(filter)
|
|
if v, ok := imageFilters[filter]; ok {
|
|
c.Filter = v
|
|
}
|
|
}
|
|
|
|
if anchor != "" {
|
|
anchor = strings.ToLower(anchor)
|
|
if v, ok := anchorPositions[anchor]; ok {
|
|
c.Anchor = v
|
|
}
|
|
}
|
|
|
|
return c
|
|
}
|