Skip to content

Commit c6ae33c

Browse files
committed
images: Add compression option to image config and clean up some of the options handling
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.
1 parent edeebf0 commit c6ae33c

File tree

13 files changed

+115
-91
lines changed

13 files changed

+115
-91
lines changed

‎internal/warpc/genwebp/webp.c‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@ typedef struct
7373

7474
typedef struct
7575
{
76-
float quality; // between 0 and 100. Set to 0 for lossless.
77-
char hint[64]; // drawing, icon, photo, picture, or text. Default is photo.
78-
int preset; // preset to use; resolved from hint.
76+
float quality; // between 1 and 100.
77+
char compression[32]; // "lossy" or "lossless"
78+
char hint[64]; // drawing, icon, photo, picture, or text. Default is photo.
79+
int preset; // preset to use; resolved from hint.
7980

8081
bool useSharpYuv; // use sharp YUV for better quality.
8182

@@ -302,7 +303,7 @@ static uint8_t initEncoderConfig(WebPConfig *config, InputOptions opts)
302303
return 0;
303304
}
304305

305-
if (opts.quality == 0)
306+
if (strcmp(opts.compression, "lossless") == 0)
306307
{
307308
// Activate the lossless compression mode with the desired efficiency level
308309
// between 0 (fastest, lowest compression) and 9 (slower, best compression).
@@ -396,6 +397,12 @@ InputMessage parse_input_message(const char *line)
396397
if (options_object != NULL)
397398
{
398399
msg.data.options.quality = (int)json_object_get_number(options_object, "quality");
400+
const char *compression_str = json_object_get_string(options_object, "compression");
401+
if (compression_str != NULL)
402+
{
403+
strncpy(msg.data.options.compression, compression_str, sizeof(msg.data.options.compression) - 1);
404+
msg.data.options.compression[sizeof(msg.data.options.compression) - 1] = '\0';
405+
}
399406
const char *hint_str = json_object_get_string(options_object, "hint");
400407
if (hint_str != NULL)
401408
{

‎internal/warpc/warpc.go‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -792,11 +792,9 @@ func (d *Dispatchers) Webp() (Dispatcher[WebpInput, WebpOutput], error) {
792792
return d.webp.start()
793793
}
794794

795-
func (d *Dispatchers) NewWepCodec(quality int, hint string) (*WebpCodec, error) {
795+
func (d *Dispatchers) NewWepCodec() (*WebpCodec, error) {
796796
return &WebpCodec{
797-
d: d.Webp,
798-
quality: quality,
799-
hint: hint,
797+
d: d.Webp,
800798
}, nil
801799
}
802800

‎internal/warpc/wasm/webp.wasm‎

90 Bytes
Binary file not shown.

‎internal/warpc/webp.go‎

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"image/color"
2323
"image/draw"
2424
"io"
25+
"maps"
2526

2627
"github.com/gohugoio/hugo/common/himage"
2728
"github.com/gohugoio/hugo/common/hugio"
@@ -82,9 +83,7 @@ type WebpOutput struct {
8283
}
8384

8485
type WebpCodec struct {
85-
d func() (Dispatcher[WebpInput, WebpOutput], error)
86-
quality int
87-
hint string
86+
d func() (Dispatcher[WebpInput, WebpOutput], error)
8887
}
8988

9089
// Decode reads a WEBP image from r and returns it as an image.Image.
@@ -201,11 +200,7 @@ func (d *WebpCodec) DecodeConfig(r io.Reader) (image.Config, error) {
201200
}, nil
202201
}
203202

204-
func (d *WebpCodec) Encode(w io.Writer, img image.Image) error {
205-
return d.EncodeOptions(w, img, nil)
206-
}
207-
208-
func (d *WebpCodec) EncodeOptions(w io.Writer, img image.Image, options map[string]any) error {
203+
func (d *WebpCodec) Encode(w io.Writer, img image.Image, opts map[string]any) error {
209204
b := img.Bounds()
210205
if b.Dx() >= 1<<16 || b.Dy() >= 1<<16 {
211206
return errors.New("webp: image is too large to encode")
@@ -298,24 +293,8 @@ func (d *WebpCodec) EncodeOptions(w io.Writer, img image.Image, options map[stri
298293
return fmt.Errorf("no image bytes extracted from %T", img)
299294
}
300295

301-
// Commands:
302-
// encodeNRGBA
303-
// encodeGray
304-
// decode
305-
// config
306-
307-
opts := map[string]any{
308-
"quality": d.quality, // a number between 0 and 100. Set to 0 for lossless.
309-
"hint": d.hint, // drawing, icon, photo, picture, or text
310-
"useSharpYuv": true, // Use sharp (and slow) RGB->YUV conversion.
311-
}
312-
313-
// Override with per-image options if provided.
314-
for _, key := range []string{"quality", "hint"} {
315-
if v, ok := options[key]; ok {
316-
opts[key] = v
317-
}
318-
}
296+
opts = maps.Clone(opts)
297+
opts["useSharpYuv"] = true // Use sharp (and slow) RGB->YUV conversion.
319298

320299
message := Message[WebpInput]{
321300
Header: Header{

‎resources/images/codec.go‎

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -37,35 +37,27 @@ type Decoder interface {
3737
DecodeConfig(r io.Reader) (image.Config, error)
3838
}
3939

40-
// Encoder defines the encoding of an image format.
41-
type Encoder interface {
42-
Encode(w io.Writer, src image.Image) error
43-
}
44-
4540
type ToEncoder interface {
4641
EncodeTo(conf ImageConfig, w io.Writer, src image.Image) error
4742
}
4843

49-
// EncoderWithOptions defines the encoding of an image format with options.
50-
// This is currently only used for WebP and the options are passed as a map
51-
// to match the internal WASM API. The options map may be nil when no options
52-
// need to be overridden.
53-
type EncoderWithOptions interface {
54-
EncodeOptions(w io.Writer, src image.Image, options map[string]any) error
44+
// EncoderWithOptions defines the encoding of an image format with the given options.
45+
type Encoder interface {
46+
Encode(w io.Writer, src image.Image, options map[string]any) error
5547
}
5648

57-
// CodecStdlib defines both decoding and encoding of an image format as defined by the standard library.
58-
type CodecStdlib interface {
49+
// EncodeDecoder defines both decoding and encoding of an image format as defined by the standard library.
50+
type EncodeDecoder interface {
5951
Decoder
6052
Encoder
6153
}
6254

6355
// Codec is a generic image codec supporting multiple formats.
6456
type Codec struct {
65-
webp CodecStdlib
57+
webp EncodeDecoder
6658
}
6759

68-
func newCodec(webp CodecStdlib) *Codec {
60+
func newCodec(webp EncodeDecoder) *Codec {
6961
return &Codec{webp: webp}
7062
}
7163

@@ -131,20 +123,12 @@ func (d *Codec) EncodeTo(conf ImageConfig, w io.Writer, img image.Image) error {
131123
case BMP:
132124
return bmp.Encode(w, img)
133125
case WEBP:
134-
if enc, ok := d.webp.(EncoderWithOptions); ok {
135-
var opts map[string]any
136-
if conf.qualitySetForImage || conf.hintSetForImage {
137-
opts = make(map[string]any)
138-
if conf.qualitySetForImage {
139-
opts["quality"] = conf.Quality
140-
}
141-
if conf.hintSetForImage {
142-
opts["hint"] = conf.Hint
143-
}
144-
}
145-
return enc.EncodeOptions(w, img, opts)
126+
opts := map[string]any{
127+
"compression": conf.Compression,
128+
"quality": conf.Quality,
129+
"hint": conf.Hint,
146130
}
147-
return d.webp.Encode(w, img)
131+
return d.webp.Encode(w, img, opts)
148132
default:
149133
return errors.New("format not supported")
150134
}

‎resources/images/config.go‎

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ var anchorPositions = map[string]gift.Anchor{
8585
smartCropIdentifier: SmartCropAnchor,
8686
}
8787

88+
var compressionMethods = map[string]bool{
89+
"lossy": true,
90+
"lossless": true,
91+
}
92+
8893
// These encoding hints are currently only relevant for Webp.
8994
var hints = map[string]bool{
9095
"picture": true,
@@ -127,6 +132,7 @@ const (
127132
defaultResampleFilter = "box"
128133
defaultBgColor = "#ffffff"
129134
defaultHint = "photo"
135+
defaultCompression = "lossy"
130136
)
131137

132138
var (
@@ -135,6 +141,7 @@ var (
135141
"bgColor": defaultBgColor,
136142
"hint": defaultHint,
137143
"quality": defaultJPEGQuality,
144+
"compression": defaultCompression,
138145
}
139146

140147
defaultImageConfig *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]
@@ -226,7 +233,8 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin
226233
c.Filter = filter
227234
} else if _, ok := hints[part]; ok {
228235
c.Hint = part
229-
c.hintSetForImage = true
236+
} else if _, ok := compressionMethods[part]; ok {
237+
c.Compression = part
230238
} else if part[0] == '#' {
231239
c.BgColor, err = hexStringToColorGo(part[1:])
232240
if err != nil {
@@ -237,10 +245,9 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin
237245
if err != nil {
238246
return c, err
239247
}
240-
if c.Quality < 0 || c.Quality > 100 {
241-
return c, errors.New("quality ranges from 0 to 100 inclusive")
248+
if c.Quality < 1 || c.Quality > 100 {
249+
return c, errors.New("quality ranges from 1 to 100 inclusive")
242250
}
243-
c.qualitySetForImage = true
244251
} else if part[0] == 'r' {
245252
c.Rotate, err = strconv.Atoi(part[1:])
246253
if err != nil {
@@ -306,12 +313,16 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin
306313
c.TargetFormat = sourceFormat
307314
}
308315

309-
if !c.qualitySetForImage && c.Quality <= 0 && c.TargetFormat.RequiresDefaultQuality() {
316+
if c.Quality <= 0 && c.TargetFormat.RequiresDefaultQuality() {
310317
// We need a quality setting for all JPEGs and WEBPs,
311-
// unless the user explicitly set quality (e.g., q0 for lossless WebP).
318+
// unless the user explicitly set quality.
312319
c.Quality = defaults.Config.Imaging.Quality
313320
}
314321

322+
if c.Compression == "" {
323+
c.Compression = defaults.Config.Imaging.Compression
324+
}
325+
315326
if c.BgColor == nil && c.TargetFormat != sourceFormat {
316327
if sourceFormat.SupportsTransparency() && !c.TargetFormat.SupportsTransparency() {
317328
c.BgColor = defaults.Config.BgColor
@@ -341,11 +352,11 @@ type ImageConfig struct {
341352
// If set, this will be used as the key in filenames etc.
342353
Key string
343354

344-
// Quality ranges from 1 to 100 inclusive, higher is better.
355+
// Quality ranges from 0 to 100 inclusive, higher is better.
345356
// This is only relevant for JPEG and WEBP images.
357+
// For WebP, 0 means lossless.
346358
// Default is 75.
347-
Quality int
348-
qualitySetForImage bool // Whether the above is set for this image.
359+
Quality int
349360

350361
// Rotate rotates an image by the given angle counter-clockwise.
351362
// The rotation will be performed first.
@@ -360,8 +371,9 @@ type ImageConfig struct {
360371

361372
// Hint about what type of picture this is. Used to optimize encoding
362373
// when target is set to webp.
363-
Hint string
364-
hintSetForImage bool // Whether the above is set for this image.
374+
Hint string
375+
376+
Compression string
365377

366378
Width int
367379
Height int
@@ -415,6 +427,11 @@ type ImagingConfig struct {
415427
// Default image quality setting (1-100). Only used for JPEG and WebP images.
416428
Quality int
417429

430+
// Compression method to use.
431+
// One of "lossy" or "lossless".
432+
// Note that lossless is currently only supported for WebP.
433+
Compression string
434+
418435
// Resample filter to use in resize operations.
419436
ResampleFilter string
420437

@@ -434,14 +451,15 @@ type ImagingConfig struct {
434451
}
435452

436453
func (cfg *ImagingConfig) init() error {
437-
if cfg.Quality < 0 || cfg.Quality > 100 {
454+
if cfg.Quality < 1 || cfg.Quality > 100 {
438455
return errors.New("image quality must be a number between 1 and 100")
439456
}
440457

441458
cfg.BgColor = strings.ToLower(strings.TrimPrefix(cfg.BgColor, "#"))
442459
cfg.Anchor = strings.ToLower(cfg.Anchor)
443460
cfg.ResampleFilter = strings.ToLower(cfg.ResampleFilter)
444461
cfg.Hint = strings.ToLower(cfg.Hint)
462+
cfg.Compression = strings.ToLower(cfg.Compression)
445463

446464
if cfg.Anchor == "" {
447465
cfg.Anchor = smartCropIdentifier

‎resources/images/config_test.go‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ func newImageConfig(action string, width, height, quality, rotate int, filter, a
133133
c.Width = width
134134
c.Height = height
135135
c.Quality = quality
136-
c.qualitySetForImage = quality != 75
137136
c.Rotate = rotate
138137
c.BgColor, _ = hexStringToColorGo(bgColor)
139138
c.Anchor = SmartCropAnchor

‎resources/images/image.go‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ func NewImageProcessor(warnl logg.LevelLogger, wasmDispatchers *warpc.Dispatcher
137137
return nil, err
138138
}
139139

140-
webpCodec, err := wasmDispatchers.NewWepCodec(cfg.Config.Imaging.Quality, cfg.Config.Imaging.Hint)
140+
webpCodec, err := wasmDispatchers.NewWepCodec()
141141
if err != nil {
142142
return nil, err
143143
}
@@ -300,9 +300,10 @@ func GetDefaultImageConfig(defaults *config.ConfigNamespace[ImagingConfig, Imagi
300300
defaults = defaultImageConfig
301301
}
302302
return ImageConfig{
303-
Anchor: -1, // The real values start at 0.
304-
Hint: "photo",
305-
Quality: defaults.Config.Imaging.Quality,
303+
Anchor: -1, // The real values start at 0.
304+
Hint: "photo",
305+
Quality: defaults.Config.Imaging.Quality,
306+
Compression: defaults.Config.Imaging.Compression,
306307
}
307308
}
308309

0 commit comments

Comments
 (0)