mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 07:18:54 +00:00
images: Deprecate Imaging.Compression and move it down to webp and avif configs
Also clean up and simplify the image config handling. Closes #14998
This commit is contained in:
@@ -90,7 +90,13 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
m := p.p.GetStringMap(d.key)
|
||||
if _, found := m["quality"]; found {
|
||||
hugo.DeprecateWithLogger("project config key imaging.quality", "Set the quality per format instead with imaging.jpeg.quality, imaging.webp.quality and/or imaging.avif.quality.", "v0.164.0", p.logger.Logger())
|
||||
hugo.DeprecateWithLogger("project config key imaging.quality", "Set the quality per format instead with imaging.jpeg.quality, imaging.webp.quality and/or imaging.avif.quality.", "v0.163.0", p.logger.Logger())
|
||||
}
|
||||
if _, found := m["compression"]; found {
|
||||
hugo.DeprecateWithLogger("project config key imaging.compression", "Set the compression type per format instead with imaging.webp.compression and/or imaging.avif.compression.", "v0.163.0", p.logger.Logger())
|
||||
}
|
||||
if _, found := m["hint"]; found {
|
||||
hugo.DeprecateWithLogger("project config key imaging.hint", "Set the hint per format instead with imaging.webp.hint and/or imaging.avif.hint.", "v0.163.0", p.logger.Logger())
|
||||
}
|
||||
var err error
|
||||
p.c.Imaging, err = images.DecodeConfig(m)
|
||||
|
||||
+199
-160
@@ -169,8 +169,6 @@ func ImageFormatFromMediaSubType(sub string) (Format, ImageResourceType) {
|
||||
}
|
||||
|
||||
const (
|
||||
defaultJPEGQuality = 75
|
||||
defaultAVIFQuality = 60
|
||||
defaultResampleFilter = "box"
|
||||
defaultBgColor = "#ffffff"
|
||||
defaultHint = "photo"
|
||||
@@ -180,32 +178,6 @@ const (
|
||||
defaultAvifEncoderSpeed = 10
|
||||
)
|
||||
|
||||
var (
|
||||
defaultImaging = map[string]any{
|
||||
"resampleFilter": defaultResampleFilter,
|
||||
"bgColor": defaultBgColor,
|
||||
"hint": defaultHint,
|
||||
"compression": defaultCompression,
|
||||
"webp": map[string]any{
|
||||
"useSharpYuv": defaultWebpUseSharpYuv,
|
||||
"method": defaultWebpMethod,
|
||||
},
|
||||
"avif": map[string]any{
|
||||
"encoderSpeed": defaultAvifEncoderSpeed,
|
||||
},
|
||||
}
|
||||
|
||||
defaultImageConfig *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
defaultImageConfig, err = DecodeConfig(defaultImaging)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, ImagingConfigInternal], error) {
|
||||
if in == nil {
|
||||
in = make(map[string]any)
|
||||
@@ -216,25 +188,24 @@ func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, Ima
|
||||
if err != nil {
|
||||
return ImagingConfigInternal{}, nil, err
|
||||
}
|
||||
_, qualityExplicit := m["quality"]
|
||||
// Merge in the defaults.
|
||||
hmaps.MergeShallow(m, defaultImaging)
|
||||
|
||||
// Deep merge webp defaults.
|
||||
if webp, ok := m["webp"].(map[string]any); ok {
|
||||
hmaps.MergeShallow(webp, defaultImaging["webp"].(map[string]any))
|
||||
i := ImagingConfigInternal{
|
||||
Imaging: ImagingConfig{
|
||||
BgColor: defaultBgColor,
|
||||
ResampleFilter: defaultResampleFilter,
|
||||
Webp: WebpConfig{
|
||||
UseSharpYuv: defaultWebpUseSharpYuv,
|
||||
Method: defaultWebpMethod,
|
||||
},
|
||||
Avif: AvifConfig{
|
||||
EncoderSpeed: defaultAvifEncoderSpeed,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Deep merge avif defaults.
|
||||
if avif, ok := m["avif"].(map[string]any); ok {
|
||||
hmaps.MergeShallow(avif, defaultImaging["avif"].(map[string]any))
|
||||
}
|
||||
|
||||
var i ImagingConfigInternal
|
||||
if err := mapstructure.Decode(m, &i.Imaging); err != nil {
|
||||
return i, nil, err
|
||||
}
|
||||
i.Imaging.qualityExplicit = qualityExplicit
|
||||
|
||||
if err := i.Imaging.init(); err != nil {
|
||||
return i, nil, err
|
||||
@@ -272,9 +243,8 @@ 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
|
||||
qualitySet bool
|
||||
c ImageConfig = newImageConfig()
|
||||
err error
|
||||
)
|
||||
|
||||
// Make to lower case, trim space and remove any empty strings.
|
||||
@@ -314,7 +284,6 @@ 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 {
|
||||
@@ -361,37 +330,8 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin
|
||||
}
|
||||
}
|
||||
|
||||
if c.Action != "" && c.Filter == nil {
|
||||
c.Filter = defaults.Config.ResampleFilter
|
||||
}
|
||||
|
||||
if c.Action != "" && c.Anchor == -1 {
|
||||
c.Anchor = defaults.Config.Anchor
|
||||
}
|
||||
|
||||
// default to the source format
|
||||
if c.TargetFormat == 0 {
|
||||
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.
|
||||
c.Quality = defaults.Config.Imaging.qualityFor(c.TargetFormat)
|
||||
}
|
||||
|
||||
if c.Compression == "" {
|
||||
c.Compression = defaults.Config.Imaging.Compression
|
||||
}
|
||||
|
||||
if c.BgColor == nil && c.TargetFormat != sourceFormat {
|
||||
if sourceFormat.SupportsTransparency() && !c.TargetFormat.SupportsTransparency() {
|
||||
c.BgColor = defaults.Config.BgColor
|
||||
}
|
||||
if err := c.init(defaults.Config, sourceFormat); err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
if mainImageVersionNumber > 0 {
|
||||
@@ -417,6 +357,7 @@ type ImageConfig struct {
|
||||
// This defines the output format of the output image. It defaults to the source format.
|
||||
TargetFormat Format
|
||||
|
||||
// Optional image processing action to perform ("resize", "crop", "fit", "fill"). When empty, the operation may still perform e.g. format conversion.
|
||||
Action string
|
||||
|
||||
// If set, this will be used as the key in filenames etc.
|
||||
@@ -439,6 +380,9 @@ type ImageConfig struct {
|
||||
// transparency.
|
||||
BgColor color.Color
|
||||
|
||||
Filter gift.Resampling
|
||||
Anchor gift.Anchor
|
||||
|
||||
// Hint about what type of picture this is. Used to optimize encoding
|
||||
// when target is webp (preset) or avif (chroma subsampling).
|
||||
Hint string
|
||||
@@ -454,16 +398,54 @@ type ImageConfig struct {
|
||||
|
||||
Width int
|
||||
Height int
|
||||
|
||||
Filter gift.Resampling
|
||||
|
||||
Anchor gift.Anchor
|
||||
}
|
||||
|
||||
func (cfg ImageConfig) Reanchor(a gift.Anchor) ImageConfig {
|
||||
cfg.Anchor = a
|
||||
cfg.Key = hashing.HashStringHex(cfg.Key, "reanchor", a)
|
||||
return cfg
|
||||
func (c *ImageConfig) init(defaults ImagingConfigInternal, sourceFormat Format) error {
|
||||
if c.Action != "" && c.Anchor == -1 {
|
||||
c.Anchor = defaults.Anchor
|
||||
}
|
||||
if c.TargetFormat == 0 {
|
||||
c.TargetFormat = sourceFormat
|
||||
}
|
||||
if c.Filter == nil {
|
||||
c.Filter = defaults.ResampleFilter
|
||||
}
|
||||
if c.Anchor == -1 {
|
||||
c.Anchor = defaults.Anchor
|
||||
}
|
||||
if c.Hint == "" {
|
||||
c.Hint = defaults.Imaging.hintFor(c.TargetFormat)
|
||||
}
|
||||
|
||||
if c.Quality == 0 {
|
||||
c.Quality = defaults.Imaging.qualityFor(c.TargetFormat)
|
||||
}
|
||||
|
||||
if c.Compression == "" {
|
||||
c.Compression = defaults.Imaging.compressionFor(c.TargetFormat)
|
||||
}
|
||||
|
||||
if c.TargetFormat == AVIF {
|
||||
c.EncoderSpeed = defaults.Imaging.Avif.EncoderSpeed
|
||||
}
|
||||
|
||||
if c.TargetFormat == WEBP {
|
||||
c.Method = defaults.Imaging.Webp.Method
|
||||
c.UseSharpYuv = defaults.Imaging.Webp.UseSharpYuv
|
||||
}
|
||||
|
||||
if c.BgColor == nil && c.TargetFormat != sourceFormat {
|
||||
if sourceFormat.SupportsTransparency() && !c.TargetFormat.SupportsTransparency() {
|
||||
c.BgColor = defaults.BgColor
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c ImageConfig) Reanchor(a gift.Anchor) ImageConfig {
|
||||
c.Anchor = a
|
||||
c.Key = hashing.HashStringHex(c.Key, "reanchor", a)
|
||||
return c
|
||||
}
|
||||
|
||||
type ImagingConfigInternal struct {
|
||||
@@ -504,18 +486,15 @@ type ImagingConfig struct {
|
||||
// Default image quality setting (1-100). Used as the fallback for JPEG,
|
||||
// WebP and AVIF when no per-format quality is set. When left unset, JPEG
|
||||
// and WebP default to 75 and AVIF to 60 (its scale differs perceptually).
|
||||
// Deprecated in v0.164.0: set the quality per format instead, see
|
||||
// Deprecated in v0.163.0: set the quality per format instead, see
|
||||
// imaging.jpeg.quality, imaging.webp.quality and imaging.avif.quality.
|
||||
Quality int
|
||||
|
||||
// Whether Quality was explicitly set in the config. When false, AVIF uses
|
||||
// its own lower default instead of the global Quality.
|
||||
qualityExplicit bool
|
||||
Quality int `json:"-"`
|
||||
|
||||
// Compression method to use.
|
||||
// One of "lossy" or "lossless".
|
||||
// Note that lossless is currently only supported for WebP and AVIF.
|
||||
Compression string
|
||||
// Deprecated in v0.163.0: set the compression method per format instead, see imaging.webp.compression and imaging.avif.compression.
|
||||
Compression string `json:"-"`
|
||||
|
||||
// Resample filter to use in resize operations.
|
||||
ResampleFilter string
|
||||
@@ -540,33 +519,37 @@ type ImagingConfig struct {
|
||||
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
|
||||
return cfg.Jpeg.Quality
|
||||
case WEBP:
|
||||
q = cfg.Webp.Quality
|
||||
return cfg.Webp.Quality
|
||||
case AVIF:
|
||||
q = cfg.Avif.Quality
|
||||
return cfg.Avif.Quality
|
||||
}
|
||||
if q > 0 {
|
||||
return q
|
||||
return 0
|
||||
}
|
||||
|
||||
func (cfg *ImagingConfig) compressionFor(f Format) string {
|
||||
switch f {
|
||||
case WEBP:
|
||||
return cfg.Webp.Compression
|
||||
case AVIF:
|
||||
return cfg.Avif.Compression
|
||||
}
|
||||
if f == AVIF && !cfg.qualityExplicit {
|
||||
return defaultAVIFQuality
|
||||
}
|
||||
return cfg.Quality
|
||||
return ""
|
||||
}
|
||||
|
||||
// hintFor returns the configured hint for the given target format.
|
||||
func (cfg *ImagingConfig) hintFor(f Format) string {
|
||||
if f == AVIF {
|
||||
switch f {
|
||||
case WEBP:
|
||||
return cfg.Webp.Hint
|
||||
case AVIF:
|
||||
return cfg.Avif.Hint
|
||||
}
|
||||
return cfg.Webp.Hint
|
||||
return ""
|
||||
}
|
||||
|
||||
var validMetaSources = map[string]bool{
|
||||
@@ -576,18 +559,23 @@ var validMetaSources = map[string]bool{
|
||||
}
|
||||
|
||||
func (cfg *ImagingConfig) init() error {
|
||||
if cfg.Quality == 0 && !cfg.qualityExplicit {
|
||||
cfg.Quality = defaultJPEGQuality
|
||||
}
|
||||
if cfg.Quality < 1 || cfg.Quality > 100 {
|
||||
return errors.New("image quality must be a number between 1 and 100")
|
||||
}
|
||||
|
||||
cfg.BgColor = strings.ToLower(strings.TrimPrefix(cfg.BgColor, "#"))
|
||||
cfg.Anchor = strings.ToLower(cfg.Anchor)
|
||||
cfg.ResampleFilter = strings.ToLower(cfg.ResampleFilter)
|
||||
cfg.Hint = strings.ToLower(cfg.Hint)
|
||||
cfg.Compression = strings.ToLower(cfg.Compression)
|
||||
if err := cfg.Jpeg.init(cfg); err != nil {
|
||||
return fmt.Errorf("invalid jpeg config: %w", err)
|
||||
}
|
||||
if err := cfg.Webp.init(cfg); err != nil {
|
||||
return fmt.Errorf("invalid webp config: %w", err)
|
||||
}
|
||||
if err := cfg.Avif.init(cfg); err != nil {
|
||||
return fmt.Errorf("invalid avif config: %w", err)
|
||||
}
|
||||
if cfg.Quality < 0 || cfg.Quality > 100 {
|
||||
return fmt.Errorf("imaging.quality must be between 1 and 100 inclusive, got %d", cfg.Quality)
|
||||
}
|
||||
|
||||
if cfg.Anchor == "" {
|
||||
cfg.Anchor = smartCropIdentifier
|
||||
@@ -621,53 +609,6 @@ func (cfg *ImagingConfig) init() error {
|
||||
}
|
||||
}
|
||||
|
||||
// WebP config with backwards compatibility for root-level Hint.
|
||||
cfg.Webp.Hint = strings.ToLower(cfg.Webp.Hint)
|
||||
if cfg.Webp.Hint == "" {
|
||||
// Fall back to root-level hint for backwards compatibility.
|
||||
if cfg.Hint != "" {
|
||||
cfg.Webp.Hint = cfg.Hint
|
||||
} else {
|
||||
cfg.Webp.Hint = defaultHint
|
||||
}
|
||||
}
|
||||
if cfg.Webp.Hint != "" && !hints[cfg.Webp.Hint] {
|
||||
return fmt.Errorf("invalid webp hint %q; must be one of picture, photo, drawing, icon, or text", cfg.Webp.Hint)
|
||||
}
|
||||
if cfg.Webp.Method == 0 {
|
||||
cfg.Webp.Method = defaultWebpMethod
|
||||
}
|
||||
if cfg.Webp.Method < 0 || cfg.Webp.Method > 6 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
if cfg.Avif.EncoderSpeed < 1 || cfg.Avif.EncoderSpeed > 10 {
|
||||
return fmt.Errorf("avif encoderSpeed must be between 1 and 10, got %d", cfg.Avif.EncoderSpeed)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -713,11 +654,28 @@ type JpegConfig struct {
|
||||
Quality int
|
||||
}
|
||||
|
||||
func (c *JpegConfig) init(ic *ImagingConfig) error {
|
||||
if c.Quality == 0 {
|
||||
c.Quality = ic.Quality
|
||||
}
|
||||
if c.Quality == 0 {
|
||||
c.Quality = 75
|
||||
}
|
||||
if c.Quality < 1 || c.Quality > 100 {
|
||||
return fmt.Errorf("imaging.jpeg.quality must be between 1 and 100 inclusive, got %d", c.Quality)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AvifConfig holds AVIF-specific encoding configuration.
|
||||
type AvifConfig struct {
|
||||
// Quality setting (1-100). Falls back to the global imaging.quality if unset.
|
||||
Quality int
|
||||
|
||||
// Compression method to use.
|
||||
// One of "lossy" or "lossless".
|
||||
Compression string
|
||||
|
||||
// Hint about what type of image this is. Used for chroma subsampling.
|
||||
// Valid values are "picture", "photo", "drawing", "icon", or "text".
|
||||
// Default is "photo".
|
||||
@@ -730,16 +688,59 @@ type AvifConfig struct {
|
||||
// build time.
|
||||
// We recommend sticking with the default of 10 unless you have a specific reason to change it,
|
||||
// and to stay above 5 to avoid very long build times and timeouts.
|
||||
// 0 is treated as unset and falls back to the default.
|
||||
EncoderSpeed int
|
||||
}
|
||||
|
||||
func (c *AvifConfig) init(ic *ImagingConfig) error {
|
||||
if c.Hint == "" {
|
||||
c.Hint = ic.Hint
|
||||
}
|
||||
if c.Compression == "" {
|
||||
c.Compression = ic.Compression
|
||||
}
|
||||
if c.Quality == 0 {
|
||||
c.Quality = ic.Quality
|
||||
}
|
||||
if c.Hint == "" {
|
||||
c.Hint = defaultHint
|
||||
}
|
||||
if c.Compression == "" {
|
||||
c.Compression = defaultCompression
|
||||
}
|
||||
if c.Quality == 0 {
|
||||
c.Quality = 60
|
||||
}
|
||||
|
||||
c.Hint = strings.ToLower(c.Hint)
|
||||
c.Compression = strings.ToLower(c.Compression)
|
||||
|
||||
if c.Hint != "" && !hints[c.Hint] {
|
||||
return fmt.Errorf("imaging.avif.hint must be one of picture, photo, drawing, icon, or text, got %q", c.Hint)
|
||||
}
|
||||
if c.EncoderSpeed < 1 || c.EncoderSpeed > 10 {
|
||||
return fmt.Errorf("imaging.avif.encoderSpeed must be between 1 and 10, got %d", c.EncoderSpeed)
|
||||
}
|
||||
|
||||
if c.Compression != "" && !compressionMethods[c.Compression] {
|
||||
return fmt.Errorf("imaging.avif.compression must be one of lossy or lossless, got %q", c.Compression)
|
||||
}
|
||||
if c.Quality < 1 || c.Quality > 100 {
|
||||
return fmt.Errorf("imaging.avif.quality must be between 1 and 100 inclusive, got %d", c.Quality)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Compression method to use.
|
||||
// One of "lossy" or "lossless".
|
||||
Compression string
|
||||
|
||||
// Hint about what type of image this is.
|
||||
// Valid values are "picture", "photo", "drawing", "icon", or "text".
|
||||
// Default is "photo".
|
||||
@@ -753,3 +754,41 @@ type WebpConfig struct {
|
||||
// Default is 2.
|
||||
Method int
|
||||
}
|
||||
|
||||
func (c *WebpConfig) init(ic *ImagingConfig) error {
|
||||
if c.Hint == "" {
|
||||
c.Hint = ic.Hint
|
||||
}
|
||||
if c.Compression == "" {
|
||||
c.Compression = ic.Compression
|
||||
}
|
||||
if c.Quality == 0 {
|
||||
c.Quality = ic.Quality
|
||||
}
|
||||
if c.Hint == "" {
|
||||
c.Hint = defaultHint
|
||||
}
|
||||
if c.Compression == "" {
|
||||
c.Compression = defaultCompression
|
||||
}
|
||||
if c.Quality == 0 {
|
||||
c.Quality = 75
|
||||
}
|
||||
|
||||
c.Hint = strings.ToLower(c.Hint)
|
||||
c.Compression = strings.ToLower(c.Compression)
|
||||
|
||||
if c.Hint != "" && !hints[c.Hint] {
|
||||
return fmt.Errorf("imaging.webp.hint must be one of picture, photo, drawing, icon, or text, got %q", c.Hint)
|
||||
}
|
||||
if c.Method < 0 || c.Method > 6 {
|
||||
return fmt.Errorf("imaging.webp.method must be between 0 and 6, got %d", c.Method)
|
||||
}
|
||||
if c.Compression != "" && !compressionMethods[c.Compression] {
|
||||
return fmt.Errorf("imaging.webp.compression must be one of lossy or lossless, got %q", c.Compression)
|
||||
}
|
||||
if c.Quality < 1 || c.Quality > 100 {
|
||||
return fmt.Errorf("imaging.webp.quality must be between 1 and 100 inclusive, got %d", c.Quality)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+40
-153
@@ -18,8 +18,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
@@ -97,13 +95,6 @@ func TestDecodeConfig(t *testing.T) {
|
||||
})
|
||||
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},
|
||||
})
|
||||
@@ -111,163 +102,58 @@ 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) {
|
||||
func TestImageConfigPerFormat(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},
|
||||
"quality": 80,
|
||||
"compression": "lossy",
|
||||
"hint": "text",
|
||||
"jpeg": map[string]any{"quality": 80},
|
||||
"webp": map[string]any{"quality": 70, "hint": "picture", "compression": "lossless"},
|
||||
"avif": map[string]any{"quality": 55},
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
quality := func(opts ...string) int {
|
||||
conf := func(opts ...string) ImageConfig {
|
||||
conf, err := DecodeImageConfig(append([]string{"resize", "100x"}, opts...), cfg, JPEG)
|
||||
c.Assert(err, qt.IsNil)
|
||||
return conf.Quality
|
||||
return conf
|
||||
}
|
||||
|
||||
// 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)
|
||||
c.Assert(conf("jpg").Quality, qt.Equals, 80)
|
||||
c.Assert(conf("avif").Quality, qt.Equals, 55)
|
||||
c.Assert(conf("webp").Quality, qt.Equals, 70)
|
||||
|
||||
// A per-image quality always wins.
|
||||
c.Assert(quality("webp", "q33"), qt.Equals, 33)
|
||||
c.Assert(quality("avif", "q33"), qt.Equals, 33)
|
||||
c.Assert(conf("webp", "q33").Quality, qt.Equals, 33)
|
||||
c.Assert(conf("avif", "q33").Quality, 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)
|
||||
}
|
||||
c.Assert(conf("webp").Hint, qt.Equals, "picture")
|
||||
c.Assert(conf("avif").Hint, qt.Equals, "text")
|
||||
c.Assert(conf("jpeg").Hint, qt.Equals, "")
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// See issue 14990.
|
||||
func TestImageConfigFormatVersionNumber(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
cfg, err := DecodeConfig(map[string]any{})
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
key := func(f string) string {
|
||||
conf, err := DecodeImageConfig([]string{"resize", "100x", f}, cfg, JPEG)
|
||||
c.Assert(err, qt.IsNil)
|
||||
return conf.Key
|
||||
}
|
||||
|
||||
avifBefore, jpgBefore := key("avif"), key("jpg")
|
||||
|
||||
orig := formatVersionNumbers[AVIF]
|
||||
defer func() { formatVersionNumbers[AVIF] = orig }()
|
||||
formatVersionNumbers[AVIF] = orig + 1
|
||||
|
||||
// Bumping the AVIF version invalidates AVIF images only.
|
||||
c.Assert(key("avif"), qt.Not(qt.Equals), avifBefore)
|
||||
c.Assert(key("jpg"), qt.Equals, jpgBefore)
|
||||
c.Assert(conf("webp").Compression, qt.Equals, "lossless")
|
||||
c.Assert(conf("avif").Compression, qt.Equals, "lossy")
|
||||
c.Assert(conf("jpeg").Compression, qt.Equals, "")
|
||||
}
|
||||
|
||||
func TestDecodeImageConfig(t *testing.T) {
|
||||
c := qt.New(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", "300x400", newTestImageConfig("resize", 300, 400, 75, 0, "box", "smart", "")},
|
||||
{"resize", "300x400 #fff", newTestImageConfig("resize", 300, 400, 75, 0, "box", "smart", "fff")},
|
||||
{"resize", "100x200 bottomRight", newTestImageConfig("resize", 100, 200, 75, 0, "box", "BottomRight", "")},
|
||||
{"resize", "10x20 topleft Lanczos", newTestImageConfig("resize", 10, 20, 75, 0, "Lanczos", "topleft", "")},
|
||||
{"resize", "linear left 10x r180", newTestImageConfig("resize", 10, 0, 75, 180, "linear", "left", "")},
|
||||
{"resize", "x20 riGht Cosine q95", newTestImageConfig("resize", 0, 20, 95, 0, "cosine", "right", "")},
|
||||
{"crop", "300x400", newTestImageConfig("crop", 300, 400, 75, 0, "box", "smart", "")},
|
||||
{"fill", "300x400", newTestImageConfig("fill", 300, 400, 75, 0, "box", "smart", "")},
|
||||
{"fit", "300x400", newTestImageConfig("fit", 300, 400, 75, 0, "box", "smart", "")},
|
||||
|
||||
{"resize", "", false},
|
||||
{"resize", "foo", false},
|
||||
@@ -282,7 +168,7 @@ func TestDecodeImageConfig(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
options := append([]string{this.action}, strings.Fields(this.in)...)
|
||||
result, err := DecodeImageConfig(options, cfg, PNG)
|
||||
result, err := DecodeImageConfig(options, cfg, WEBP)
|
||||
if b, ok := this.expect.(bool); ok && !b {
|
||||
if err == nil {
|
||||
t.Errorf("[%d] parseImageConfig didn't return an expected error", i)
|
||||
@@ -294,24 +180,25 @@ func TestDecodeImageConfig(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
c.Assert(fmt.Sprint(result), qt.Equals, fmt.Sprint(expect))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newImageConfig(action string, width, height, quality, rotate int, filter, anchor, bgColor string) ImageConfig {
|
||||
var c ImageConfig = GetDefaultImageConfig(nil)
|
||||
func newTestImageConfig(action string, width, height, quality, rotate int, filter, anchor, bgColor string) ImageConfig {
|
||||
var c ImageConfig = newImageConfig()
|
||||
c.Action = action
|
||||
c.TargetFormat = PNG
|
||||
c.Hint = defaultHint // Resolved per target format in DecodeImageConfig.
|
||||
c.TargetFormat = WEBP
|
||||
c.Hint = defaultHint
|
||||
c.Compression = defaultCompression
|
||||
c.Width = width
|
||||
c.Height = height
|
||||
c.Quality = quality
|
||||
c.Rotate = rotate
|
||||
c.BgColor, _ = hexStringToColorGo(bgColor)
|
||||
c.Anchor = SmartCropAnchor
|
||||
c.Method = defaultWebpMethod
|
||||
|
||||
if filter != "" {
|
||||
filter = strings.ToLower(filter)
|
||||
|
||||
@@ -342,18 +342,9 @@ func (p *ImageProcessor) doFilter(src image.Image, targetFormat Format, filters
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
func GetDefaultImageConfig(defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) ImageConfig {
|
||||
if defaults == nil {
|
||||
defaults = defaultImageConfig
|
||||
}
|
||||
func newImageConfig() ImageConfig {
|
||||
return ImageConfig{
|
||||
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,
|
||||
Method: defaults.Config.Imaging.Webp.Method,
|
||||
EncoderSpeed: defaults.Config.Imaging.Avif.EncoderSpeed,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,12 +396,6 @@ 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 || f == AVIF
|
||||
}
|
||||
|
||||
// SupportsTransparency reports whether it supports transparency in any form.
|
||||
func (f Format) SupportsTransparency() bool {
|
||||
return f != JPEG
|
||||
|
||||
@@ -105,15 +105,15 @@ CropSmart: {{ .Width }}x{{ .Height }}|
|
||||
b.AssertFileContent("public/index.html", "Original: 900x562|CropTopLeft: 900x561|CropSmart: 900x561|")
|
||||
}
|
||||
|
||||
// The global imaging.quality was deprecated in v0.164.0 in favour of the
|
||||
// per-format quality settings. See issue 14979.
|
||||
func TestImagingQualityDeprecated(t *testing.T) {
|
||||
func TestImagingGlobalsDeprecated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[imaging]
|
||||
quality = 70
|
||||
hint = "picture"
|
||||
compression = "lossless"
|
||||
-- layouts/home.html --
|
||||
Home.
|
||||
`
|
||||
@@ -122,20 +122,11 @@ Home.
|
||||
cfg.LogLevel = logg.LevelInfo
|
||||
}))
|
||||
|
||||
b.AssertLogContains("project config key imaging.quality was deprecated in Hugo v0.164.0")
|
||||
|
||||
// The per-format settings should not trigger the deprecation.
|
||||
files = `
|
||||
-- hugo.toml --
|
||||
[imaging.jpeg]
|
||||
quality = 70
|
||||
-- layouts/home.html --
|
||||
Home.
|
||||
`
|
||||
b = hugolib.Test(t, files, hugolib.TestOptWithConfig(func(cfg *hugolib.IntegrationTestConfig) {
|
||||
cfg.LogLevel = logg.LevelInfo
|
||||
}))
|
||||
b.AssertLogContains("! project config key imaging.quality was deprecated")
|
||||
b.AssertLogContains(
|
||||
"project config key imaging.quality was deprecated in Hugo v0.163.0",
|
||||
"project config key imaging.hint was deprecated in Hugo v0.163.0",
|
||||
"project config key imaging.compression was deprecated in Hugo v0.163.0",
|
||||
)
|
||||
}
|
||||
|
||||
func BenchmarkImageResize(b *testing.B) {
|
||||
|
||||
Reference in New Issue
Block a user