package artwork
import (
"bytes"
"context"
"fmt"
"image"
"image/draw"
"image/jpeg"
"image/png"
"io"
"sync"
"time"
"github.com/gen2brain/webp"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/core/ffmpeg"
"github.com/navidrome/navidrome/log"
xdraw "golang.org/x/image/draw"
)
func init() {
conf.AddHook(func() {
if err := webp.Dynamic(); err != nil {
log.Debug("Artwork: Using WASM WebP encoder/decoder", "reason", err)
} else {
log.Debug("Artwork: Using native libwebp for WebP encoding/decoding")
}
})
}
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func resizeImageData(ctx context.Context, ffm ffmpeg.FFmpeg, data []byte, size int, square bool) (io.Reader, int, error) {
start := time.Now()
defer func() {
log.Trace(ctx, "Artwork: Resized image", "bytes", len(data), "size", size, "square", square,
"elapsed", time.Since(start))
}()
if isAnimatedGIF(data) {
if ffm.IsAvailable() {
r, err := ffm.ConvertAnimatedImage(ctx, bytes.NewReader(data), size, conf.Server.CoverArtQuality)
if err == nil {
return r, 0, nil
}
log.Warn(ctx, "Artwork: Could not convert animated GIF, falling back to static", err)
}
} else if isAnimatedWebP(data) || isAnimatedPNG(data) {
return bytes.NewReader(data), 0, nil
}
return resizeStaticImage(data, size, square)
}
func toFastScaleType(img image.Image) image.Image {
switch img.(type) {
case *image.RGBA, *image.NRGBA, *image.Gray, *image.YCbCr:
return img
default:
rgba := image.NewRGBA(img.Bounds())
draw.Draw(rgba, rgba.Bounds(), img, img.Bounds().Min, draw.Src)
return rgba
}
}
func resizeStaticImage(data []byte, size int, square bool) (io.Reader, int, error) {
original, format, err := decodeCapped(data)
if err != nil {
return nil, 0, err
}
bounds := original.Bounds()
originalSize := max(bounds.Max.X, bounds.Max.Y)
if size > originalSize {
size = originalSize
}
if originalSize <= size && !square {
return nil, originalSize, nil
}
srcW, srcH := bounds.Dx(), bounds.Dy()
scale := float64(size) / float64(max(srcW, srcH))
dstW := int(float64(srcW) * scale)
dstH := int(float64(srcH) * scale)
var dst *image.NRGBA
var dstRect image.Rectangle
if square {
dst = image.NewNRGBA(image.Rect(0, 0, size, size))
offsetX := (size - dstW) / 2
offsetY := (size - dstH) / 2
dstRect = image.Rect(offsetX, offsetY, offsetX+dstW, offsetY+dstH)
} else {
dst = image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
dstRect = dst.Bounds()
}
original = toFastScaleType(original)
xdraw.CatmullRom.Scale(dst, dstRect, original, bounds, draw.Src, nil)
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
if conf.Server.EnableWebPEncoding {
err = webp.Encode(buf, dst, webp.Options{Quality: conf.Server.CoverArtQuality})
} else if format == "png" || square {
err = png.Encode(buf, dst)
} else {
err = jpeg.Encode(buf, dst, &jpeg.Options{Quality: conf.Server.CoverArtQuality})
}
if err != nil {
bufPool.Put(buf)
return nil, originalSize, err
}
encoded := bytes.Clone(buf.Bytes())
bufPool.Put(buf)
return bytes.NewReader(encoded), originalSize, nil
}
func formatQualityTag() string {
if conf.Server.EnableWebPEncoding {
return fmt.Sprintf("webp%d", conf.Server.CoverArtQuality)
}
return fmt.Sprintf("q%d", conf.Server.CoverArtQuality)
}