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 |
|-------------------------------|------------|----------------------------------------------------------------------|
| 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/>.**
| Filename | Author | URL |
|----------------|------------|----------------------------------------------------------------------|
| pythagoras.gif | Petrus3743 | <https://commons.wikimedia.org/wiki/File:01-Satz_des_Pythagoras.gif> |

Binary file not shown.

View File

@ -96,6 +96,11 @@ func (c *Config) SipsBin() string {
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.
func (c *Config) HeifConvertBin() string {
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-blacklist", c.RawtherapeeBlacklist()},
{"sips-bin", c.SipsBin()},
{"sips-blacklist", c.SipsBlacklist()},
{"heifconvert-bin", c.HeifConvertBin()},
{"ffmpeg-bin", c.FFmpegBin()},
{"ffmpeg-encoder", c.FFmpegEncoder().String()},

View File

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

View File

@ -558,7 +558,7 @@ var Flags = CliFlags{
Flag: cli.StringFlag{
Name: "rawtherapee-blacklist",
Usage: "do not use RawTherapee to convert files with these `EXTENSIONS`",
Value: "",
Value: "avif,avifs",
EnvVar: "PHOTOPRISM_RAWTHERAPEE_BLACKLIST",
}},
CliFlag{
@ -568,6 +568,13 @@ var Flags = CliFlags{
Value: "sips",
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{
Flag: cli.StringFlag{
Name: "heifconvert-bin",

View File

@ -19,6 +19,7 @@ import (
type Convert struct {
conf *config.Config
cmdMutex sync.Mutex
sipsBlacklist fs.Blacklist
darktableBlacklist fs.Blacklist
rawtherapeeBlacklist fs.Blacklist
}
@ -27,6 +28,7 @@ type Convert struct {
func NewConvert(conf *config.Config) *Convert {
c := &Convert{
conf: conf,
sipsBlacklist: fs.NewBlacklist(conf.SipsBlacklist()),
darktableBlacklist: fs.NewBlacklist(conf.DarktableBlacklist()),
rawtherapeeBlacklist: fs.NewBlacklist(conf.RawtherapeeBlacklist()),
}
@ -97,7 +99,7 @@ func (c *Convert) Start(path string, force bool) (err error) {
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
}

View File

@ -134,9 +134,9 @@ func (c *Convert) JpegConvertCommand(f *MediaFile, jpegName string, xmpName stri
maxSize := strconv.Itoa(c.conf.JpegSize())
// 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())
} else if f.IsRaw() && c.conf.RawEnabled() {
} else if f.IsRaw() && c.conf.RawEnabled() || f.IsAVIF() {
if c.conf.DarktableEnabled() && c.darktableBlacklist.Ok(fileExt) {
var args []string

View File

@ -363,6 +363,8 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
result.Main = f
} else if f.IsRaw() {
result.Main = f
} else if f.IsAVIF() {
result.Main = f
} else if f.IsHEIF() {
isHEIF = true
result.Main = f
@ -726,6 +728,11 @@ func (m *MediaFile) IsHEIF() bool {
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.
func (m *MediaFile) IsBitmap() bool {
return m.MimeType() == fs.MimeTypeBitmap
@ -765,6 +772,8 @@ func (m *MediaFile) FileType() fs.Type {
return fs.ImagePNG
case m.IsGif():
return fs.ImageGIF
case m.IsAVIF():
return fs.ImageAVIF
case m.IsHEIF():
return fs.ImageHEIF
case m.IsBitmap():
@ -835,7 +844,7 @@ func (m *MediaFile) IsImageNative() bool {
// IsImage checks if the file is an image
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.
@ -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).
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).

View File

@ -889,15 +889,22 @@ func TestMediaFile_MimeType(t *testing.T) {
}
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) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "image/heif", mediaFile.MimeType())
assert.True(t, mediaFile.IsHEIF())
})
t.Run("IMG_4120.AAE", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120.AAE")
if err != nil {
@ -1092,6 +1099,13 @@ func TestMediaFile_HasType(t *testing.T) {
}
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) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
if err != nil {

View File

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

View File

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

View File

@ -12,13 +12,14 @@ import (
// File types.
const (
RawImage Type = "raw" // RAW image file.
ImageJPEG Type = "jpg" // JPEG image file.
ImageHEIF Type = "heif" // High Efficiency Image File Format
ImageTIFF Type = "tiff" // TIFF image file.
ImagePNG Type = "png" // PNG image file.
ImageGIF Type = "gif" // GIF image file.
ImageBMP Type = "bmp" // BMP image file.
RawImage Type = "raw" // RAW image
ImageJPEG Type = "jpg" // JPEG image
ImageAVIF Type = "avif" // AV1 Image File Format (AVIF)
ImageHEIF Type = "heif" // High Efficiency Image File Format (HEIF/HEIC)
ImageTIFF Type = "tiff" // TIFF image
ImagePNG Type = "png" // PNG image
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
ImageWebP Type = "webp" // Google WebP Image
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
VideoASF Type = "asf" // Advanced Systems/Streaming Format (ASF)
VideoWMV Type = "wmv" // Windows Media Video (based on ASF)
XmpFile Type = "xmp" // Adobe XMP sidecar file (XML).
AaeFile Type = "aae" // Apple image edits sidecar file (based on XML).
XmlFile Type = "xml" // XML metadata / config / sidecar file.
YamlFile Type = "yml" // YAML metadata / config / sidecar file.
JsonFile Type = "json" // JSON metadata / config / sidecar file.
TextFile Type = "txt" // Text config / sidecar file.
MarkdownFile Type = "md" // Markdown text sidecar file.
UnknownType Type = "" // Unknown file type.
XmpFile Type = "xmp" // Adobe XMP sidecar file (XML)
AaeFile Type = "aae" // Apple image edits sidecar file (based on XML)
XmlFile Type = "xml" // XML metadata / config / sidecar file
YamlFile Type = "yml" // YAML metadata / config / sidecar file
JsonFile Type = "json" // JSON metadata / config / sidecar file
TextFile Type = "txt" // Text config / sidecar file
MarkdownFile Type = "md" // Markdown text sidecar file
UnknownType Type = "" // Unknown file
)

View File

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

View File

@ -2,6 +2,7 @@ package fs
import (
"os"
"path/filepath"
"github.com/h2non/filetype"
)
@ -13,11 +14,17 @@ const (
MimeTypeBitmap = "image/bmp"
MimeTypeWebP = "image/webp"
MimeTypeTiff = "image/tiff"
MimeTypeAVIF = "image/avif"
MimeTypeHEIF = "image/heif"
)
// MimeType returns the mime type of a file, an empty string if it is unknown.
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)
if err != nil {

View File

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