diff --git a/internal/api/images.go b/internal/api/images.go index 9e16fc0df..e0f11416a 100644 --- a/internal/api/images.go +++ b/internal/api/images.go @@ -26,6 +26,7 @@ var imageBoxExts = []string{ ".gif", ".svg", ".webp", + ".avif", } func newImageBox(box fs.FS) (*imageBox, error) { diff --git a/internal/dlna/cms.go b/internal/dlna/cms.go index e4a560462..daf43b382 100644 --- a/internal/dlna/cms.go +++ b/internal/dlna/cms.go @@ -27,7 +27,7 @@ import ( // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // 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 { *Server diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index c7b1c1fdf..3b3b2c054 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -293,7 +293,7 @@ const ( // slice default values var ( 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"} defaultMenuItems = []string{"scenes", "images", "groups", "markers", "galleries", "performers", "studios", "tags"} ) diff --git a/pkg/file/image/scan.go b/pkg/file/image/scan.go index 635f421e4..1e09ce023 100644 --- a/pkg/file/image/scan.go +++ b/pkg/file/image/scan.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "image" + "path/filepath" + "strings" _ "image/gif" _ "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 // TODO - copy to temp file if not an OsFS 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) return decorateFallback(fs, f) } diff --git a/pkg/image/thumbnail.go b/pkg/image/thumbnail.go index c65cfc77e..e63ada41b 100644 --- a/pkg/image/thumbnail.go +++ b/pkg/image/thumbnail.go @@ -22,12 +22,8 @@ const ffmpegImageQuality = 5 var vipsPath string var once sync.Once -var ( - ErrUnsupportedImageFormat = errors.New("unsupported image format") - - // ErrNotSupportedForThumbnail is returned if the image format is not supported for thumbnail generation - ErrNotSupportedForThumbnail = errors.New("unsupported image format for thumbnail") -) +// ErrNotSupportedForThumbnail is returned if the image format is not supported for thumbnail generation +var ErrNotSupportedForThumbnail = errors.New("unsupported image format for thumbnail") type ThumbnailEncoder struct { FFMpeg *ffmpeg.FFMpeg @@ -83,8 +79,9 @@ func (e *ThumbnailEncoder) GetThumbnail(f models.File, maxSize int) ([]byte, err data := buf.Bytes() + format := "" if imageFile, ok := f.(*models.ImageFile); ok { - format := imageFile.Format + format = imageFile.Format animated := imageFile.Format == formatGif // #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 { 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 @@ -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) { - args := transcoder.ImageThumbnail("-", transcoder.ImageThumbnailOptions{ + options := transcoder.ImageThumbnailOptions{ OutputFormat: ffmpeg.ImageFormatJpeg, OutputPath: "-", MaxDimensions: maxSize, Quality: ffmpegImageQuality, - }) + } + + args := transcoder.ImageThumbnail("-", options) 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 { var thumbFilter ffmpeg.VideoFilter thumbFilter = thumbFilter.ScaleMaxSize(maxSize) diff --git a/ui/v2.5/src/docs/en/Manual/Images.md b/ui/v2.5/src/docs/en/Manual/Images.md index e5733fc2e..ede9b3457 100644 --- a/ui/v2.5/src/docs/en/Manual/Images.md +++ b/ui/v2.5/src/docs/en/Manual/Images.md @@ -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. +> **: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. 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.