mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Feature: AVIF support (#6288)
This commit is contained in:
parent
4ef3a605dd
commit
90d1b2df2d
6 changed files with 43 additions and 11 deletions
|
|
@ -26,6 +26,7 @@ var imageBoxExts = []string{
|
||||||
".gif",
|
".gif",
|
||||||
".svg",
|
".svg",
|
||||||
".webp",
|
".webp",
|
||||||
|
".avif",
|
||||||
}
|
}
|
||||||
|
|
||||||
func newImageBox(box fs.FS) (*imageBox, error) {
|
func newImageBox(box fs.FS) (*imageBox, error) {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import (
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
// THE SOFTWARE.
|
// THE SOFTWARE.
|
||||||
|
|
||||||
const defaultProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*"
|
const defaultProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*,http-get:*:image/avif:*"
|
||||||
|
|
||||||
type connectionManagerService struct {
|
type connectionManagerService struct {
|
||||||
*Server
|
*Server
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,7 @@ const (
|
||||||
// slice default values
|
// slice default values
|
||||||
var (
|
var (
|
||||||
defaultVideoExtensions = []string{"m4v", "mp4", "mov", "wmv", "avi", "mpg", "mpeg", "rmvb", "rm", "flv", "asf", "mkv", "webm", "f4v"}
|
defaultVideoExtensions = []string{"m4v", "mp4", "mov", "wmv", "avi", "mpg", "mpeg", "rmvb", "rm", "flv", "asf", "mkv", "webm", "f4v"}
|
||||||
defaultImageExtensions = []string{"png", "jpg", "jpeg", "gif", "webp"}
|
defaultImageExtensions = []string{"png", "jpg", "jpeg", "gif", "webp", "avif"}
|
||||||
defaultGalleryExtensions = []string{"zip", "cbz"}
|
defaultGalleryExtensions = []string{"zip", "cbz"}
|
||||||
defaultMenuItems = []string{"scenes", "images", "groups", "markers", "galleries", "performers", "studios", "tags"}
|
defaultMenuItems = []string{"scenes", "images", "groups", "markers", "galleries", "performers", "studios", "tags"}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
_ "image/gif"
|
_ "image/gif"
|
||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
|
|
@ -28,6 +30,11 @@ func (d *Decorator) Decorate(ctx context.Context, fs models.FS, f models.File) (
|
||||||
// ignore clips in non-OsFS filesystems as ffprobe cannot read them
|
// ignore clips in non-OsFS filesystems as ffprobe cannot read them
|
||||||
// TODO - copy to temp file if not an OsFS
|
// TODO - copy to temp file if not an OsFS
|
||||||
if _, isOs := fs.(*file.OsFS); !isOs {
|
if _, isOs := fs.(*file.OsFS); !isOs {
|
||||||
|
// AVIF images inside zip files are not supported
|
||||||
|
if strings.ToLower(filepath.Ext(base.Path)) == ".avif" {
|
||||||
|
logger.Warnf("Skipping AVIF image in zip file: %s", base.Path)
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
logger.Debugf("assuming ImageFile for non-OsFS file %q", base.Path)
|
logger.Debugf("assuming ImageFile for non-OsFS file %q", base.Path)
|
||||||
return decorateFallback(fs, f)
|
return decorateFallback(fs, f)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,8 @@ const ffmpegImageQuality = 5
|
||||||
var vipsPath string
|
var vipsPath string
|
||||||
var once sync.Once
|
var once sync.Once
|
||||||
|
|
||||||
var (
|
// ErrNotSupportedForThumbnail is returned if the image format is not supported for thumbnail generation
|
||||||
ErrUnsupportedImageFormat = errors.New("unsupported image format")
|
var ErrNotSupportedForThumbnail = errors.New("unsupported image format for thumbnail")
|
||||||
|
|
||||||
// ErrNotSupportedForThumbnail is returned if the image format is not supported for thumbnail generation
|
|
||||||
ErrNotSupportedForThumbnail = errors.New("unsupported image format for thumbnail")
|
|
||||||
)
|
|
||||||
|
|
||||||
type ThumbnailEncoder struct {
|
type ThumbnailEncoder struct {
|
||||||
FFMpeg *ffmpeg.FFMpeg
|
FFMpeg *ffmpeg.FFMpeg
|
||||||
|
|
@ -83,8 +79,9 @@ func (e *ThumbnailEncoder) GetThumbnail(f models.File, maxSize int) ([]byte, err
|
||||||
|
|
||||||
data := buf.Bytes()
|
data := buf.Bytes()
|
||||||
|
|
||||||
|
format := ""
|
||||||
if imageFile, ok := f.(*models.ImageFile); ok {
|
if imageFile, ok := f.(*models.ImageFile); ok {
|
||||||
format := imageFile.Format
|
format = imageFile.Format
|
||||||
animated := imageFile.Format == formatGif
|
animated := imageFile.Format == formatGif
|
||||||
|
|
||||||
// #2266 - if image is webp, then determine if it is animated
|
// #2266 - if image is webp, then determine if it is animated
|
||||||
|
|
@ -96,6 +93,15 @@ func (e *ThumbnailEncoder) GetThumbnail(f models.File, maxSize int) ([]byte, err
|
||||||
if animated {
|
if animated {
|
||||||
return nil, fmt.Errorf("%w: %s", ErrNotSupportedForThumbnail, format)
|
return nil, fmt.Errorf("%w: %s", ErrNotSupportedForThumbnail, format)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AVIF cannot be read from stdin, must use file path
|
||||||
|
// AVIF in zip files is not supported
|
||||||
|
if format == "avif" {
|
||||||
|
if f.Base().ZipFileID != nil {
|
||||||
|
return nil, fmt.Errorf("%w: AVIF in zip file", ErrNotSupportedForThumbnail)
|
||||||
|
}
|
||||||
|
return e.ffmpegImageThumbnailPath(f.Base().Path, maxSize)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Videofiles can only be thumbnailed with ffmpeg
|
// Videofiles can only be thumbnailed with ffmpeg
|
||||||
|
|
@ -130,16 +136,32 @@ func (e *ThumbnailEncoder) GetPreview(inPath string, outPath string, maxSize int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ThumbnailEncoder) ffmpegImageThumbnail(image *bytes.Buffer, maxSize int) ([]byte, error) {
|
func (e *ThumbnailEncoder) ffmpegImageThumbnail(image *bytes.Buffer, maxSize int) ([]byte, error) {
|
||||||
args := transcoder.ImageThumbnail("-", transcoder.ImageThumbnailOptions{
|
options := transcoder.ImageThumbnailOptions{
|
||||||
OutputFormat: ffmpeg.ImageFormatJpeg,
|
OutputFormat: ffmpeg.ImageFormatJpeg,
|
||||||
OutputPath: "-",
|
OutputPath: "-",
|
||||||
MaxDimensions: maxSize,
|
MaxDimensions: maxSize,
|
||||||
Quality: ffmpegImageQuality,
|
Quality: ffmpegImageQuality,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
args := transcoder.ImageThumbnail("-", options)
|
||||||
|
|
||||||
return e.FFMpeg.GenerateOutput(context.TODO(), args, image)
|
return e.FFMpeg.GenerateOutput(context.TODO(), args, image)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ffmpegImageThumbnailPath generates a thumbnail from a file path (used for AVIF which can't be piped)
|
||||||
|
func (e *ThumbnailEncoder) ffmpegImageThumbnailPath(inputPath string, maxSize int) ([]byte, error) {
|
||||||
|
options := transcoder.ImageThumbnailOptions{
|
||||||
|
OutputFormat: ffmpeg.ImageFormatJpeg,
|
||||||
|
OutputPath: "-",
|
||||||
|
MaxDimensions: maxSize,
|
||||||
|
Quality: ffmpegImageQuality,
|
||||||
|
}
|
||||||
|
|
||||||
|
args := transcoder.ImageThumbnail(inputPath, options)
|
||||||
|
|
||||||
|
return e.FFMpeg.GenerateOutput(context.TODO(), args, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (e *ThumbnailEncoder) getClipPreview(inPath string, outPath string, maxSize int, clipDuration float64, frameRate float64) error {
|
func (e *ThumbnailEncoder) getClipPreview(inPath string, outPath string, maxSize int, clipDuration float64, frameRate float64) error {
|
||||||
var thumbFilter ffmpeg.VideoFilter
|
var thumbFilter ffmpeg.VideoFilter
|
||||||
thumbFilter = thumbFilter.ScaleMaxSize(maxSize)
|
thumbFilter = thumbFilter.ScaleMaxSize(maxSize)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ You can add images to every gallery manually in the gallery detail page. Deletin
|
||||||
|
|
||||||
For best results, images in zip file should be stored without compression (copy, store or no compression options depending on the software you use. Eg on linux: `zip -0 -r gallery.zip foldertozip/`). This impacts **heavily** on the zip read performance.
|
For best results, images in zip file should be stored without compression (copy, store or no compression options depending on the software you use. Eg on linux: `zip -0 -r gallery.zip foldertozip/`). This impacts **heavily** on the zip read performance.
|
||||||
|
|
||||||
|
> **:warning: Note:** AVIF files in ZIP archives are currently unsupported.
|
||||||
|
|
||||||
If a filename of an image in the gallery zip file ends with `cover.jpg`, it will be treated like a cover and presented first in the gallery view page and as a gallery cover in the gallery list view. If more than one images match the name the first one found in natural sort order is selected.
|
If a filename of an image in the gallery zip file ends with `cover.jpg`, it will be treated like a cover and presented first in the gallery view page and as a gallery cover in the gallery list view. If more than one images match the name the first one found in natural sort order is selected.
|
||||||
|
|
||||||
You can also manually select any image from a gallery as its cover. On the gallery details page, select the desired cover image, and then select **Set as Cover** in the ⋯ menu.
|
You can also manually select any image from a gallery as its cover. On the gallery details page, select the desired cover image, and then select **Set as Cover** in the ⋯ menu.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue