Add image processing support for AVIF
The encode/decode is implemented in a WebAssembly module built from a small C wrapper around libavif. Bundled libraries (statically linked, compiled with the WASI SDK): * libavif v1.4.1 (container + codec glue) * libaom v3.14.1 (AV1 encoder + decoder) * dav1d 1.5.3 (AV1 decoder) * libyuv (Chromium pin) for color conversion * parson for JSON message passing across the wasm boundary HDR handling on the encoder: * SDR images are written as BT.709 / sRGB / BT.601 (8-bit). * 10-bit and up are written as BT.2020 primaries with PQ (SMPTE ST 2084) transfer and BT.2020-NCL matrix coefficients, signalled via CICP. * Adobe-style SDR+gainmap inputs (e.g. Lightroom HDR exports) are baked into a single true-HDR image in BT.2020/PQ at 10-bit, with the CLLI (Content Light Level Information) box carried through so HDR-capable clients can tone-map correctly. Limitations: * Animated input (animated WebP/GIF) is collapsed to its first frame when re-encoded as AVIF; animated AVIF output is not yet supported. Fixes #7837
@@ -15,6 +15,8 @@ package images
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
@@ -60,11 +62,12 @@ type EncodeDecoder interface {
|
||||
// Codec is a generic image codec supporting multiple formats.
|
||||
type Codec struct {
|
||||
webp EncodeDecoder
|
||||
avif EncodeDecoder
|
||||
debugl logg.LevelLogger
|
||||
}
|
||||
|
||||
func newCodec(webp EncodeDecoder, debugl logg.LevelLogger) *Codec {
|
||||
return &Codec{webp: webp, debugl: debugl}
|
||||
func newCodec(webp, avif EncodeDecoder, debugl logg.LevelLogger) *Codec {
|
||||
return &Codec{webp: webp, avif: avif, debugl: debugl}
|
||||
}
|
||||
|
||||
func (d *Codec) EncodeTo(conf ImageConfig, w io.Writer, img image.Image) error {
|
||||
@@ -136,6 +139,13 @@ func (d *Codec) EncodeTo(conf ImageConfig, w io.Writer, img image.Image) error {
|
||||
return tiff.Encode(w, img, &tiff.Options{Compression: tiff.Deflate, Predictor: true})
|
||||
case BMP:
|
||||
return bmp.Encode(w, img)
|
||||
case AVIF:
|
||||
opts := map[string]any{
|
||||
"compression": conf.Compression,
|
||||
"quality": conf.Quality,
|
||||
"encoderSpeed": conf.EncoderSpeed,
|
||||
}
|
||||
return d.avif.Encode(w, img, opts)
|
||||
case WEBP:
|
||||
// Convert bool to int because the C code reads it as a number.
|
||||
useSharpYuvInt := 0
|
||||
@@ -151,7 +161,7 @@ func (d *Codec) EncodeTo(conf ImageConfig, w io.Writer, img image.Image) error {
|
||||
}
|
||||
return d.webp.Encode(w, img, opts)
|
||||
default:
|
||||
return errors.New("format not supported")
|
||||
return fmt.Errorf("format %q not supported", conf.TargetFormat)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +195,8 @@ func (d *Codec) DecodeFormat(f Format, r io.Reader) (image.Image, error) {
|
||||
return tiff.Decode(r)
|
||||
case BMP:
|
||||
return bmp.Decode(r)
|
||||
case AVIF:
|
||||
return d.avif.Decode(r)
|
||||
case WEBP:
|
||||
img, err := d.webp.Decode(r)
|
||||
if err == nil {
|
||||
@@ -296,6 +308,12 @@ const (
|
||||
magicGif = "GIF8???"
|
||||
)
|
||||
|
||||
var (
|
||||
avifBrandAvis = []byte("avis")
|
||||
avifBrandAvif = []byte("avif")
|
||||
avifFtyp = []byte("ftyp")
|
||||
)
|
||||
|
||||
type magicFormat struct {
|
||||
magic string
|
||||
format Format
|
||||
@@ -308,7 +326,7 @@ var magicFormats = []magicFormat{
|
||||
|
||||
// formatFromImage determines the image format from the magic bytes.
|
||||
// Note that this is only a partial implementation,
|
||||
// as we currently only need WebP and GIF detection.
|
||||
// as we currently only need WebP, GIF and AVIF detection.
|
||||
// The others can be handled by the standard library.
|
||||
func formatFromImage(r peekReader) (Format, error) {
|
||||
for _, mf := range magicFormats {
|
||||
@@ -318,9 +336,39 @@ func formatFromImage(r peekReader) (Format, error) {
|
||||
return mf.format, nil
|
||||
}
|
||||
}
|
||||
if isAvif(r) {
|
||||
return AVIF, nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// isAvif reports whether r begins with an ISOBMFF ftyp box that identifies
|
||||
// the file as AVIF, either via the major brand ("avif"/"avis") or the
|
||||
// compatible brands list (e.g. major "mif1" with "avif" as a compatible brand).
|
||||
func isAvif(r peekReader) bool {
|
||||
b, _ := r.Peek(12)
|
||||
if len(b) < 12 || !bytes.Equal(b[4:8], avifFtyp) {
|
||||
return false
|
||||
}
|
||||
if brand := b[8:12]; bytes.Equal(brand, avifBrandAvif) || bytes.Equal(brand, avifBrandAvis) {
|
||||
return true
|
||||
}
|
||||
b, _ = r.Peek(64)
|
||||
if len(b) < 16 {
|
||||
return false
|
||||
}
|
||||
end := int(binary.BigEndian.Uint32(b[:4]))
|
||||
if end < 16 || end > len(b) {
|
||||
end = len(b)
|
||||
}
|
||||
for i := 16; i+4 <= end; i += 4 {
|
||||
if brand := b[i : i+4]; bytes.Equal(brand, avifBrandAvif) || bytes.Equal(brand, avifBrandAvis) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func match(magic string, b []byte) bool {
|
||||
if len(magic) != len(b) {
|
||||
return false
|
||||
|
||||
@@ -71,6 +71,7 @@ var (
|
||||
media.Builtin.BMPType.SubType: BMP,
|
||||
media.Builtin.GIFType.SubType: GIF,
|
||||
media.Builtin.WEBPType.SubType: WEBP,
|
||||
media.Builtin.AVIFType.SubType: AVIF,
|
||||
}
|
||||
|
||||
// We cannot process these formats, but we can provide metadata support for them (including width/height).
|
||||
@@ -161,13 +162,14 @@ func ImageFormatFromMediaSubType(sub string) (Format, ImageResourceType) {
|
||||
}
|
||||
|
||||
const (
|
||||
defaultJPEGQuality = 75
|
||||
defaultResampleFilter = "box"
|
||||
defaultBgColor = "#ffffff"
|
||||
defaultHint = "photo"
|
||||
defaultCompression = "lossy"
|
||||
defaultWebpUseSharpYuv = false
|
||||
defaultWebpMethod = 2
|
||||
defaultJPEGQuality = 75
|
||||
defaultResampleFilter = "box"
|
||||
defaultBgColor = "#ffffff"
|
||||
defaultHint = "photo"
|
||||
defaultCompression = "lossy"
|
||||
defaultWebpUseSharpYuv = false
|
||||
defaultWebpMethod = 2
|
||||
defaultAvifEncoderSpeed = 10
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -181,6 +183,9 @@ var (
|
||||
"useSharpYuv": defaultWebpUseSharpYuv,
|
||||
"method": defaultWebpMethod,
|
||||
},
|
||||
"avif": map[string]any{
|
||||
"encoderSpeed": defaultAvifEncoderSpeed,
|
||||
},
|
||||
}
|
||||
|
||||
defaultImageConfig *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]
|
||||
@@ -212,6 +217,11 @@ func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, Ima
|
||||
hmaps.MergeShallow(webp, defaultImaging["webp"].(map[string]any))
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -279,6 +289,8 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin
|
||||
c.Hint = part
|
||||
} else if _, ok := compressionMethods[part]; ok {
|
||||
c.Compression = part
|
||||
} else if f, ok := ImageFormatFromExt("." + part); ok {
|
||||
c.TargetFormat = f
|
||||
} else if part[0] == '#' {
|
||||
c.BgColor, err = hexStringToColorGo(part[1:])
|
||||
if err != nil {
|
||||
@@ -320,8 +332,6 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin
|
||||
} else {
|
||||
return c, errors.New("invalid image dimensions")
|
||||
}
|
||||
} else if f, ok := ImageFormatFromExt("." + part); ok {
|
||||
c.TargetFormat = f
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,6 +434,9 @@ type ImageConfig struct {
|
||||
UseSharpYuv bool
|
||||
Method int
|
||||
|
||||
// AVIF-specific options.
|
||||
EncoderSpeed int
|
||||
|
||||
Width int
|
||||
Height int
|
||||
|
||||
@@ -478,7 +491,7 @@ type ImagingConfig struct {
|
||||
|
||||
// Compression method to use.
|
||||
// One of "lossy" or "lossless".
|
||||
// Note that lossless is currently only supported for WebP.
|
||||
// Note that lossless is currently only supported for WebP and AVIF.
|
||||
Compression string
|
||||
|
||||
// Resample filter to use in resize operations.
|
||||
@@ -500,6 +513,7 @@ type ImagingConfig struct {
|
||||
Exif ExifConfig
|
||||
Meta MetaConfig
|
||||
Webp WebpConfig
|
||||
Avif AvifConfig
|
||||
}
|
||||
|
||||
var validMetaSources = map[string]bool{
|
||||
@@ -571,6 +585,13 @@ func (cfg *ImagingConfig) init() error {
|
||||
return fmt.Errorf("webp method must be between 0 and 6, got %d", cfg.Webp.Method)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -610,6 +631,19 @@ type MetaConfig struct {
|
||||
Sources []string
|
||||
}
|
||||
|
||||
// AvifConfig holds AVIF-specific encoding configuration.
|
||||
type AvifConfig struct {
|
||||
// Encoder quality/speed trade-off, 1 (slowest, best quality / smallest
|
||||
// files) to 10 (fastest). Default is 10 — fast enough for incremental
|
||||
// builds with quality indistinguishable from slower settings at typical
|
||||
// web thumbnail sizes. Lower values reduce file size at the cost of
|
||||
// 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
|
||||
}
|
||||
|
||||
// WebpConfig holds WebP-specific encoding configuration.
|
||||
type WebpConfig struct {
|
||||
// Hint about what type of image this is.
|
||||
|
||||
@@ -76,6 +76,37 @@ func TestDecodeConfig(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
func TestDecodeImageConfig(t *testing.T) {
|
||||
|
||||
@@ -99,7 +99,7 @@ func (i *Image) InitConfig(r io.Reader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (i *Image) initConfig() error {
|
||||
func (i *Image) initConfig() {
|
||||
var err error
|
||||
i.configInit.Do(func() {
|
||||
if i.configLoaded {
|
||||
@@ -118,10 +118,8 @@ func (i *Image) initConfig() error {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load image config: %w", err)
|
||||
panic(fmt.Errorf("failed to load image config: %w", err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewImageProcessor(debugl, warnl logg.LevelLogger, wasmDispatchers *warpc.Dispatchers, cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) (*ImageProcessor, error) {
|
||||
@@ -154,7 +152,11 @@ func NewImageProcessor(debugl, warnl logg.LevelLogger, wasmDispatchers *warpc.Di
|
||||
if webpCodec == nil {
|
||||
return nil, errors.New("webp codec is not available")
|
||||
}
|
||||
imageCodec := newCodec(webpCodec, debugl)
|
||||
avifCodec, err := wasmDispatchers.NewAvifCodec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imageCodec := newCodec(webpCodec, avifCodec, debugl)
|
||||
|
||||
return &ImageProcessor{
|
||||
Cfg: cfg,
|
||||
@@ -256,7 +258,12 @@ func (p *ImageProcessor) resolveSrc(src image.Image, targetFormat Format) image.
|
||||
if animatedImage, ok := src.(himage.AnimatedImage); ok {
|
||||
frames := animatedImage.GetFrames()
|
||||
// If e.g. converting an animated GIF to JPEG, we only want the first frame.
|
||||
if len(frames) < 2 || !targetFormat.SupportsAnimation() {
|
||||
// Preserve the AnimatedImage wrapper for color properties only when there is
|
||||
// no actual animation to preserve. Multi-frame animated sources must still be
|
||||
// collapsed unless the target format supports animation output.
|
||||
shouldExtractFirstFrame := len(frames) < 2 || !targetFormat.SupportsAnimation()
|
||||
colorPropsNeedPreserving := himage.HasColorProperties(src) && len(frames) < 2
|
||||
if shouldExtractFirstFrame && !colorPropsNeedPreserving {
|
||||
src = frames[0]
|
||||
}
|
||||
}
|
||||
@@ -268,20 +275,36 @@ func (p *ImageProcessor) doFilter(src image.Image, targetFormat Format, filters
|
||||
|
||||
if anim, ok := src.(himage.AnimatedImage); ok {
|
||||
frames := anim.GetFrames()
|
||||
if len(frames) < 2 || !targetFormat.SupportsAnimation() {
|
||||
// Check if we should extract first frame or preserve AnimatedImage wrapper.
|
||||
// Preserve AnimatedImage if color properties need to be maintained.
|
||||
shouldExtractFirstFrame := len(frames) < 2 || !targetFormat.SupportsAnimation()
|
||||
colorPropsNeedPreserving := himage.HasColorProperties(src) && len(frames) < 2
|
||||
if shouldExtractFirstFrame && !colorPropsNeedPreserving {
|
||||
src = frames[0]
|
||||
} else {
|
||||
var bounds image.Rectangle
|
||||
firstFrame := frames[0]
|
||||
tmp := image.NewNRGBA(firstFrame.Bounds())
|
||||
// Preserve bit depth for HDR images.
|
||||
var tmp draw.Image
|
||||
switch firstFrame.(type) {
|
||||
case *image.RGBA64, *image.NRGBA64:
|
||||
tmp = image.NewNRGBA64(firstFrame.Bounds())
|
||||
default:
|
||||
tmp = image.NewNRGBA(firstFrame.Bounds())
|
||||
}
|
||||
for i, frame := range frames {
|
||||
gift.New().DrawAt(tmp, frame, frame.Bounds().Min, gift.OverOperator)
|
||||
bounds = filter.Bounds(tmp.Bounds())
|
||||
var dst draw.Image
|
||||
if paletted, ok := frame.(*image.Paletted); ok {
|
||||
switch f := frame.(type) {
|
||||
case *image.Paletted:
|
||||
// Gif.
|
||||
dst = image.NewPaletted(bounds, paletted.Palette)
|
||||
} else {
|
||||
dst = image.NewPaletted(bounds, f.Palette)
|
||||
case *image.RGBA64:
|
||||
dst = image.NewRGBA64(bounds)
|
||||
case *image.NRGBA64:
|
||||
dst = image.NewNRGBA64(bounds)
|
||||
default:
|
||||
dst = image.NewNRGBA(bounds)
|
||||
}
|
||||
filter.Draw(dst, tmp)
|
||||
@@ -305,6 +328,12 @@ func (p *ImageProcessor) doFilter(src image.Image, targetFormat Format, filters
|
||||
dst = image.NewNRGBA(bounds)
|
||||
case *image.Gray:
|
||||
dst = image.NewGray(bounds)
|
||||
case *image.RGBA64:
|
||||
dst = image.NewRGBA64(bounds)
|
||||
case *image.NRGBA64:
|
||||
dst = image.NewNRGBA64(bounds)
|
||||
case *image.Gray16:
|
||||
dst = image.NewGray16(bounds)
|
||||
default:
|
||||
dst = image.NewNRGBA(bounds)
|
||||
}
|
||||
@@ -318,12 +347,13 @@ func GetDefaultImageConfig(defaults *config.ConfigNamespace[ImagingConfig, Imagi
|
||||
defaults = defaultImageConfig
|
||||
}
|
||||
return ImageConfig{
|
||||
Anchor: -1, // The real values start at 0.
|
||||
Hint: defaults.Config.Imaging.Webp.Hint,
|
||||
Quality: defaults.Config.Imaging.Quality,
|
||||
Compression: defaults.Config.Imaging.Compression,
|
||||
UseSharpYuv: defaults.Config.Imaging.Webp.UseSharpYuv,
|
||||
Method: defaults.Config.Imaging.Webp.Method,
|
||||
Anchor: -1, // The real values start at 0.
|
||||
Hint: defaults.Config.Imaging.Webp.Hint,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,9 +372,9 @@ const (
|
||||
TIFF
|
||||
BMP
|
||||
WEBP
|
||||
AVIF
|
||||
|
||||
// Below: We have no encoder/decoder for these, but we can provide metadata support for them (including width/height).
|
||||
AVIF
|
||||
HEIF
|
||||
HEIC
|
||||
)
|
||||
@@ -387,6 +417,9 @@ func (f Format) SupportsTransparency() bool {
|
||||
}
|
||||
|
||||
// SupportsAnimation reports whether the format supports animation.
|
||||
// AVIF supports animation in the spec, but Hugo's AVIF encoder is single-frame
|
||||
// only — including AVIF here would make the pipeline preserve frames just to
|
||||
// drop them at encode time.
|
||||
func (f Format) SupportsAnimation() bool {
|
||||
return f == GIF || f == WEBP
|
||||
}
|
||||
|
||||
@@ -336,6 +336,91 @@ Home.
|
||||
{{ template "process" (dict "spec" "resize 300x300 jpg #b31280" "img" $gopher) }}
|
||||
|
||||
|
||||
` + goldenProcess
|
||||
|
||||
opts := imagetesting.DefaultGoldenOpts
|
||||
opts.T = t
|
||||
opts.Name = name
|
||||
opts.Files = files
|
||||
|
||||
imagetesting.RunGolden(opts)
|
||||
}
|
||||
|
||||
func TestImagesGoldenProcessAvif(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if imagetesting.SkipGoldenTests {
|
||||
t.Skip("Skip golden test on this architecture")
|
||||
}
|
||||
|
||||
// Will be used as the base folder for generated images.
|
||||
name := "process/avif"
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- assets/anim.webp --
|
||||
sourcefilename: ../testdata/webp/anim.webp
|
||||
-- assets/dock.avif --
|
||||
sourcefilename: ../testdata/bep/dock-75-hdr.avif
|
||||
-- assets/sunset420.avif --
|
||||
sourcefilename: ../testdata/sunset_420.avif
|
||||
-- assets/sunset.jpg --
|
||||
sourcefilename: ../testdata/sunset.jpg
|
||||
-- assets/giphy.avif --
|
||||
sourcefilename: ../testdata/giphy.avif
|
||||
-- assets/fuzzycircle.webp --
|
||||
sourcefilename: ../testdata/webp/fuzzy-cirlcle-transparent-32.webp
|
||||
-- layouts/home.html --
|
||||
Home.
|
||||
{{ $sunset := resources.Get "sunset.jpg" }}
|
||||
{{ $dock := resources.Get "dock.avif" }}
|
||||
{{ $sunset420 := resources.Get "sunset420.avif" }}
|
||||
{{ $webpAnim := resources.Get "anim.webp" }}
|
||||
{{ $giphy := resources.Get "giphy.avif" }}
|
||||
{{ $fuzzyCircle := resources.Get "fuzzycircle.webp" }}
|
||||
|
||||
{{ template "process" (dict "spec" "r1" "img" $dock) }}
|
||||
{{ template "process" (dict "spec" "q50" "img" $dock) }}
|
||||
{{ template "process" (dict "spec" "r2" "img" $sunset420) }}
|
||||
{{ template "process" (dict "spec" "avif" "img" $webpAnim) }}
|
||||
{{ template "process" (dict "spec" "gif" "img" $giphy) }}
|
||||
{{ template "process" (dict "spec" "crop 300x300 smart avif" "img" $fuzzyCircle) }}
|
||||
{{ template "process" (dict "spec" "crop 300x300 smart #ff9999 avif" "img" $fuzzyCircle) }}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
` + goldenProcess
|
||||
|
||||
opts := imagetesting.DefaultGoldenOpts
|
||||
opts.T = t
|
||||
opts.Name = name
|
||||
opts.Files = files
|
||||
|
||||
imagetesting.RunGolden(opts)
|
||||
}
|
||||
|
||||
func TestImagesGoldenProcessAviStraws(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if imagetesting.SkipGoldenTests {
|
||||
t.Skip("Skip golden test on this architecture")
|
||||
}
|
||||
|
||||
// Will be used as the base folder for generated images.
|
||||
name := "process/avifstraws"
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- assets/straws.avif --
|
||||
sourcefilename: ../testdata/bep/straws.avif
|
||||
-- layouts/home.html --
|
||||
Home.
|
||||
{{ $straws := resources.Get "straws.avif" }}
|
||||
{{ template "process" (dict "spec" "resize 900x" "img" $straws) }}
|
||||
|
||||
|
||||
` + goldenProcess
|
||||
|
||||
opts := imagetesting.DefaultGoldenOpts
|
||||
|
||||
@@ -294,7 +294,7 @@ $ic.Width/Height: 900x562
|
||||
|
||||
IsImageResource AVIF: true
|
||||
IsImageResourceWithMeta AVIF: true
|
||||
IsImageResourceProcessable AVIF: false
|
||||
IsImageResourceProcessable AVIF: true
|
||||
Has width AVIF: true
|
||||
Has meta AVIF: true
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 83 KiB |
@@ -0,0 +1 @@
|
||||
All images in this folder are copyrighted Bjørn Erik Pedersen (2026), Creative Commons Attribution-Share Alike 4.0 International license.
|
||||
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 50 KiB |