Adobe: Add support for PDF, AI, and PSD file formats #1177 #2207

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2023-02-13 20:02:26 +01:00
parent 7f08efe369
commit e533aa7beb
14 changed files with 108 additions and 80 deletions

View file

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

View file

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

View file

@ -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...))
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -90,6 +90,5 @@
<!-- in order to avoid to get image with password text -->
<policy domain="path" rights="none" pattern="@*"/>
<!-- disable pdf and xps format types -->
<policy domain="coder" rights="none" pattern="PDF" />
<policy domain="coder" rights="none" pattern="XPS" />
</policymap>