/
githubmirror
/
hugo
Обзор
Документация
Войти
/
githubmirror
/
hugo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
resources/images/codec.go
433 строки
11 KB
Bjørn Erik Pedersen
images: Make AVIF chroma subsampling content-aware via the hint
04 июн 2026, 19:54
04 июн 2026, 19:54
341f575
Код
Авторство
О чём код?
// Copyright 2025 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package images import ( "bufio" "bytes" "encoding/binary" "errors" "fmt" "image" "image/color" "image/color/palette" "image/draw" "image/gif" "image/jpeg" "image/png" "io" "strings" "github.com/bep/imagemeta" "github.com/bep/logg" "github.com/gohugoio/hugo/common/himage" "github.com/gohugoio/hugo/common/hugio" "golang.org/x/image/bmp" "golang.org/x/image/tiff" ) // Decoder defines the decoding of an image format. // These matches the globalmpackage image functions. type Decoder interface { Decode(r io.Reader) (image.Image, error) DecodeConfig(r io.Reader) (image.Config, error) } type ToEncoder interface { EncodeTo(conf ImageConfig, w io.Writer, src image.Image) 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 } // 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 EncodeDecoder avif EncodeDecoder debugl logg.LevelLogger } 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 { d.debugl.Log( // This construct looks odd, but the func will only be called if debug is enabled. logg.StringFunc( func() string { return fmt.Sprintf("Encoding image to format %s with config %+v", conf.TargetFormat, conf) }, ), ) switch conf.TargetFormat { case JPEG: var rgba *image.RGBA quality := conf.Quality if nrgba, ok := img.(*image.NRGBA); ok { if nrgba.Opaque() { rgba = &image.RGBA{ Pix: nrgba.Pix, Stride: nrgba.Stride, Rect: nrgba.Rect, } } } if rgba != nil { return jpeg.Encode(w, rgba, &jpeg.Options{Quality: quality}) } return jpeg.Encode(w, img, &jpeg.Options{Quality: quality}) case PNG: encoder := png.Encoder{CompressionLevel: png.DefaultCompression} return encoder.Encode(w, img) case GIF: if anim, ok := img.(himage.AnimatedImage); ok { if g, ok := anim.GetRaw().(*gif.GIF); ok { return gif.EncodeAll(w, g) } // Animated image, but not a GIF. Convert it. frames := anim.GetFrames() if len(frames) == 0 { return gif.Encode(w, img, &gif.Options{NumColors: 256}) } frameDurations := anim.GetFrameDurations() if len(frameDurations) != len(frames) { return errors.New("gif: number of frame durations does not match number of frames") } outGif := &gif.GIF{ Delay: himage.FrameDurationsToGifDelays(frameDurations), } outGif.LoopCount = anim.GetLoopCount() for _, frame := range frames { bounds := frame.Bounds() palettedImage := image.NewPaletted(bounds, palette.Plan9) draw.Draw(palettedImage, palettedImage.Rect, frame, bounds.Min, draw.Src) outGif.Image = append(outGif.Image, palettedImage) } return gif.EncodeAll(w, outGif) } return gif.Encode(w, img, &gif.Options{ NumColors: 256, }) case TIFF: 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, "hint": conf.Hint, } return d.avif.Encode(w, img, opts) case WEBP: // Convert bool to int because the C code reads it as a number. useSharpYuvInt := 0 if conf.UseSharpYuv { useSharpYuvInt = 1 } opts := map[string]any{ "compression": conf.Compression, "quality": conf.Quality, "hint": conf.Hint, "useSharpYuv": useSharpYuvInt, "method": conf.Method, } return d.webp.Encode(w, img, opts) default: return fmt.Errorf("format %q not supported", conf.TargetFormat) } } func (d *Codec) DecodeFormat(f Format, r io.Reader) (image.Image, error) { d.debugl.Log( logg.StringFunc( func() string { return fmt.Sprintf("Decoding image from format %s", f) }, ), ) switch f { case JPEG, PNG: // We reworked this decode/encode setup to get full WebP support in v0.153.0. // In the first take of that we used f to decide whether to call png.Decode or jpeg.Decode here, // but testing it on some sites, it seems that it's not uncommon to store JPEGs with PNG extensions and vice versa. // So, to reduce some noise in that release, we fallback to the standard library here, // which will read the magic bytes and decode accordingly. img, _, err := image.Decode(r) return img, err case GIF: g, err := gif.DecodeAll(r) if err != nil { return nil, fmt.Errorf("failed to decode gif: %w", err) } if len(g.Delay) > 1 { return &giphy{gif: g, Image: g.Image[0]}, nil } return g.Image[0], nil case TIFF: 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 { return img, nil } if rs, ok := r.(io.ReadSeeker); ok { // See issue 14288. Turns out it's not uncommon to e.g. name their PNG files with a WEBP extension. // With the old Go's webp decoder, this didn't fail (it looked for the file header), // but now some error has surfaced. // To reduce some noise, we try to reset and decode again using the standard library. _, err2 := rs.Seek(0, io.SeekStart) if err2 != nil { return nil, err } img, _, err2 = image.Decode(rs) if err2 == nil { return img, nil } } return nil, err default: return nil, errors.New("format not supported") } } func (d *Codec) Decode(r io.Reader) (image.Image, error) { d.debugl.Log( logg.StringFunc( func() string { return "Decoding image from unknown format" }, ), ) rr := toPeekReader(r) format, err := formatFromImage(rr) if err != nil { return nil, err } if format != 0 { return d.DecodeFormat(format, rr) } // Fallback to the standard image.Decode. img, _, err := image.Decode(rr) return img, err } func (d *Codec) DecodeConfig(f Format, r io.Reader) (image.Config, string, error) { rr := toPeekReader(r) format, err := formatFromImage(rr) if err != nil { return image.Config{}, "", err } if format == 0 { format = f } r = rr if format.UseImageMetaConfigDecoder() { rs, err := hugio.NewReadSeekerNoOpCloserFromReader(r) if err != nil { return image.Config{}, "", err } rs.Seek(0, 0) res, err := imagemeta.Decode( imagemeta.Options{ R: rs, ImageFormat: format.ToImageMetaImageFormatFormat(), Sources: imagemeta.CONFIG, }, ) if err == nil { return image.Config{ Width: res.ImageConfig.Width, Height: res.ImageConfig.Height, ColorModel: color.RGBAModel, }, strings.ToLower(format.String()), nil } // Fallback to the standard image.DecodeConfig. rs.Seek(0, 0) r = rs } conf, name, err := image.DecodeConfig(r) return conf, name, err } // toPeekReader converts an io.Reader to a peekReader. func toPeekReader(r io.Reader) peekReader { if rr, ok := r.(peekReader); ok { return rr } return bufio.NewReader(r) } // A peekReader is an io.Reader that can also peek ahead. type peekReader interface { io.Reader Peek(int) ([]byte, error) } const ( // The WebP file header is 12 bytes long and starts with "RIFF" followed by // 4 bytes indicating the file size, followed by "WEBP" and the VP8 chunk header. // We use '?' as a wildcard for the 4 size bytes. magicWebp = "RIFF????WEBPVP8" // The GIF file header is 6 bytes long and starts with "GIF87a" or "GIF89a". magicGif = "GIF8???" ) var ( avifBrandAvis = []byte("avis") avifBrandAvif = []byte("avif") avifFtyp = []byte("ftyp") ) type magicFormat struct { magic string format Format } var magicFormats = []magicFormat{ {magic: magicWebp, format: WEBP}, {magic: magicGif, format: GIF}, } // formatFromImage determines the image format from the magic bytes. // Note that this is only a partial implementation, // 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 { magicLen := len(mf.magic) b, err := r.Peek(magicLen) if err == nil && match(mf.magic, b) { 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 } for i := 0; i < len(magic); i++ { if magic[i] != '?' && magic[i] != b[i] { return false } } return true } var ( _ himage.AnimatedImage = (*giphy)(nil) _ himage.ImageConfigProvider = (*giphy)(nil) ) type giphy struct { image.Image gif *gif.GIF } func (g *giphy) GetRaw() any { return g.gif } func (g *giphy) GetLoopCount() int { return g.gif.LoopCount } func (g *giphy) GetFrames() []image.Image { frames := make([]image.Image, len(g.gif.Image)) for i, frame := range g.gif.Image { frames[i] = frame } return frames } func (g *giphy) GetImageConfig() image.Config { return g.gif.Config } func (g *giphy) SetFrames(frames []image.Image) { if len(frames) == 0 { panic("frames cannot be empty") } g.gif.Image = make([]*image.Paletted, len(frames)) for i, frame := range frames { g.gif.Image[i] = frame.(*image.Paletted) } g.Image = g.gif.Image[0] } func (g *giphy) SetWidthHeight(width, height int) { g.gif.Config.Width = width g.gif.Config.Height = height } func (g *giphy) GetFrameDurations() []int { return himage.GifDelaysToFrameDurations(g.gif.Delay) }