From c6ae43c1d696b048350edbf7442f9c7c7c855d02 Mon Sep 17 00:00:00 2001 From: Gykes <24581046+Gykes@users.noreply.github.com> Date: Thu, 27 Nov 2025 22:00:23 -0600 Subject: [PATCH] Feature Request: Vips AVIF Support (#6337) --- pkg/image/thumbnail.go | 16 ++++++++++++---- pkg/image/vips.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/pkg/image/thumbnail.go b/pkg/image/thumbnail.go index e63ada41b..d0fba0f60 100644 --- a/pkg/image/thumbnail.go +++ b/pkg/image/thumbnail.go @@ -96,10 +96,14 @@ func (e *ThumbnailEncoder) GetThumbnail(f models.File, maxSize int) ([]byte, err // AVIF cannot be read from stdin, must use file path // AVIF in zip files is not supported + // Note: No Windows check needed here since we use file path, not stdin if format == "avif" { if f.Base().ZipFileID != nil { return nil, fmt.Errorf("%w: AVIF in zip file", ErrNotSupportedForThumbnail) } + if e.vips != nil { + return e.vips.ImageThumbnailPath(f.Base().Path, maxSize) + } return e.ffmpegImageThumbnailPath(f.Base().Path, maxSize) } } @@ -110,11 +114,15 @@ func (e *ThumbnailEncoder) GetThumbnail(f models.File, maxSize int) ([]byte, err } // vips has issues loading files from stdin on Windows - if e.vips != nil && runtime.GOOS != "windows" { - return e.vips.ImageThumbnail(buf, maxSize) - } else { - return e.ffmpegImageThumbnail(buf, maxSize) + if e.vips != nil { + if runtime.GOOS == "windows" && f.Base().ZipFileID == nil { + return e.vips.ImageThumbnailPath(f.Base().Path, maxSize) + } + if runtime.GOOS != "windows" { + return e.vips.ImageThumbnail(buf, maxSize) + } } + return e.ffmpegImageThumbnail(buf, maxSize) } // GetPreview returns the preview clip of the provided image clip resized to diff --git a/pkg/image/vips.go b/pkg/image/vips.go index 39809dc18..0a0350aa8 100644 --- a/pkg/image/vips.go +++ b/pkg/image/vips.go @@ -24,6 +24,38 @@ func (e *vipsEncoder) ImageThumbnail(image *bytes.Buffer, maxSize int) ([]byte, return []byte(data), err } +// ImageThumbnailPath generates a thumbnail from a file path instead of stdin. +// This is required for formats like AVIF that need random file access (seeking) +// which stdin cannot provide. +func (e *vipsEncoder) ImageThumbnailPath(path string, maxSize int) ([]byte, error) { + // vips thumbnail syntax: thumbnail input output width [options] + // Using .jpg[Q=70,strip] as output writes to stdout + args := []string{ + "thumbnail", + path, + ".jpg[Q=70,strip]", + fmt.Sprint(maxSize), + "--size", "down", + } + + cmd := exec.Command(string(*e), args...) + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Start(); err != nil { + return nil, err + } + + if err := cmd.Wait(); err != nil { + logger.Errorf("image encoder error when running command <%s>: %s", strings.Join(cmd.Args, " "), stderr.String()) + return nil, err + } + + return stdout.Bytes(), nil +} + func (e *vipsEncoder) run(args []string, stdin *bytes.Buffer) (string, error) { cmd := exec.Command(string(*e), args...)