Images: Add AV1 Image File Format (AVIF) support #2706

AVIF can be converted

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2022-09-15 00:43:08 +02:00
parent de57063118
commit 278ebd1c62
16 changed files with 84 additions and 31 deletions

View file

@ -1,6 +1,9 @@
Sample File Attribution # Sample File Attribution
===========================================================================
| Filename | Author | URL | | Filename | Author | URL |
|----------------|------------|----------------------------------------------------------------------| |-------------------------------|------------|----------------------------------------------------------------------|
| pythagoras.gif | Petrus3743 | <https://commons.wikimedia.org/wiki/File:01-Satz_des_Pythagoras.gif> | | pythagoras.gif | Petrus3743 | <https://commons.wikimedia.org/wiki/File:01-Satz_des_Pythagoras.gif> |
| fox.profile0.8bpc.yuv420.avif | Link-U | <https://github.com/link-u/avif-sample-images> |
**Additional File Samples can be found at <https://dl.photoprism.app/samples/>.**

Binary file not shown.

View file

@ -96,6 +96,11 @@ func (c *Config) SipsBin() string {
return findExecutable(c.options.SipsBin, "sips") return findExecutable(c.options.SipsBin, "sips")
} }
// SipsBlacklist returns the Sips file extension blacklist.
func (c *Config) SipsBlacklist() string {
return c.options.SipsBlacklist
}
// HeifConvertBin returns the heif-convert executable file name. // HeifConvertBin returns the heif-convert executable file name.
func (c *Config) HeifConvertBin() string { func (c *Config) HeifConvertBin() string {
return findExecutable(c.options.HeifConvertBin, "heif-convert") return findExecutable(c.options.HeifConvertBin, "heif-convert")

View file

@ -136,6 +136,7 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"rawtherapee-bin", c.RawtherapeeBin()}, {"rawtherapee-bin", c.RawtherapeeBin()},
{"rawtherapee-blacklist", c.RawtherapeeBlacklist()}, {"rawtherapee-blacklist", c.RawtherapeeBlacklist()},
{"sips-bin", c.SipsBin()}, {"sips-bin", c.SipsBin()},
{"sips-blacklist", c.SipsBlacklist()},
{"heifconvert-bin", c.HeifConvertBin()}, {"heifconvert-bin", c.HeifConvertBin()},
{"ffmpeg-bin", c.FFmpegBin()}, {"ffmpeg-bin", c.FFmpegBin()},
{"ffmpeg-encoder", c.FFmpegEncoder().String()}, {"ffmpeg-encoder", c.FFmpegEncoder().String()},

View file

@ -107,6 +107,7 @@ type Options struct {
RawtherapeeBin string `yaml:"RawtherapeeBin" json:"-" flag:"rawtherapee-bin"` RawtherapeeBin string `yaml:"RawtherapeeBin" json:"-" flag:"rawtherapee-bin"`
RawtherapeeBlacklist string `yaml:"RawtherapeeBlacklist" json:"-" flag:"rawtherapee-blacklist"` RawtherapeeBlacklist string `yaml:"RawtherapeeBlacklist" json:"-" flag:"rawtherapee-blacklist"`
SipsBin string `yaml:"SipsBin" json:"-" flag:"sips-bin"` SipsBin string `yaml:"SipsBin" json:"-" flag:"sips-bin"`
SipsBlacklist string `yaml:"SipsBlacklist" json:"-" flag:"sips-blacklist"`
HeifConvertBin string `yaml:"HeifConvertBin" json:"-" flag:"heifconvert-bin"` HeifConvertBin string `yaml:"HeifConvertBin" json:"-" flag:"heifconvert-bin"`
FFmpegBin string `yaml:"FFmpegBin" json:"-" flag:"ffmpeg-bin"` FFmpegBin string `yaml:"FFmpegBin" json:"-" flag:"ffmpeg-bin"`
FFmpegEncoder string `yaml:"FFmpegEncoder" json:"FFmpegEncoder" flag:"ffmpeg-encoder"` FFmpegEncoder string `yaml:"FFmpegEncoder" json:"FFmpegEncoder" flag:"ffmpeg-encoder"`

View file

@ -558,7 +558,7 @@ var Flags = CliFlags{
Flag: cli.StringFlag{ Flag: cli.StringFlag{
Name: "rawtherapee-blacklist", Name: "rawtherapee-blacklist",
Usage: "do not use RawTherapee to convert files with these `EXTENSIONS`", Usage: "do not use RawTherapee to convert files with these `EXTENSIONS`",
Value: "", Value: "avif,avifs",
EnvVar: "PHOTOPRISM_RAWTHERAPEE_BLACKLIST", EnvVar: "PHOTOPRISM_RAWTHERAPEE_BLACKLIST",
}}, }},
CliFlag{ CliFlag{
@ -568,6 +568,13 @@ var Flags = CliFlags{
Value: "sips", Value: "sips",
EnvVar: "PHOTOPRISM_SIPS_BIN", EnvVar: "PHOTOPRISM_SIPS_BIN",
}}, }},
CliFlag{
Flag: cli.StringFlag{
Name: "sips-blacklist",
Usage: "do not use Sips to convert files with these `EXTENSIONS`*macOS only*",
Value: "avif,avifs",
EnvVar: "PHOTOPRISM_SIPS_BLACKLIST",
}},
CliFlag{ CliFlag{
Flag: cli.StringFlag{ Flag: cli.StringFlag{
Name: "heifconvert-bin", Name: "heifconvert-bin",

View file

@ -19,6 +19,7 @@ import (
type Convert struct { type Convert struct {
conf *config.Config conf *config.Config
cmdMutex sync.Mutex cmdMutex sync.Mutex
sipsBlacklist fs.Blacklist
darktableBlacklist fs.Blacklist darktableBlacklist fs.Blacklist
rawtherapeeBlacklist fs.Blacklist rawtherapeeBlacklist fs.Blacklist
} }
@ -27,6 +28,7 @@ type Convert struct {
func NewConvert(conf *config.Config) *Convert { func NewConvert(conf *config.Config) *Convert {
c := &Convert{ c := &Convert{
conf: conf, conf: conf,
sipsBlacklist: fs.NewBlacklist(conf.SipsBlacklist()),
darktableBlacklist: fs.NewBlacklist(conf.DarktableBlacklist()), darktableBlacklist: fs.NewBlacklist(conf.DarktableBlacklist()),
rawtherapeeBlacklist: fs.NewBlacklist(conf.RawtherapeeBlacklist()), rawtherapeeBlacklist: fs.NewBlacklist(conf.RawtherapeeBlacklist()),
} }
@ -97,7 +99,7 @@ func (c *Convert) Start(path string, force bool) (err error) {
f, err := NewMediaFile(fileName) f, err := NewMediaFile(fileName)
if err != nil || f.Empty() || !(f.IsRaw() || f.IsHEIF() || f.IsImageOther() || f.IsVideo()) { if err != nil || f.Empty() || !(f.IsRaw() || f.IsHEIF() || f.IsAVIF() || f.IsImageOther() || f.IsVideo()) {
return nil return nil
} }

View file

@ -134,9 +134,9 @@ func (c *Convert) JpegConvertCommand(f *MediaFile, jpegName string, xmpName stri
maxSize := strconv.Itoa(c.conf.JpegSize()) maxSize := strconv.Itoa(c.conf.JpegSize())
// Select conversion command depending on the file type and runtime environment. // Select conversion command depending on the file type and runtime environment.
if c.conf.SipsEnabled() && (f.IsRaw() || f.IsHEIF()) { if (f.IsRaw() || f.IsHEIF() || f.IsAVIF()) && c.conf.SipsEnabled() && c.sipsBlacklist.Ok(fileExt) {
result = exec.Command(c.conf.SipsBin(), "-Z", maxSize, "-s", "format", "jpeg", "--out", jpegName, f.FileName()) result = exec.Command(c.conf.SipsBin(), "-Z", maxSize, "-s", "format", "jpeg", "--out", jpegName, f.FileName())
} else if f.IsRaw() && c.conf.RawEnabled() { } else if f.IsRaw() && c.conf.RawEnabled() || f.IsAVIF() {
if c.conf.DarktableEnabled() && c.darktableBlacklist.Ok(fileExt) { if c.conf.DarktableEnabled() && c.darktableBlacklist.Ok(fileExt) {
var args []string var args []string

View file

@ -363,6 +363,8 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
result.Main = f result.Main = f
} else if f.IsRaw() { } else if f.IsRaw() {
result.Main = f result.Main = f
} else if f.IsAVIF() {
result.Main = f
} else if f.IsHEIF() { } else if f.IsHEIF() {
isHEIF = true isHEIF = true
result.Main = f result.Main = f
@ -726,6 +728,11 @@ func (m *MediaFile) IsHEIF() bool {
return m.MimeType() == fs.MimeTypeHEIF return m.MimeType() == fs.MimeTypeHEIF
} }
// IsAVIF returns true if this is an AV1 Image File Format image.
func (m *MediaFile) IsAVIF() bool {
return m.MimeType() == fs.MimeTypeAVIF
}
// IsBitmap returns true if this is a bitmap image. // IsBitmap returns true if this is a bitmap image.
func (m *MediaFile) IsBitmap() bool { func (m *MediaFile) IsBitmap() bool {
return m.MimeType() == fs.MimeTypeBitmap return m.MimeType() == fs.MimeTypeBitmap
@ -765,6 +772,8 @@ func (m *MediaFile) FileType() fs.Type {
return fs.ImagePNG return fs.ImagePNG
case m.IsGif(): case m.IsGif():
return fs.ImageGIF return fs.ImageGIF
case m.IsAVIF():
return fs.ImageAVIF
case m.IsHEIF(): case m.IsHEIF():
return fs.ImageHEIF return fs.ImageHEIF
case m.IsBitmap(): case m.IsBitmap():
@ -835,7 +844,7 @@ func (m *MediaFile) IsImageNative() bool {
// IsImage checks if the file is an image // IsImage checks if the file is an image
func (m *MediaFile) IsImage() bool { func (m *MediaFile) IsImage() bool {
return m.IsImageNative() || m.IsRaw() || m.IsHEIF() return m.IsImageNative() || m.IsRaw() || m.IsAVIF() || m.IsHEIF()
} }
// IsLive checks if the file is a live photo. // IsLive checks if the file is a live photo.
@ -858,7 +867,7 @@ func (m *MediaFile) ExifSupported() bool {
// IsMedia returns true if this is a media file (photo or video, not sidecar or other). // IsMedia returns true if this is a media file (photo or video, not sidecar or other).
func (m *MediaFile) IsMedia() bool { func (m *MediaFile) IsMedia() bool {
return m.IsJpeg() || m.IsVideo() || m.IsRaw() || m.IsHEIF() || m.IsImageOther() return m.IsJpeg() || m.IsVideo() || m.IsRaw() || m.IsAVIF() || m.IsHEIF() || m.IsImageOther()
} }
// Jpeg returns the JPEG version of the media file (if exists). // Jpeg returns the JPEG version of the media file (if exists).

View file

@ -889,15 +889,22 @@ func TestMediaFile_MimeType(t *testing.T) {
} }
assert.Equal(t, "", mediaFile.MimeType()) assert.Equal(t, "", mediaFile.MimeType())
}) })
t.Run("fox.profile0.8bpc.yuv420.avif", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/fox.profile0.8bpc.yuv420.avif")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "image/avif", mediaFile.MimeType())
assert.True(t, mediaFile.IsAVIF())
})
t.Run("iphone_7.heic", func(t *testing.T) { t.Run("iphone_7.heic", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic") mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
assert.Equal(t, "image/heif", mediaFile.MimeType()) assert.Equal(t, "image/heif", mediaFile.MimeType())
assert.True(t, mediaFile.IsHEIF())
}) })
t.Run("IMG_4120.AAE", func(t *testing.T) { t.Run("IMG_4120.AAE", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120.AAE") mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120.AAE")
if err != nil { if err != nil {
@ -1092,6 +1099,13 @@ func TestMediaFile_HasType(t *testing.T) {
} }
assert.Equal(t, false, mediaFile.HasFileType("jpg")) assert.Equal(t, false, mediaFile.HasFileType("jpg"))
}) })
t.Run("fox.profile0.8bpc.yuv420.avif", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/fox.profile0.8bpc.yuv420.avif")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, true, mediaFile.HasFileType("avif"))
})
t.Run("iphone_7.heic", func(t *testing.T) { t.Run("iphone_7.heic", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic") mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
if err != nil { if err != nil {

View file

@ -65,6 +65,7 @@ func ShareSelection(originals bool) FileSelection {
fs.ImagePNG.String(), fs.ImagePNG.String(),
fs.ImageWebP.String(), fs.ImageWebP.String(),
fs.ImageTIFF.String(), fs.ImageTIFF.String(),
fs.ImageAVIF.String(),
fs.ImageHEIF.String(), fs.ImageHEIF.String(),
fs.ImageBMP.String(), fs.ImageBMP.String(),
fs.ImageGIF.String(), fs.ImageGIF.String(),

View file

@ -17,6 +17,8 @@ var Extensions = FileExtensions{
".jfif": ImageJPEG, ".jfif": ImageJPEG,
".jfi": ImageJPEG, ".jfi": ImageJPEG,
".thm": ImageJPEG, ".thm": ImageJPEG,
".avif": ImageAVIF,
".avifs": ImageAVIF,
".heif": ImageHEIF, ".heif": ImageHEIF,
".hif": ImageHEIF, ".hif": ImageHEIF,
".heic": ImageHEIF, ".heic": ImageHEIF,
@ -24,8 +26,6 @@ var Extensions = FileExtensions{
".heics": ImageHEIF, ".heics": ImageHEIF,
".avci": ImageHEIF, ".avci": ImageHEIF,
".avcs": ImageHEIF, ".avcs": ImageHEIF,
".avif": ImageHEIF,
".avifs": ImageHEIF,
".webp": ImageWebP, ".webp": ImageWebP,
".tif": ImageTIFF, ".tif": ImageTIFF,
".tiff": ImageTIFF, ".tiff": ImageTIFF,
@ -133,7 +133,7 @@ func (m FileExtensions) Known(name string) bool {
return false return false
} }
// TypesExt returns known extensions by file type. // Types returns known extensions by file type.
func (m FileExtensions) Types(noUppercase bool) TypesExt { func (m FileExtensions) Types(noUppercase bool) TypesExt {
result := make(TypesExt) result := make(TypesExt)

View file

@ -12,13 +12,14 @@ import (
// File types. // File types.
const ( const (
RawImage Type = "raw" // RAW image file. RawImage Type = "raw" // RAW image
ImageJPEG Type = "jpg" // JPEG image file. ImageJPEG Type = "jpg" // JPEG image
ImageHEIF Type = "heif" // High Efficiency Image File Format ImageAVIF Type = "avif" // AV1 Image File Format (AVIF)
ImageTIFF Type = "tiff" // TIFF image file. ImageHEIF Type = "heif" // High Efficiency Image File Format (HEIF/HEIC)
ImagePNG Type = "png" // PNG image file. ImageTIFF Type = "tiff" // TIFF image
ImageGIF Type = "gif" // GIF image file. ImagePNG Type = "png" // PNG image
ImageBMP Type = "bmp" // BMP image file. ImageGIF Type = "gif" // GIF image
ImageBMP Type = "bmp" // BMP image
ImageMPO Type = "mpo" // Stereoscopic Image that consists of two JPG images that are combined into one 3D image ImageMPO Type = "mpo" // Stereoscopic Image that consists of two JPG images that are combined into one 3D image
ImageWebP Type = "webp" // Google WebP Image ImageWebP Type = "webp" // Google WebP Image
VideoWebM Type = "webm" // Google WebM Video VideoWebM Type = "webm" // Google WebM Video
@ -40,12 +41,12 @@ const (
VideoOGV Type = "ogv" // Ogg container format maintained by the Xiph.Org, free and open VideoOGV Type = "ogv" // Ogg container format maintained by the Xiph.Org, free and open
VideoASF Type = "asf" // Advanced Systems/Streaming Format (ASF) VideoASF Type = "asf" // Advanced Systems/Streaming Format (ASF)
VideoWMV Type = "wmv" // Windows Media Video (based on ASF) VideoWMV Type = "wmv" // Windows Media Video (based on ASF)
XmpFile Type = "xmp" // Adobe XMP sidecar file (XML). XmpFile Type = "xmp" // Adobe XMP sidecar file (XML)
AaeFile Type = "aae" // Apple image edits sidecar file (based on XML). AaeFile Type = "aae" // Apple image edits sidecar file (based on XML)
XmlFile Type = "xml" // XML metadata / config / sidecar file. XmlFile Type = "xml" // XML metadata / config / sidecar file
YamlFile Type = "yml" // YAML metadata / config / sidecar file. YamlFile Type = "yml" // YAML metadata / config / sidecar file
JsonFile Type = "json" // JSON metadata / config / sidecar file. JsonFile Type = "json" // JSON metadata / config / sidecar file
TextFile Type = "txt" // Text config / sidecar file. TextFile Type = "txt" // Text config / sidecar file
MarkdownFile Type = "md" // Markdown text sidecar file. MarkdownFile Type = "md" // Markdown text sidecar file
UnknownType Type = "" // Unknown file type. UnknownType Type = "" // Unknown file
) )

View file

@ -9,6 +9,7 @@ var TypeInfo = map[Type]string{
ImageTIFF: "Tag Image File Format", ImageTIFF: "Tag Image File Format",
ImageBMP: "Bitmap", ImageBMP: "Bitmap",
ImageMPO: "Stereoscopic JPEG (3D)", ImageMPO: "Stereoscopic JPEG (3D)",
ImageAVIF: "AV1 Image File Format",
ImageHEIF: "High Efficiency Image File Format", ImageHEIF: "High Efficiency Image File Format",
ImageWebP: "Google WebP", ImageWebP: "Google WebP",
VideoWebM: "Google WebM", VideoWebM: "Google WebM",

View file

@ -2,6 +2,7 @@ package fs
import ( import (
"os" "os"
"path/filepath"
"github.com/h2non/filetype" "github.com/h2non/filetype"
) )
@ -13,11 +14,17 @@ const (
MimeTypeBitmap = "image/bmp" MimeTypeBitmap = "image/bmp"
MimeTypeWebP = "image/webp" MimeTypeWebP = "image/webp"
MimeTypeTiff = "image/tiff" MimeTypeTiff = "image/tiff"
MimeTypeAVIF = "image/avif"
MimeTypeHEIF = "image/heif" MimeTypeHEIF = "image/heif"
) )
// MimeType returns the mime type of a file, an empty string if it is unknown. // MimeType returns the mime type of a file, an empty string if it is unknown.
func MimeType(filename string) string { func MimeType(filename string) string {
// Workaround, since "image/avif " cannot be recognized yet.
if Extensions[filepath.Ext(filename)] == ImageAVIF {
return MimeTypeAVIF
}
handle, err := os.Open(filename) handle, err := os.Open(filename)
if err != nil { if err != nil {

View file

@ -11,6 +11,7 @@ var Formats = map[fs.Type]Type{
fs.ImageTIFF: Image, fs.ImageTIFF: Image,
fs.ImageBMP: Image, fs.ImageBMP: Image,
fs.ImageMPO: Image, fs.ImageMPO: Image,
fs.ImageAVIF: Image,
fs.ImageHEIF: Image, fs.ImageHEIF: Image,
fs.VideoHEVC: Video, fs.VideoHEVC: Video,
fs.ImageWebP: Image, fs.ImageWebP: Image,