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 @@ -