mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Improve image scanning performance and thumbnail generation (#1655)
* Improve image scanning performance and thumbnail generation * Add vips-tools to build image * Add option to write generated thumbnails to disk * Fallback to image if thumbnail generation fails Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -2,39 +2,126 @@ package image
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func ThumbnailNeeded(srcImage image.Image, maxSize int) bool {
|
||||
dim := srcImage.Bounds().Max
|
||||
w := dim.X
|
||||
h := dim.Y
|
||||
var vipsPath string
|
||||
var once sync.Once
|
||||
|
||||
return w > maxSize || h > maxSize
|
||||
type ThumbnailEncoder struct {
|
||||
FFMPEGPath string
|
||||
VipsPath string
|
||||
}
|
||||
|
||||
func GetVipsPath() string {
|
||||
once.Do(func() {
|
||||
vipsPath, _ = exec.LookPath("vips")
|
||||
})
|
||||
return vipsPath
|
||||
}
|
||||
|
||||
func NewThumbnailEncoder(ffmpegPath string) ThumbnailEncoder {
|
||||
return ThumbnailEncoder{
|
||||
FFMPEGPath: ffmpegPath,
|
||||
VipsPath: GetVipsPath(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetThumbnail returns the thumbnail image of the provided image resized to
|
||||
// the provided max size. It resizes based on the largest X/Y direction.
|
||||
// It returns nil and an error if an error occurs reading, decoding or encoding
|
||||
// the image.
|
||||
func GetThumbnail(srcImage image.Image, maxSize int) ([]byte, error) {
|
||||
var resizedImage image.Image
|
||||
|
||||
// if height is longer then resize by height instead of width
|
||||
dim := srcImage.Bounds().Max
|
||||
if dim.Y > dim.X {
|
||||
resizedImage = imaging.Resize(srcImage, 0, maxSize, imaging.Box)
|
||||
} else {
|
||||
resizedImage = imaging.Resize(srcImage, maxSize, 0, imaging.Box)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err := jpeg.Encode(buf, resizedImage, nil)
|
||||
func (e *ThumbnailEncoder) GetThumbnail(img *models.Image, maxSize int) ([]byte, error) {
|
||||
reader, err := openSourceImage(img.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(reader)
|
||||
|
||||
_, format, err := DecodeSourceImage(img)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if format != nil && *format == "gif" {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
if e.VipsPath != "" {
|
||||
return e.getVipsThumbnail(buf, maxSize)
|
||||
} else {
|
||||
return e.getFFMPEGThumbnail(buf, format, maxSize, img.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ThumbnailEncoder) getVipsThumbnail(image *bytes.Buffer, maxSize int) ([]byte, error) {
|
||||
args := []string{
|
||||
"thumbnail_source",
|
||||
"[descriptor=0]",
|
||||
".jpg[Q=70,strip]",
|
||||
fmt.Sprint(maxSize),
|
||||
"--size", "down",
|
||||
}
|
||||
data, err := e.run(e.VipsPath, args, image)
|
||||
|
||||
return []byte(data), err
|
||||
}
|
||||
|
||||
func (e *ThumbnailEncoder) getFFMPEGThumbnail(image *bytes.Buffer, format *string, maxDimensions int, path string) ([]byte, error) {
|
||||
// ffmpeg spends a long sniffing image format when data is piped through stdio, so we pass the format explicitly instead
|
||||
ffmpegformat := ""
|
||||
if format != nil && *format == "jpeg" {
|
||||
ffmpegformat = "mjpeg"
|
||||
} else if format != nil && *format == "png" {
|
||||
ffmpegformat = "png_pipe"
|
||||
} else if format != nil && *format == "webp" {
|
||||
ffmpegformat = "webp_pipe"
|
||||
} else {
|
||||
return nil, errors.New("unsupported image format")
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-f", ffmpegformat,
|
||||
"-i", "-",
|
||||
"-vf", fmt.Sprintf("scale=%v:%v:force_original_aspect_ratio=decrease", maxDimensions, maxDimensions),
|
||||
"-c:v", "mjpeg",
|
||||
"-q:v", "5",
|
||||
"-f", "image2pipe",
|
||||
"-",
|
||||
}
|
||||
data, err := e.run(e.FFMPEGPath, args, image)
|
||||
|
||||
return []byte(data), err
|
||||
}
|
||||
|
||||
func (e *ThumbnailEncoder) run(path string, args []string, stdin *bytes.Buffer) (string, error) {
|
||||
cmd := exec.Command(path, args...)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Stdin = stdin
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err := cmd.Wait()
|
||||
|
||||
if err != nil {
|
||||
// error message should be in the stderr stream
|
||||
logger.Errorf("image encoder error when running command <%s>: %s", strings.Join(cmd.Args, " "), stderr.String())
|
||||
return stdout.String(), err
|
||||
}
|
||||
|
||||
return stdout.String(), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user