diff --git a/frontend/src/common/util.js b/frontend/src/common/util.js
index c88a14957..51d3c3956 100644
--- a/frontend/src/common/util.js
+++ b/frontend/src/common/util.js
@@ -176,6 +176,8 @@ export default class Util {
return "Portable Network Graphics";
case "tiff":
return "TIFF";
+ case "psd":
+ return "Adobe Photoshop";
case "gif":
return "GIF";
case "dng":
@@ -212,6 +214,10 @@ export default class Util {
return "Windows Media";
case "svg":
return "Scalable Vector Graphics";
+ case "pdf":
+ return "Portable Document Format";
+ case "ai":
+ return "Adobe Illustrator";
case "ps":
return "Adobe PostScript";
case "eps":
diff --git a/internal/photoprism/convert.go b/internal/photoprism/convert.go
index fb97aeaf7..b0a40e64c 100644
--- a/internal/photoprism/convert.go
+++ b/internal/photoprism/convert.go
@@ -108,7 +108,7 @@ func (c *Convert) Start(path string, ext []string, force bool) (err error) {
f, err := NewMediaFile(fileName)
- if err != nil || f.Empty() || !(f.IsRaw() || f.IsVector() || f.IsHEIC() || f.IsAVIF() || f.IsImageOther() || f.IsVideo()) {
+ if err != nil || f.Empty() || f.IsPreviewImage() || !f.IsMedia() {
return nil
}
diff --git a/internal/photoprism/convert_preview_jpeg.go b/internal/photoprism/convert_preview_jpeg.go
index b84ed31dd..52e94e39f 100644
--- a/internal/photoprism/convert_preview_jpeg.go
+++ b/internal/photoprism/convert_preview_jpeg.go
@@ -90,10 +90,10 @@ func (c *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str
// Try ImageMagick for other image file formats if allowed.
if c.conf.ImageMagickEnabled() && c.imagemagickBlacklist.Allow(fileExt) &&
- (f.IsDNG() || f.IsAVIF() || f.IsHEIC() || f.IsVector() && c.conf.VectorEnabled() || f.IsRaw() && c.conf.RawEnabled()) {
+ (f.IsImage() || f.IsVector() && c.conf.VectorEnabled() || f.IsRaw() && c.conf.RawEnabled()) {
quality := fmt.Sprintf("%d", c.conf.JpegQuality())
resize := fmt.Sprintf("%dx%d>", c.conf.JpegSize(), c.conf.JpegSize())
- args := []string{f.FileName(), "-resize", resize, "-quality", quality, jpegName}
+ args := []string{f.FileName(), "-flatten", "-resize", resize, "-quality", quality, jpegName}
result = append(result, exec.Command(c.conf.ImageMagickBin(), args...))
}
diff --git a/internal/photoprism/convert_preview_png.go b/internal/photoprism/convert_preview_png.go
index 7e2822ebd..4fe4ae898 100644
--- a/internal/photoprism/convert_preview_png.go
+++ b/internal/photoprism/convert_preview_png.go
@@ -33,9 +33,9 @@ func (c *Convert) PngConvertCommands(f *MediaFile, pngName string) (result []*ex
// Try ImageMagick for other image file formats if allowed.
if c.conf.ImageMagickEnabled() && c.imagemagickBlacklist.Allow(fileExt) &&
- (f.IsDNG() || f.IsAVIF() || f.IsHEIC() || f.IsVector() && c.conf.VectorEnabled() || f.IsRaw() && c.conf.RawEnabled()) {
+ (f.IsImage() || f.IsVector() && c.conf.VectorEnabled() || f.IsRaw() && c.conf.RawEnabled()) {
resize := fmt.Sprintf("%dx%d>", c.conf.PngSize(), c.conf.PngSize())
- args := []string{f.FileName(), "-resize", resize, pngName}
+ args := []string{f.FileName(), "-flatten", "-resize", resize, pngName}
result = append(result, exec.Command(c.conf.ImageMagickBin(), args...))
} else if f.IsVector() && c.conf.RsvgConvertEnabled() {
// Vector graphics may be also be converted with librsvg if installed.
diff --git a/internal/photoprism/index_mediafile.go b/internal/photoprism/index_mediafile.go
index b7e30322a..202824954 100644
--- a/internal/photoprism/index_mediafile.go
+++ b/internal/photoprism/index_mediafile.go
@@ -432,7 +432,7 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
log.Warn(err.Error())
file.FileError = err.Error()
}
- case m.IsRaw(), m.IsDNG(), m.IsHEIC(), m.IsAVIF(), m.IsImageOther():
+ case m.IsRaw(), m.IsImage():
if metaData := m.MetaData(); metaData.Error == nil {
// Update basic metadata.
photo.SetTitle(metaData.Title, entity.SrcMeta)
diff --git a/internal/photoprism/mediafile.go b/internal/photoprism/mediafile.go
index 0493b5153..84a015756 100644
--- a/internal/photoprism/mediafile.go
+++ b/internal/photoprism/mediafile.go
@@ -379,14 +379,10 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
result.Main = f
} else if f.IsVector() {
result.Main = f
- } else if f.IsDNG() {
- result.Main = f
- } else if f.IsAVIF() {
- result.Main = f
} else if f.IsHEIC() {
isHEIC = true
result.Main = f
- } else if f.IsImageOther() {
+ } else if f.IsImage() && !f.IsPreviewImage() {
result.Main = f
} else if f.IsVideo() && !isHEIC {
result.Main = f
@@ -409,12 +405,16 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
return result, fmt.Errorf("%s is unsupported (%s)", clean.Log(m.BaseName()), t)
}
- // Add hidden JPEG if exists.
+ // Add hidden preview image if needed.
if !result.HasPreview() {
if jpegName := fs.ImageJPEG.FindFirst(result.Main.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); jpegName != "" {
if resultFile, _ := NewMediaFile(jpegName); resultFile.Ok() {
result.Files = append(result.Files, resultFile)
}
+ } else if pngName := fs.ImagePNG.FindFirst(result.Main.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); pngName != "" {
+ if resultFile, _ := NewMediaFile(pngName); resultFile.Ok() {
+ result.Files = append(result.Files, resultFile)
+ }
}
}
@@ -771,11 +771,6 @@ func (m *MediaFile) IsWebP() bool {
return m.MimeType() == fs.MimeTypeWebP
}
-// IsVideo returns true if this is a video file.
-func (m *MediaFile) IsVideo() bool {
- return strings.HasPrefix(m.MimeType(), "video/") || m.Media() == media.Video
-}
-
// Duration returns the duration if the file is a video.
func (m *MediaFile) Duration() time.Duration {
if !m.IsVideo() {
@@ -790,11 +785,6 @@ func (m *MediaFile) IsAnimatedGif() bool {
return m.IsGif() && m.MetaData().Frames > 1
}
-// IsAnimated returns true if it is a video or animated image.
-func (m *MediaFile) IsAnimated() bool {
- return m.IsVideo() || m.IsAnimatedGif()
-}
-
// IsJson return true if this media file is a json sidecar file.
func (m *MediaFile) IsJson() bool {
return m.HasFileType(fs.SidecarJSON)
@@ -841,21 +831,41 @@ func (m *MediaFile) HasFileType(fileType fs.Type) bool {
return m.FileType() == fileType
}
+// IsImage checks if the file is an image.
+func (m *MediaFile) IsImage() bool {
+ return m.HasMediaType(media.Image)
+}
+
+// IsRaw returns true if this is a RAW file.
+func (m *MediaFile) IsRaw() bool {
+ return m.HasFileType(fs.ImageRaw) || m.HasMediaType(media.Raw) || m.IsDNG()
+}
+
+// IsAnimated returns true if it is a video or animated image.
+func (m *MediaFile) IsAnimated() bool {
+ return m.IsVideo() || m.IsAnimatedGif()
+}
+
+// IsVideo returns true if this is a video file.
+func (m *MediaFile) IsVideo() bool {
+ return strings.HasPrefix(m.MimeType(), "video/") || m.Media() == media.Video
+}
+
// IsVector returns true if this is a vector graphics.
func (m *MediaFile) IsVector() bool {
return m.HasMediaType(media.Vector) || m.IsSVG()
}
+// IsSidecar checks if the file is a metadata sidecar file, independent of the storage location.
+func (m *MediaFile) IsSidecar() bool {
+ return m.Media() == media.Sidecar
+}
+
// IsSVG returns true if this is a SVG vector graphics.
func (m *MediaFile) IsSVG() bool {
return m.HasFileType(fs.VectorSVG)
}
-// IsRaw returns true if this is a RAW file.
-func (m *MediaFile) IsRaw() bool {
- return m.HasFileType(fs.ImageRaw) || m.IsDNG()
-}
-
// IsXMP returns true if this is a XMP sidecar file.
func (m *MediaFile) IsXMP() bool {
return m.FileType() == fs.SidecarXMP
@@ -871,11 +881,6 @@ func (m *MediaFile) InSidecar() bool {
return m.Root() == entity.RootSidecar
}
-// IsSidecar checks if the file is a metadata sidecar file, independent of the storage location.
-func (m *MediaFile) IsSidecar() bool {
- return m.Media() == media.Sidecar
-}
-
// IsPlayableVideo checks if the file is a video in playable format.
func (m *MediaFile) IsPlayableVideo() bool {
return m.IsVideo() && (m.HasFileType(fs.VideoMP4) || m.HasFileType(fs.VideoAVC))
@@ -896,11 +901,6 @@ func (m *MediaFile) IsImageNative() bool {
return m.IsJpeg() || m.IsImageOther()
}
-// IsImage checks if the file is an image
-func (m *MediaFile) IsImage() bool {
- return m.IsImageNative() || m.IsRaw() || m.IsDNG() || m.IsAVIF() || m.IsHEIC()
-}
-
// IsLive checks if the file is a live photo.
func (m *MediaFile) IsLive() bool {
if m.IsHEIC() {
@@ -921,7 +921,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.IsImageNative() || m.IsVideo() || m.IsVector() || m.IsRaw() || m.IsDNG() || m.IsAVIF() || m.IsHEIC()
+ return m.IsImage() || m.IsRaw() || m.IsVideo() || m.IsVector()
}
// PreviewImage returns a PNG or JPEG version of the media file, if exists.
diff --git a/internal/photoprism/mediafile_test.go b/internal/photoprism/mediafile_test.go
index 18b6e6851..9da6f2624 100644
--- a/internal/photoprism/mediafile_test.go
+++ b/internal/photoprism/mediafile_test.go
@@ -1410,56 +1410,61 @@ func TestMediaFile_IsSidecar(t *testing.T) {
}
func TestMediaFile_IsImage(t *testing.T) {
- t.Run("iphone_7.json", func(t *testing.T) {
- conf := config.TestConfig()
+ cnf := config.TestConfig()
- mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.json")
+ t.Run("iphone_7.json", func(t *testing.T) {
+ f, err := NewMediaFile(cnf.ExamplesPath() + "/iphone_7.json")
if err != nil {
t.Fatal(err)
}
- assert.Equal(t, false, mediaFile.IsImage())
+ assert.Equal(t, false, f.IsImage())
+ assert.Equal(t, false, f.IsRaw())
+ assert.Equal(t, true, f.IsSidecar())
})
t.Run("iphone_7.xmp", func(t *testing.T) {
- conf := config.TestConfig()
-
- mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.xmp")
+ f, err := NewMediaFile(cnf.ExamplesPath() + "/iphone_7.xmp")
assert.Nil(t, err)
- assert.Equal(t, false, mediaFile.IsImage())
+ assert.Equal(t, false, f.IsImage())
+ assert.Equal(t, false, f.IsRaw())
+ assert.Equal(t, true, f.IsSidecar())
})
t.Run("iphone_7.heic", func(t *testing.T) {
- conf := config.TestConfig()
-
- mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
+ f, err := NewMediaFile(cnf.ExamplesPath() + "/iphone_7.heic")
if err != nil {
t.Fatal(err)
}
- assert.Equal(t, true, mediaFile.IsImage())
+ assert.Equal(t, true, f.IsImage())
+ assert.Equal(t, false, f.IsRaw())
+ assert.Equal(t, false, f.IsSidecar())
})
t.Run("canon_eos_6d.dng", func(t *testing.T) {
- conf := config.TestConfig()
-
- mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/canon_eos_6d.dng")
+ f, err := NewMediaFile(cnf.ExamplesPath() + "/canon_eos_6d.dng")
if err != nil {
t.Fatal(err)
}
- assert.Equal(t, true, mediaFile.IsImage())
+ assert.Equal(t, false, f.IsImage())
+ assert.Equal(t, true, f.IsRaw())
+ assert.Equal(t, false, f.IsSidecar())
})
t.Run("elephants.jpg", func(t *testing.T) {
- conf := config.TestConfig()
-
- mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
- assert.Nil(t, err)
- assert.Equal(t, true, mediaFile.IsImage())
+ f, err := NewMediaFile(cnf.ExamplesPath() + "/elephants.jpg")
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, true, f.IsImage())
+ assert.Equal(t, false, f.IsRaw())
+ assert.Equal(t, false, f.IsSidecar())
})
}
func TestMediaFile_IsVideo(t *testing.T) {
- conf := config.TestConfig()
+ cnf := config.TestConfig()
t.Run("christmas.mp4", func(t *testing.T) {
- if f, err := NewMediaFile(filepath.Join(conf.ExamplesPath(), "christmas.mp4")); err != nil {
+ if f, err := NewMediaFile(filepath.Join(cnf.ExamplesPath(), "christmas.mp4")); err != nil {
t.Fatal(err)
} else {
+ assert.Equal(t, false, f.IsRaw())
assert.Equal(t, false, f.IsImage())
assert.Equal(t, true, f.IsVideo())
assert.Equal(t, false, f.IsJson())
@@ -1467,19 +1472,21 @@ func TestMediaFile_IsVideo(t *testing.T) {
}
})
t.Run("canon_eos_6d.dng", func(t *testing.T) {
- if f, err := NewMediaFile(filepath.Join(conf.ExamplesPath(), "canon_eos_6d.dng")); err != nil {
+ if f, err := NewMediaFile(filepath.Join(cnf.ExamplesPath(), "canon_eos_6d.dng")); err != nil {
t.Fatal(err)
} else {
- assert.Equal(t, true, f.IsImage())
+ assert.Equal(t, true, f.IsRaw())
+ assert.Equal(t, false, f.IsImage())
assert.Equal(t, false, f.IsVideo())
assert.Equal(t, false, f.IsJson())
assert.Equal(t, false, f.IsSidecar())
}
})
t.Run("iphone_7.json", func(t *testing.T) {
- if f, err := NewMediaFile(filepath.Join(conf.ExamplesPath(), "iphone_7.json")); err != nil {
+ if f, err := NewMediaFile(filepath.Join(cnf.ExamplesPath(), "iphone_7.json")); err != nil {
t.Fatal(err)
} else {
+ assert.Equal(t, false, f.IsRaw())
assert.Equal(t, false, f.IsImage())
assert.Equal(t, false, f.IsVideo())
assert.Equal(t, true, f.IsJson())
@@ -1489,10 +1496,10 @@ func TestMediaFile_IsVideo(t *testing.T) {
}
func TestMediaFile_IsAnimated(t *testing.T) {
- conf := config.TestConfig()
+ cnf := config.TestConfig()
t.Run("example.gif", func(t *testing.T) {
- if f, err := NewMediaFile(filepath.Join(conf.ExamplesPath(), "example.gif")); err != nil {
+ if f, err := NewMediaFile(filepath.Join(cnf.ExamplesPath(), "example.gif")); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, true, f.IsImage())
@@ -1504,7 +1511,7 @@ func TestMediaFile_IsAnimated(t *testing.T) {
}
})
t.Run("pythagoras.gif", func(t *testing.T) {
- if f, err := NewMediaFile(filepath.Join(conf.ExamplesPath(), "pythagoras.gif")); err != nil {
+ if f, err := NewMediaFile(filepath.Join(cnf.ExamplesPath(), "pythagoras.gif")); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, true, f.IsImage())
@@ -1516,7 +1523,7 @@ func TestMediaFile_IsAnimated(t *testing.T) {
}
})
t.Run("christmas.mp4", func(t *testing.T) {
- if f, err := NewMediaFile(filepath.Join(conf.ExamplesPath(), "christmas.mp4")); err != nil {
+ if f, err := NewMediaFile(filepath.Join(cnf.ExamplesPath(), "christmas.mp4")); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, false, f.IsImage())
diff --git a/pkg/fs/file_exts.go b/pkg/fs/file_exts.go
index 2f13d0cfd..ab7796316 100644
--- a/pkg/fs/file_exts.go
+++ b/pkg/fs/file_exts.go
@@ -19,6 +19,7 @@ var Extensions = FileExtensions{
".thm": ImageJPEG,
".tif": ImageTIFF,
".tiff": ImageTIFF,
+ ".psd": ImagePSD,
".png": ImagePNG,
".pn": ImagePNG,
".gif": ImageGIF,
@@ -105,6 +106,8 @@ var Extensions = FileExtensions{
".asf": VideoASF,
".wmv": VideoWMV,
".svg": VectorSVG,
+ ".pdf": VectorPDF,
+ ".ai": VectorAI,
".ps": VectorPS,
".ps2": VectorPS,
".ps3": VectorPS,
diff --git a/pkg/fs/file_types.go b/pkg/fs/file_types.go
index 5cac25c1c..f6e7ced54 100644
--- a/pkg/fs/file_types.go
+++ b/pkg/fs/file_types.go
@@ -17,6 +17,7 @@ const (
ImagePNG Type = "png" // PNG image
ImageGIF Type = "gif" // GIF image
ImageTIFF Type = "tiff" // TIFF image
+ ImagePSD Type = "psd" // Adobe Photoshop
ImageDNG Type = "dng" // Adobe Digital Negative image
ImageAVIF Type = "avif" // AV1 Image File Format (AVIF)
ImageHEIF Type = "heif" // High Efficiency Image File Format (HEIF)
@@ -45,6 +46,8 @@ const (
VideoASF Type = "asf" // Advanced Systems/Streaming Format (ASF)
VideoWMV Type = "wmv" // Windows Media Video (based on ASF)
VectorSVG Type = "svg" // Scalable Vector Graphics
+ VectorPDF Type = "pdf" // Portable Document Format
+ VectorAI Type = "ai" // Adobe Illustrator
VectorPS Type = "ps" // Adobe PostScript
VectorEPS Type = "eps" // Encapsulated PostScript
SidecarXMP Type = "xmp" // Adobe XMP sidecar file (XML)
diff --git a/pkg/fs/file_types_info.go b/pkg/fs/file_types_info.go
index 73fd63469..b6ce08bcc 100644
--- a/pkg/fs/file_types_info.go
+++ b/pkg/fs/file_types_info.go
@@ -8,6 +8,7 @@ var TypeInfo = map[Type]string{
ImagePNG: "Portable Network Graphics",
ImageGIF: "Graphics Interchange Format",
ImageTIFF: "Tag Image File Format",
+ ImagePSD: "Adobe Photoshop",
ImageBMP: "Bitmap",
ImageMPO: "Stereoscopic JPEG (3D)",
ImageAVIF: "AV1 Image File Format",
@@ -35,6 +36,8 @@ var TypeInfo = map[Type]string{
VideoBDAV: "Blu-ray MPEG-2 Transport Stream",
VideoOGV: "Ogg Media (OGG)",
VectorSVG: "Scalable Vector Graphics",
+ VectorPDF: "Portable Document Format",
+ VectorAI: "Adobe Illustrator",
VectorPS: "Adobe PostScript",
VectorEPS: "Encapsulated PostScript",
SidecarXMP: "Adobe Extensible Metadata Platform",
diff --git a/pkg/fs/mime.go b/pkg/fs/mime.go
index c06ce5f8d..1eb23ad41 100644
--- a/pkg/fs/mime.go
+++ b/pkg/fs/mime.go
@@ -21,6 +21,7 @@ const (
MimeTypeMP4 = "video/mp4"
MimeTypeMOV = "video/quicktime"
MimeTypeSVG = "image/svg+xml"
+ MimeTypeAI = "application/vnd.adobe.illustrator"
MimeTypePS = "application/ps"
MimeTypeEPS = "image/eps"
MimeTypeXML = "text/xml"
@@ -29,22 +30,23 @@ const (
// MimeType returns the mime type of a file, or an empty string if it could not be detected.
func MimeType(filename string) (mimeType string) {
- // Workaround, since "image/dng" cannot be recognized yet.
- if ext := Extensions[strings.ToLower(filepath.Ext(filename))]; ext == "" {
- // Continue.
- } else if ext == ImageDNG {
+ // Workaround for types that cannot be reliably detected.
+ switch Extensions[strings.ToLower(filepath.Ext(filename))] {
+ case ImageDNG:
return MimeTypeDNG
- } else if ext == ImageAVIF {
+ case MimeTypeAVIF:
return MimeTypeAVIF
- } else if ext == VideoMP4 {
+ case VideoMP4:
return MimeTypeMP4
- } else if ext == VideoMOV {
+ case MimeTypeMOV:
return MimeTypeMOV
- } else if ext == VectorSVG {
+ case VectorSVG:
return MimeTypeSVG
- } else if ext == VectorPS {
+ case VectorAI:
+ return MimeTypeAI
+ case VectorPS:
return MimeTypePS
- } else if ext == VectorEPS {
+ case VectorEPS:
return MimeTypeEPS
}
diff --git a/pkg/media/formats.go b/pkg/media/formats.go
index 7e587b024..2cfcaee12 100644
--- a/pkg/media/formats.go
+++ b/pkg/media/formats.go
@@ -10,6 +10,7 @@ var Formats = map[fs.Type]Type{
fs.ImagePNG: Image,
fs.ImageGIF: Image,
fs.ImageTIFF: Image,
+ fs.ImagePSD: Image,
fs.ImageBMP: Image,
fs.ImageMPO: Image,
fs.ImageAVIF: Image,
@@ -36,6 +37,8 @@ var Formats = map[fs.Type]Type{
fs.VideoASF: Video,
fs.VideoWMV: Video,
fs.VectorSVG: Vector,
+ fs.VectorAI: Vector,
+ fs.VectorPDF: Vector,
fs.VectorPS: Vector,
fs.VectorEPS: Vector,
fs.SidecarXMP: Sidecar,
diff --git a/pkg/txt/specialwords.go b/pkg/txt/specialwords.go
index bcdb59a39..2d5cd8378 100644
--- a/pkg/txt/specialwords.go
+++ b/pkg/txt/specialwords.go
@@ -57,11 +57,13 @@ var SpecialWords = map[string]string{
"dci": "DCI",
"xmp": "XMP",
"xml": "XML",
+ "svg": "SVG",
+ "ai": "AI",
+ "psd": "PSD",
"pdf": "PDF",
"ps": "PS",
"postscript": "PostScript",
"eps": "EPS",
- "svg": "SVG",
"mov": "MOV",
"avc": "AVC",
"wto": "WTO",
diff --git a/scripts/dist/convert/policy.xml b/scripts/dist/convert/policy.xml
index ed62059e2..328037f1c 100644
--- a/scripts/dist/convert/policy.xml
+++ b/scripts/dist/convert/policy.xml
@@ -90,6 +90,5 @@
-