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:
parent
de57063118
commit
278ebd1c62
16 changed files with 84 additions and 31 deletions
|
@ -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> |
|
BIN
assets/examples/fox.profile0.8bpc.yuv420.avif
Normal file
BIN
assets/examples/fox.profile0.8bpc.yuv420.avif
Normal file
Binary file not shown.
|
@ -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")
|
||||
|
|
|
@ -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()},
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue