diff --git a/internal/warpc/avif_integration_test.go b/internal/warpc/avif_integration_test.go index 3b3b4f907..7c0b3e0fa 100644 --- a/internal/warpc/avif_integration_test.go +++ b/internal/warpc/avif_integration_test.go @@ -67,6 +67,26 @@ gif:{{ $gif.RelPermalink }} AssertFrameDurations(durations) } +// See issue 14987. +func TestAvifEncodeHintSubsampling(t *testing.T) { + files := ` +-- hugo.toml -- +-- assets/logo.png -- +sourcefilename: ../../resources/testdata/gohugoio24.png +-- layouts/home.html -- +{{ $img := resources.Get "logo.png" }} +default:{{ ($img.Process "avif photo").RelPermalink }}| +photo:{{ ($img.Process "avif photo").RelPermalink }}| +text:{{ ($img.Process "avif text").RelPermalink }}| +` + + b := hugolib.Test(t, files) + b.AssertFileContent("public/index.html", + "default:/logo_hu_add3d7dfc55b7f80.avif|", + "photo:/logo_hu_add3d7dfc55b7f80.avif|", + "text:/logo_hu_b7080fe2a2e2a664.avif|") +} + // See issue 14985. func TestAvifEncodeOutOfMemory(t *testing.T) { files := ` diff --git a/internal/warpc/genavif/avif.c b/internal/warpc/genavif/avif.c index 3932f7fcb..6096df0f7 100644 --- a/internal/warpc/genavif/avif.c +++ b/internal/warpc/genavif/avif.c @@ -50,6 +50,7 @@ typedef struct float quality; // between 1 and 100. char compression[32]; // "lossy" or "lossless" int encoderSpeed; // 1 (slowest, best) to 10 (fastest). 0 means use default. + char hint[64]; // drawing, icon, photo, picture, or text. Selects chroma subsampling. } InputOptions; typedef struct @@ -165,6 +166,12 @@ InputMessage parse_input_message(const char *line) 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) + { + strncpy(msg.data.options.hint, hint_str, sizeof(msg.data.options.hint) - 1); + msg.data.options.hint[sizeof(msg.data.options.hint) - 1] = '\0'; + } } } @@ -260,6 +267,19 @@ static void drain_bytes(FILE *stream, size_t n) } } +// avifFormatForHint maps a content hint to a chroma subsampling format. +// Photographic content tolerates 4:2:0, which roughly halves the encoder's +// memory footprint and the output size. Sharp-edged content (text, icons, line +// art) keeps full 4:4:4 chroma. See issue 14987. +static avifPixelFormat avifFormatForHint(const char *hint) +{ + if (strcmp(hint, "drawing") == 0 || strcmp(hint, "icon") == 0 || strcmp(hint, "text") == 0) + { + return AVIF_PIXEL_FORMAT_YUV444; + } + return AVIF_PIXEL_FORMAT_YUV420; // photo, picture, and the default. +} + void handle_commands(FILE *stream) { @@ -620,8 +640,15 @@ void handle_commands(FILE *stream) goto cleanup; } + // Pick chroma subsampling from the content hint. Lossless keeps 4:4:4, + // since subsampling discards chroma and would defeat it. + avifPixelFormat yuvFormat = avifFormatForHint(input.data.options.hint); + if (strcmp(compression, "lossless") == 0) { + yuvFormat = AVIF_PIXEL_FORMAT_YUV444; + } + // Create image with the target bit depth for encoding. - avifImage *image = avifImageCreate(width, height, depth, AVIF_PIXEL_FORMAT_YUV444); + avifImage *image = avifImageCreate(width, height, depth, yuvFormat); if (!image) { snprintf(output.header.err, sizeof(output.header.err), "encodeNRGBA: Failed to create avifImage"); write_output_message(&output); diff --git a/internal/warpc/wasm/avif.wasm b/internal/warpc/wasm/avif.wasm index 9968def00..3d446b320 100755 Binary files a/internal/warpc/wasm/avif.wasm and b/internal/warpc/wasm/avif.wasm differ diff --git a/resources/images/codec.go b/resources/images/codec.go index c30c9d6ea..eec1cfcc5 100644 --- a/resources/images/codec.go +++ b/resources/images/codec.go @@ -144,6 +144,7 @@ func (d *Codec) EncodeTo(conf ImageConfig, w io.Writer, img image.Image) error { "compression": conf.Compression, "quality": conf.Quality, "encoderSpeed": conf.EncoderSpeed, + "hint": conf.Hint, } return d.avif.Encode(w, img, opts) case WEBP: diff --git a/resources/images/config.go b/resources/images/config.go index 4a7cc9fbe..f56c40930 100644 --- a/resources/images/config.go +++ b/resources/images/config.go @@ -104,7 +104,7 @@ var compressionMethods = map[string]bool{ "lossless": true, } -// These encoding hints are currently only relevant for Webp. +// These encoding hints are used by Webp (preset) and Avif (chroma subsampling). var hints = map[string]bool{ "picture": true, "photo": true, @@ -429,7 +429,7 @@ type ImageConfig struct { BgColor color.Color // Hint about what type of picture this is. Used to optimize encoding - // when target is set to webp. + // when target is webp (preset) or avif (chroma subsampling). Hint string Compression string @@ -510,7 +510,7 @@ type ImagingConfig struct { ResampleFilter string // Hint about what type of image this is. - // Currently only used when encoding to Webp. + // Used when encoding to Webp (preset) and Avif (chroma subsampling). // Default is "photo". // Valid values are "picture", "photo", "drawing", "icon", or "text". // Moved to WebpConfig in v0.155.0, but kept here for backwards compatibility. diff --git a/resources/images/testdata/images_golden/process/avif/avif-q79.avif b/resources/images/testdata/images_golden/process/avif/avif-q79.avif index 83b24d1d2..9c22243d0 100644 Binary files a/resources/images/testdata/images_golden/process/avif/avif-q79.avif and b/resources/images/testdata/images_golden/process/avif/avif-q79.avif differ diff --git a/resources/images/testdata/images_golden/process/avif/avif-q80.avif b/resources/images/testdata/images_golden/process/avif/avif-q80.avif index a996ecbd9..6a3f36a46 100644 Binary files a/resources/images/testdata/images_golden/process/avif/avif-q80.avif and b/resources/images/testdata/images_golden/process/avif/avif-q80.avif differ diff --git a/resources/images/testdata/images_golden/process/avif/avif.avif b/resources/images/testdata/images_golden/process/avif/avif.avif index db3656528..aae9aeb36 100644 Binary files a/resources/images/testdata/images_golden/process/avif/avif.avif and b/resources/images/testdata/images_golden/process/avif/avif.avif differ diff --git a/resources/images/testdata/images_golden/process/avif/crop-300x300-smart-avif.avif b/resources/images/testdata/images_golden/process/avif/crop-300x300-smart-avif.avif index 66e75b055..38e35dd6b 100644 Binary files a/resources/images/testdata/images_golden/process/avif/crop-300x300-smart-avif.avif and b/resources/images/testdata/images_golden/process/avif/crop-300x300-smart-avif.avif differ diff --git a/resources/images/testdata/images_golden/process/avif/crop-300x300-smart-ff9999-avif.avif b/resources/images/testdata/images_golden/process/avif/crop-300x300-smart-ff9999-avif.avif index 2f0afd70c..2e8adcf0a 100644 Binary files a/resources/images/testdata/images_golden/process/avif/crop-300x300-smart-ff9999-avif.avif and b/resources/images/testdata/images_golden/process/avif/crop-300x300-smart-ff9999-avif.avif differ diff --git a/resources/images/testdata/images_golden/process/avif/q50.avif b/resources/images/testdata/images_golden/process/avif/q50.avif index af2faeef3..f63b929e1 100644 Binary files a/resources/images/testdata/images_golden/process/avif/q50.avif and b/resources/images/testdata/images_golden/process/avif/q50.avif differ diff --git a/resources/images/testdata/images_golden/process/avif/r1.avif b/resources/images/testdata/images_golden/process/avif/r1.avif index 660e91d79..21612e572 100644 Binary files a/resources/images/testdata/images_golden/process/avif/r1.avif and b/resources/images/testdata/images_golden/process/avif/r1.avif differ diff --git a/resources/images/testdata/images_golden/process/avif/r2.avif b/resources/images/testdata/images_golden/process/avif/r2.avif index d4c46f2fd..5b476bc8e 100644 Binary files a/resources/images/testdata/images_golden/process/avif/r2.avif and b/resources/images/testdata/images_golden/process/avif/r2.avif differ diff --git a/resources/images/testdata/images_golden/process/avifstraws/resize-900x.avif b/resources/images/testdata/images_golden/process/avifstraws/resize-900x.avif index a728de049..2adca4dc9 100644 Binary files a/resources/images/testdata/images_golden/process/avifstraws/resize-900x.avif and b/resources/images/testdata/images_golden/process/avifstraws/resize-900x.avif differ