The original idea of setting quality to 0 for lossless WebP was didn't allow a setting that made sense for both JPEG and WebP.
Note that the above didn't really work at all, so this should not break anything.
typedef struct
{
- float quality; // between 0 and 100. Set to 0 for lossless.
- char hint[64]; // drawing, icon, photo, picture, or text. Default is photo.
- int preset; // preset to use; resolved from hint.
+ float quality; // between 1 and 100.
+ char compression[32]; // "lossy" or "lossless"
+ char hint[64]; // drawing, icon, photo, picture, or text. Default is photo.
+ int preset; // preset to use; resolved from hint.
bool useSharpYuv; // use sharp YUV for better quality.
return 0;
}
- if (opts.quality == 0)
+ if (strcmp(opts.compression, "lossless") == 0)
{
// Activate the lossless compression mode with the desired efficiency level
// between 0 (fastest, lowest compression) and 9 (slower, best compression).
if (options_object != NULL)
{
msg.data.options.quality = (int)json_object_get_number(options_object, "quality");
+ const char *compression_str = json_object_get_string(options_object, "compression");
+ if (compression_str != NULL)
+ {
+ strncpy(msg.data.options.compression, compression_str, sizeof(msg.data.options.compression) - 1);
+ msg.data.options.compression[sizeof(msg.data.options.compression) - 1] = '\0';
+ }
const char *hint_str = json_object_get_string(options_object, "hint");
if (hint_str != NULL)
{
return d.webp.start()
}
-func (d *Dispatchers) NewWepCodec(quality int, hint string) (*WebpCodec, error) {
+func (d *Dispatchers) NewWepCodec() (*WebpCodec, error) {
return &WebpCodec{
- d: d.Webp,
- quality: quality,
- hint: hint,
+ d: d.Webp,
}, nil
}
"image/color"
"image/draw"
"io"
+ "maps"
"github.com/gohugoio/hugo/common/himage"
"github.com/gohugoio/hugo/common/hugio"
}
type WebpCodec struct {
- d func() (Dispatcher[WebpInput, WebpOutput], error)
- quality int
- hint string
+ d func() (Dispatcher[WebpInput, WebpOutput], error)
}
// Decode reads a WEBP image from r and returns it as an image.Image.
}, nil
}
-func (d *WebpCodec) Encode(w io.Writer, img image.Image) error {
- return d.EncodeOptions(w, img, nil)
-}
-
-func (d *WebpCodec) EncodeOptions(w io.Writer, img image.Image, options map[string]any) error {
+func (d *WebpCodec) Encode(w io.Writer, img image.Image, opts map[string]any) error {
b := img.Bounds()
if b.Dx() >= 1<<16 || b.Dy() >= 1<<16 {
return errors.New("webp: image is too large to encode")
return fmt.Errorf("no image bytes extracted from %T", img)
}
- // Commands:
- // encodeNRGBA
- // encodeGray
- // decode
- // config
-
- opts := map[string]any{
- "quality": d.quality, // a number between 0 and 100. Set to 0 for lossless.
- "hint": d.hint, // drawing, icon, photo, picture, or text
- "useSharpYuv": true, // Use sharp (and slow) RGB->YUV conversion.
- }
-
- // Override with per-image options if provided.
- for _, key := range []string{"quality", "hint"} {
- if v, ok := options[key]; ok {
- opts[key] = v
- }
- }
+ opts = maps.Clone(opts)
+ opts["useSharpYuv"] = true // Use sharp (and slow) RGB->YUV conversion.
message := Message[WebpInput]{
Header: Header{
DecodeConfig(r io.Reader) (image.Config, error)
}
-// Encoder defines the encoding of an image format.
-type Encoder interface {
- Encode(w io.Writer, src image.Image) error
-}
-
type ToEncoder interface {
EncodeTo(conf ImageConfig, w io.Writer, src image.Image) error
}
-// EncoderWithOptions defines the encoding of an image format with options.
-// This is currently only used for WebP and the options are passed as a map
-// to match the internal WASM API. The options map may be nil when no options
-// need to be overridden.
-type EncoderWithOptions interface {
- EncodeOptions(w io.Writer, src image.Image, options map[string]any) error
+// EncoderWithOptions defines the encoding of an image format with the given options.
+type Encoder interface {
+ Encode(w io.Writer, src image.Image, options map[string]any) error
}
-// CodecStdlib defines both decoding and encoding of an image format as defined by the standard library.
-type CodecStdlib interface {
+// EncodeDecoder defines both decoding and encoding of an image format as defined by the standard library.
+type EncodeDecoder interface {
Decoder
Encoder
}
// Codec is a generic image codec supporting multiple formats.
type Codec struct {
- webp CodecStdlib
+ webp EncodeDecoder
}
-func newCodec(webp CodecStdlib) *Codec {
+func newCodec(webp EncodeDecoder) *Codec {
return &Codec{webp: webp}
}
case BMP:
return bmp.Encode(w, img)
case WEBP:
- if enc, ok := d.webp.(EncoderWithOptions); ok {
- var opts map[string]any
- if conf.qualitySetForImage || conf.hintSetForImage {
- opts = make(map[string]any)
- if conf.qualitySetForImage {
- opts["quality"] = conf.Quality
- }
- if conf.hintSetForImage {
- opts["hint"] = conf.Hint
- }
- }
- return enc.EncodeOptions(w, img, opts)
+ opts := map[string]any{
+ "compression": conf.Compression,
+ "quality": conf.Quality,
+ "hint": conf.Hint,
}
- return d.webp.Encode(w, img)
+ return d.webp.Encode(w, img, opts)
default:
return errors.New("format not supported")
}
smartCropIdentifier: SmartCropAnchor,
}
+var compressionMethods = map[string]bool{
+ "lossy": true,
+ "lossless": true,
+}
+
// These encoding hints are currently only relevant for Webp.
var hints = map[string]bool{
"picture": true,
defaultResampleFilter = "box"
defaultBgColor = "#ffffff"
defaultHint = "photo"
+ defaultCompression = "lossy"
)
var (
"bgColor": defaultBgColor,
"hint": defaultHint,
"quality": defaultJPEGQuality,
+ "compression": defaultCompression,
}
defaultImageConfig *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]
c.Filter = filter
} else if _, ok := hints[part]; ok {
c.Hint = part
- c.hintSetForImage = true
+ } else if _, ok := compressionMethods[part]; ok {
+ c.Compression = part
} else if part[0] == '#' {
c.BgColor, err = hexStringToColorGo(part[1:])
if err != nil {
if err != nil {
return c, err
}
- if c.Quality < 0 || c.Quality > 100 {
- return c, errors.New("quality ranges from 0 to 100 inclusive")
+ if c.Quality < 1 || c.Quality > 100 {
+ return c, errors.New("quality ranges from 1 to 100 inclusive")
}
- c.qualitySetForImage = true
} else if part[0] == 'r' {
c.Rotate, err = strconv.Atoi(part[1:])
if err != nil {
c.TargetFormat = sourceFormat
}
- if !c.qualitySetForImage && c.Quality <= 0 && c.TargetFormat.RequiresDefaultQuality() {
+ if c.Quality <= 0 && c.TargetFormat.RequiresDefaultQuality() {
// We need a quality setting for all JPEGs and WEBPs,
- // unless the user explicitly set quality (e.g., q0 for lossless WebP).
+ // unless the user explicitly set quality.
c.Quality = defaults.Config.Imaging.Quality
}
+ 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 set, this will be used as the key in filenames etc.
Key string
- // Quality ranges from 1 to 100 inclusive, higher is better.
+ // Quality ranges from 0 to 100 inclusive, higher is better.
// This is only relevant for JPEG and WEBP images.
+ // For WebP, 0 means lossless.
// Default is 75.
- Quality int
- qualitySetForImage bool // Whether the above is set for this image.
+ Quality int
// Rotate rotates an image by the given angle counter-clockwise.
// The rotation will be performed first.
// Hint about what type of picture this is. Used to optimize encoding
// when target is set to webp.
- Hint string
- hintSetForImage bool // Whether the above is set for this image.
+ Hint string
+
+ Compression string
Width int
Height int
// Default image quality setting (1-100). Only used for JPEG and WebP images.
Quality int
+ // Compression method to use.
+ // One of "lossy" or "lossless".
+ // Note that lossless is currently only supported for WebP.
+ Compression string
+
// Resample filter to use in resize operations.
ResampleFilter string
}
func (cfg *ImagingConfig) init() error {
- if cfg.Quality < 0 || cfg.Quality > 100 {
+ if cfg.Quality < 1 || cfg.Quality > 100 {
return errors.New("image quality must be a number between 1 and 100")
}
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 cfg.Anchor == "" {
cfg.Anchor = smartCropIdentifier
c.Width = width
c.Height = height
c.Quality = quality
- c.qualitySetForImage = quality != 75
c.Rotate = rotate
c.BgColor, _ = hexStringToColorGo(bgColor)
c.Anchor = SmartCropAnchor
return nil, err
}
- webpCodec, err := wasmDispatchers.NewWepCodec(cfg.Config.Imaging.Quality, cfg.Config.Imaging.Hint)
+ webpCodec, err := wasmDispatchers.NewWepCodec()
if err != nil {
return nil, err
}
defaults = defaultImageConfig
}
return ImageConfig{
- Anchor: -1, // The real values start at 0.
- Hint: "photo",
- Quality: defaults.Config.Imaging.Quality,
+ Anchor: -1, // The real values start at 0.
+ Hint: "photo",
+ Quality: defaults.Config.Imaging.Quality,
+ Compression: defaults.Config.Imaging.Compression,
}
}
"github.com/gohugoio/hugo/resources/images/imagetesting"
)
+const goldenProcess = `
+{{ define "process"}}
+{{ $img := .img.Process .spec }}
+{{ $ext := path.Ext $img.RelPermalink }}
+{{ $name := printf "images/%s%s" (.spec | anchorize) $ext }}
+{{ with $img | resources.Copy $name }}
+{{ .Publish }}
+{{ end }}
+{{ end }}
+`
+
// Note, if you're enabling writeGoldenFiles on a MacOS ARM 64 you need to run the test with GOARCH=amd64, e.g.
func TestImagesGoldenFiltersMisc(t *testing.T) {
t.Parallel()
{{ template "process" (dict "spec" "resize 100x100 r180" "img" $gopher) }}
{{ template "process" (dict "spec" "resize 300x300 jpg #b31280" "img" $gopher) }}
-{{ define "process"}}
-{{ $img := .img.Process .spec }}
-{{ $ext := path.Ext $img.RelPermalink }}
-{{ $name := printf "images/%s%s" (.spec | anchorize) $ext }}
-{{ with $img | resources.Copy $name }}
-{{ .Publish }}
-{{ end }}
-{{ end }}
-`
+
+` + goldenProcess
opts := imagetesting.DefaultGoldenOpts
opts.T = t
{{/* These are sorted. The end file name will be created from the spec + extension, so make sure these are unique. */}}
{{ template "process" (dict "spec" "crop 300x300 gif" "img" $animWebp) }}
{{ template "process" (dict "spec" "crop 300x300 smart" "img" $fuzzyCircle) }}
- {{ template "process" (dict "spec" "crop 300x300 smart #ff9999" "img" $fuzzyCircle) }}
+{{ template "process" (dict "spec" "crop 300x300 smart #ff9999" "img" $fuzzyCircle) }}
{{ template "process" (dict "spec" "crop 300x300" "img" $animWebp) }}
{{ template "process" (dict "spec" "crop 500x200 smart webp" "img" $sunset) }}
{{ template "process" (dict "spec" "crop 500x200 smart webp" "img" $sunset) }}
{{ template "process" (dict "spec" "png" "img" $highContrast) }}
{{ template "process" (dict "spec" "resize 300x300" "img" $giphy) }}
{{ template "process" (dict "spec" "resize 300x300 webp" "img" $giphy) }}
-{{ template "process" (dict "spec" "resize 300x300 webp q0" "img" $sunset) }}
+{{ template "process" (dict "spec" "resize 300x300 webp lossless" "img" $sunset) }}
{{ template "process" (dict "spec" "resize 300x300 webp q1" "img" $sunset) }}
{{ template "process" (dict "spec" "resize 300x300 webp q33" "img" $sunset) }}
{{ template "process" (dict "spec" "resize 300x300 webp q75" "img" $sunset) }}
{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "fit" "spec" "200x200" ) }}
{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "200x200" ) }}
{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "350x400 center" ) }}
- {{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "350x400 smart" ) }}
+{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "350x400 smart" ) }}
{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "350x400 center r90" ) }}
{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "350x400 center q20" ) }}
{{ template "invoke" (dict "copyFormat" "png" "base" $gopher "method" "resize" "spec" "100x" ) }}
imagetesting.RunGolden(opts)
}
+
+func TestImagesGoldenConfigLossyVsQuality(t *testing.T) {
+ t.Parallel()
+
+ if imagetesting.SkipGoldenTests {
+ t.Skip("Skip golden test on this architecture")
+ }
+
+ files := `
+-- hugo.toml --
+[imaging]
+quality = 90 # will only apply to jpeg in this setup.
+compression = "lossless" # for webp
+-- assets/sunset.jpg --
+sourcefilename: ../testdata/sunset.jpg
+-- layouts/home.html --
+Home.
+{{ $sunset := resources.Get "sunset.jpg" }}
+{{ template "process" (dict "spec" "resize 300x300 webp" "img" $sunset) }}
+{{ template "process" (dict "spec" "resize 300x300 webp lossy" "img" $sunset) }}
+{{ template "process" (dict "spec" "resize 300x300 jpeg" "img" $sunset) }}
+
+` + goldenProcess
+
+ // Will be used as the base folder for generated images.
+ name := "losslessvsquality"
+
+ opts := imagetesting.DefaultGoldenOpts
+ opts.T = t
+ opts.Name = name
+ opts.Files = files
+
+ imagetesting.RunGolden(opts)
+}