Bugfix: AVIF Image PHash Support (#6556)

* AVIF phash support
* add avif check for zips
This commit is contained in:
Gykes
2026-02-10 18:38:57 -06:00
committed by GitHub
parent 5628fbc5d3
commit 7aa7276fa3
3 changed files with 44 additions and 8 deletions
+4 -4
View File
@@ -27,11 +27,11 @@ func printPhash(ff *ffmpeg.FFMpeg, ffp *ffmpeg.FFProbe, inputfile string, quiet
// Common image extensions
imageExts := map[string]bool{
"jpg": true, "jpeg": true, "png": true, "gif": true, "webp": true, "bmp": true,
"jpg": true, "jpeg": true, "png": true, "gif": true, "webp": true, "bmp": true, "avif": true,
}
if imageExts[ext] {
return printImagePhash(inputfile, quiet)
return printImagePhash(ff, inputfile, quiet)
}
return printVideoPhash(ff, ffp, inputfile, quiet)
@@ -65,12 +65,12 @@ func printVideoPhash(ff *ffmpeg.FFMpeg, ffp *ffmpeg.FFProbe, inputfile string, q
return nil
}
func printImagePhash(inputfile string, quiet *bool) error {
func printImagePhash(ff *ffmpeg.FFMpeg, inputfile string, quiet *bool) error {
imgFile := &models.ImageFile{
BaseFile: &models.BaseFile{Path: inputfile},
}
phash, err := imagephash.Generate(imgFile)
phash, err := imagephash.Generate(ff, imgFile)
if err != nil {
return err
}
@@ -41,7 +41,7 @@ func (t *GenerateImagePhashTask) Start(ctx context.Context) {
}
if !set {
generated, err := imagephash.Generate(t.File)
generated, err := imagephash.Generate(instance.FFMpeg, t.File)
if err != nil {
logger.Errorf("Error generating phash for %q: %v", t.File.Path, err)
logErrorOutput(err)
+39 -3
View File
@@ -2,17 +2,22 @@ package imagephash
import (
"bytes"
"context"
"fmt"
"image"
"path/filepath"
"strings"
"github.com/corona10/goimagehash"
"github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/ffmpeg/transcoder"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/models"
)
// Generate computes a perceptual hash for an image file.
func Generate(imageFile *models.ImageFile) (*uint64, error) {
img, err := loadImage(imageFile)
func Generate(encoder *ffmpeg.FFMpeg, imageFile *models.ImageFile) (*uint64, error) {
img, err := loadImage(encoder, imageFile)
if err != nil {
return nil, fmt.Errorf("loading image: %w", err)
}
@@ -27,7 +32,17 @@ func Generate(imageFile *models.ImageFile) (*uint64, error) {
}
// loadImage loads an image from disk and decodes it.
func loadImage(imageFile *models.ImageFile) (image.Image, error) {
// For AVIF files, ffmpeg is used to convert to BMP first since Go has no built-in AVIF decoder.
func loadImage(encoder *ffmpeg.FFMpeg, imageFile *models.ImageFile) (image.Image, error) {
ext := strings.ToLower(filepath.Ext(imageFile.Path))
if ext == ".avif" {
// AVIF in zip files is not supported - ffmpeg cannot read files inside zips
if imageFile.Base().ZipFileID != nil {
return nil, fmt.Errorf("AVIF images in zip files are not supported for phash generation")
}
return loadImageFFmpeg(encoder, imageFile.Path)
}
reader, err := imageFile.Open(&file.OsFS{})
if err != nil {
return nil, err
@@ -46,3 +61,24 @@ func loadImage(imageFile *models.ImageFile) (image.Image, error) {
return img, nil
}
// loadImageFFmpeg uses ffmpeg to convert an image to BMP and then decodes it.
func loadImageFFmpeg(encoder *ffmpeg.FFMpeg, path string) (image.Image, error) {
options := transcoder.ScreenshotOptions{
OutputPath: "-",
OutputType: transcoder.ScreenshotOutputTypeBMP,
}
args := transcoder.ScreenshotTime(path, 0, options)
data, err := encoder.GenerateOutput(context.Background(), args, nil)
if err != nil {
return nil, fmt.Errorf("converting image with ffmpeg: %w", err)
}
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("decoding ffmpeg output: %w", err)
}
return img, nil
}