713593da4e
You can now run "photoprism auth add" to create new client access tokens that allow external applications to use the built-in REST API. Signed-off-by: Michael Mayer <michael@photoprism.app>
295 lines
13 KiB
Go
295 lines
13 KiB
Go
package meta
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
|
)
|
|
|
|
// XmpDocument represents an XMP sidecar file.
|
|
type XmpDocument struct {
|
|
XMLName xml.Name `xml:"xmpmeta" json:"xmpmeta,omitempty"`
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
X string `xml:"x,attr" json:"x,omitempty"`
|
|
Xmptk string `xml:"xmptk,attr" json:"xmptk,omitempty"`
|
|
RDF struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Rdf string `xml:"rdf,attr" json:"rdf,omitempty"`
|
|
Description struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
About string `xml:"about,attr" json:"about,omitempty"`
|
|
Xmp string `xml:"xmp,attr" json:"xmp,omitempty"`
|
|
Aux string `xml:"aux,attr" json:"aux,omitempty"`
|
|
ExifEX string `xml:"exifEX,attr" json:"exifex,omitempty"`
|
|
Photoshop string `xml:"photoshop,attr" json:"photoshop,omitempty"`
|
|
XmpMM string `xml:"xmpMM,attr" json:"xmpmm,omitempty"`
|
|
Dc string `xml:"dc,attr" json:"dc,omitempty"`
|
|
Tiff string `xml:"tiff,attr" json:"tiff,omitempty"`
|
|
Exif string `xml:"exif,attr" json:"exif,omitempty"`
|
|
XmpRights string `xml:"xmpRights,attr" json:"xmprights,omitempty"`
|
|
Iptc4xmpCore string `xml:"Iptc4xmpCore,attr" json:"iptc4xmpcore,omitempty"`
|
|
Iptc4xmpExt string `xml:"Iptc4xmpExt,attr" json:"iptc4xmpext,omitempty"`
|
|
CreatorTool string `xml:"CreatorTool"` // ELE-L29 10.0.0.168(C431E2...
|
|
ModifyDate string `xml:"ModifyDate"` // 2020-01-01T17:28:23.89961...
|
|
CreateDate string `xml:"CreateDate"` // 2020-01-01T17:28:23
|
|
MetadataDate string `xml:"MetadataDate"` // 2020-01-01T17:28:23.89961...
|
|
Rating string `xml:"Rating"` // 4
|
|
FStopFavorite string `xml:"http://www.fstopapp.com/xmp/ favorite,attr"` // 1
|
|
Lens string `xml:"Lens"` // HUAWEI P30 Rear Main Came...
|
|
LensModel string `xml:"LensModel"` // HUAWEI P30 Rear Main Came...
|
|
DateCreated string `xml:"DateCreated"` // 2020-01-01T17:28:25.72962...
|
|
ColorMode string `xml:"ColorMode"` // 3
|
|
ICCProfile string `xml:"ICCProfile"` // sRGB IEC61966-2.1
|
|
AuthorsPosition string `xml:"AuthorsPosition"` // Maintainer
|
|
DocumentID string `xml:"DocumentID"` // 2C678C1811D7095FD79CC822B...
|
|
InstanceID string `xml:"InstanceID"` // 2C678C1811D7095FD79CC822B...
|
|
Format string `xml:"format"` // image/jpeg
|
|
Title struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Alt struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Li struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"` // Night Shift / Berlin / 20...
|
|
Lang string `xml:"lang,attr" json:"lang,omitempty"`
|
|
} `xml:"li" json:"li,omitempty"`
|
|
} `xml:"Alt" json:"alt,omitempty"`
|
|
} `xml:"title" json:"title,omitempty"`
|
|
Creator struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Seq struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Li string `xml:"li"` // Michael Mayer
|
|
} `xml:"Seq" json:"seq,omitempty"`
|
|
} `xml:"creator" json:"creator,omitempty"`
|
|
Description struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Alt struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Li struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"` // Example file for developm...
|
|
Lang string `xml:"lang,attr" json:"lang,omitempty"`
|
|
} `xml:"li" json:"li,omitempty"`
|
|
} `xml:"Alt" json:"alt,omitempty"`
|
|
} `xml:"description" json:"description,omitempty"`
|
|
Subject struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Bag struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Li []string `xml:"li"` // desk, coffee, computer
|
|
} `xml:"Bag" json:"bag,omitempty"`
|
|
Seq struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Li []string `xml:"li"` // desk, coffee, computer
|
|
} `xml:"Seq" json:"seq,omitempty"`
|
|
} `xml:"subject" json:"subject,omitempty"`
|
|
Rights struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Alt struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Li struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"` // This is an (edited) legal...
|
|
Lang string `xml:"lang,attr" json:"lang,omitempty"`
|
|
} `xml:"li" json:"li,omitempty"`
|
|
} `xml:"Alt" json:"alt,omitempty"`
|
|
} `xml:"rights" json:"rights,omitempty"`
|
|
ImageWidth string `xml:"ImageWidth"` // 3648
|
|
ImageLength string `xml:"ImageLength"` // 2736
|
|
BitsPerSample struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Seq struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Li []string `xml:"li"` // 8
|
|
} `xml:"Seq" json:"seq,omitempty"`
|
|
} `xml:"BitsPerSample" json:"bitspersample,omitempty"`
|
|
PhotometricInterpretation string `xml:"PhotometricInterpretation"` // 2
|
|
Orientation string `xml:"Orientation"` // 0
|
|
SamplesPerPixel string `xml:"SamplesPerPixel"` // 3
|
|
YCbCrPositioning string `xml:"YCbCrPositioning"` // 1
|
|
XResolution string `xml:"XResolution"` // 72/1
|
|
YResolution string `xml:"YResolution"` // 72/1
|
|
ResolutionUnit string `xml:"ResolutionUnit"` // 2
|
|
Make string `xml:"Make"` // HUAWEI
|
|
Model string `xml:"Model"` // ELE-L29
|
|
ExifVersion string `xml:"ExifVersion"` // 0210
|
|
FlashpixVersion string `xml:"FlashpixVersion"` // 0100
|
|
ColorSpace string `xml:"ColorProfile"` // 1
|
|
ComponentsConfiguration struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Seq struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Li []string `xml:"li"` // 1, 2, 3, 0
|
|
} `xml:"Seq" json:"seq,omitempty"`
|
|
} `xml:"ComponentsConfiguration" json:"componentsconfiguration,omitempty"`
|
|
CompressedBitsPerPixel string `xml:"CompressedBitsPerPixel"` // 95/100
|
|
PixelXDimension string `xml:"PixelXDimension"` // 3648
|
|
PixelYDimension string `xml:"PixelYDimension"` // 2736
|
|
DateTimeOriginal string `xml:"DateTimeOriginal"` // 2020-01-01T17:28:23
|
|
ExposureTime string `xml:"ExposureTime"` // 20000000/1000000000
|
|
FNumber string `xml:"FNumber"` // 180/100
|
|
ExposureProgram string `xml:"ExposureProgram"` // 2
|
|
ISOSpeedRatings struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Seq struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Li string `xml:"li"` // 200
|
|
} `xml:"Seq" json:"seq,omitempty"`
|
|
} `xml:"ISOSpeedRatings" json:"isospeedratings,omitempty"`
|
|
ShutterSpeedValue string `xml:"ShutterSpeedValue"` // 298973/10000
|
|
ApertureValue string `xml:"ApertureValue"` // 1695994/1000000
|
|
BrightnessValue string `xml:"BrightnessValue"` // 0/1
|
|
ExposureBiasValue string `xml:"ExposureBiasValue"` // 0/10
|
|
MaxApertureValue string `xml:"MaxApertureValue"` // 169/100
|
|
MeteringMode string `xml:"MeteringMode"` // 5
|
|
LightSource string `xml:"LightSource"` // 1
|
|
Flash struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
ParseType string `xml:"parseType,attr" json:"parsetype,omitempty"`
|
|
Fired string `xml:"Fired"` // False
|
|
Return string `xml:"Return"` // 0
|
|
Mode string `xml:"Mode"` // 0
|
|
Function string `xml:"Function"` // False
|
|
RedEyeMode string `xml:"RedEyeMode"` // False
|
|
} `xml:"Flash" json:"flash,omitempty"`
|
|
FocalLength string `xml:"FocalLength"` // 5580/1000
|
|
SensingMethod string `xml:"SensingMethod"` // 2
|
|
FileSource string `xml:"FileSource"` // 3
|
|
SceneType string `xml:"SceneType"` // 1
|
|
CustomRendered string `xml:"CustomRendered"` // 1
|
|
ExposureMode string `xml:"ExposureMode"` // 0
|
|
WhiteBalance string `xml:"WhiteBalance"` // 0
|
|
DigitalZoomRatio string `xml:"DigitalZoomRatio"` // 100/100
|
|
FocalLengthIn35mmFilm string `xml:"FocalLengthIn35mmFilm"` // 27
|
|
SceneCaptureType string `xml:"SceneCaptureType"` // 0
|
|
GainControl string `xml:"GainControl"` // 0
|
|
Contrast string `xml:"Contrast"` // 0
|
|
Saturation string `xml:"Saturation"` // 0
|
|
Sharpness string `xml:"Sharpness"` // 0
|
|
SubjectDistanceRange string `xml:"SubjectDistanceRange"` // 0
|
|
SubSecTime string `xml:"SubSecTime"` // 899614
|
|
SubSecTimeOriginal string `xml:"SubSecTimeOriginal"` // 899614
|
|
SubSecTimeDigitized string `xml:"SubSecTimeDigitized"` // 899614
|
|
GPSVersionID string `xml:"GPSVersionID"` // 2.2.0.0
|
|
GPSLatitude string `xml:"GPSLatitude"` // 52,27.5814N
|
|
GPSLongitude string `xml:"GPSLongitude"` // 13,19.3099E
|
|
GPSAltitudeRef string `xml:"GPSAltitudeRef"` // 1
|
|
GPSAltitude string `xml:"GPSAltitude"` // 0/100
|
|
GPSTimeStamp string `xml:"GPSTimeStamp"` // 2020-01-01T16:28:22Z
|
|
Marked string `xml:"Marked"` // False
|
|
WebStatement string `xml:"WebStatement"` // http://docs.photoprism.or...
|
|
CreatorContactInfo struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
ParseType string `xml:"parseType,attr" json:"parsetype,omitempty"`
|
|
CiAdrExtadr string `xml:"CiAdrExtadr"` // Zimmermannstr. 37
|
|
CiAdrCity string `xml:"CiAdrCity"` // Berlin
|
|
CiAdrPcode string `xml:"CiAdrPcode"` // 12163
|
|
CiAdrCtry string `xml:"CiAdrCtry"` // Germany
|
|
CiTelWork string `xml:"CiTelWork"` // +49123456789
|
|
CiEmailWork string `xml:"CiEmailWork"` // hello@photoprism.org
|
|
CiUrlWork string `xml:"CiUrlWork"` // https://photoprism.org/
|
|
} `xml:"CreatorContactInfo" json:"creatorcontactinfo,omitempty"`
|
|
PersonInImage struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Bag struct {
|
|
Text string `xml:",chardata" json:"text,omitempty"`
|
|
Li string `xml:"li"` // Gopher
|
|
} `xml:"Bag" json:"bag,omitempty"`
|
|
} `xml:"PersonInImage" json:"personinimage,omitempty"`
|
|
} `xml:"Description" json:"description,omitempty"`
|
|
} `xml:"RDF" json:"rdf,omitempty"`
|
|
}
|
|
|
|
// Load parses an XMP file and populates document values with its contents.
|
|
func (doc *XmpDocument) Load(filename string) error {
|
|
data, err := os.ReadFile(filename)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return xml.Unmarshal(data, doc)
|
|
}
|
|
|
|
// Title returns the XMP document title.
|
|
func (doc *XmpDocument) Title() string {
|
|
t := doc.RDF.Description.Title.Alt.Li.Text
|
|
t2 := doc.RDF.Description.Title.Text
|
|
if t != "" {
|
|
return SanitizeTitle(t)
|
|
} else if t2 != "" {
|
|
return SanitizeTitle(t2)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Artist returns the XMP document artist.
|
|
func (doc *XmpDocument) Artist() string {
|
|
return SanitizeString(doc.RDF.Description.Creator.Seq.Li)
|
|
}
|
|
|
|
// Description returns the XMP document description.
|
|
func (doc *XmpDocument) Description() string {
|
|
d := doc.RDF.Description.Description.Alt.Li.Text
|
|
d2 := doc.RDF.Description.Description.Text
|
|
if d != "" {
|
|
return SanitizeDescription(d)
|
|
} else if d2 != "" {
|
|
return SanitizeTitle(d2)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Copyright returns the XMP document copyright info.
|
|
func (doc *XmpDocument) Copyright() string {
|
|
return SanitizeString(doc.RDF.Description.Rights.Alt.Li.Text)
|
|
}
|
|
|
|
// CameraMake returns the XMP document camera make name.
|
|
func (doc *XmpDocument) CameraMake() string {
|
|
return SanitizeString(doc.RDF.Description.Make)
|
|
}
|
|
|
|
// CameraModel returns the XMP document camera model name.
|
|
func (doc *XmpDocument) CameraModel() string {
|
|
return SanitizeString(doc.RDF.Description.Model)
|
|
}
|
|
|
|
// LensModel returns the XMP document lens model name.
|
|
func (doc *XmpDocument) LensModel() string {
|
|
return SanitizeString(doc.RDF.Description.LensModel)
|
|
}
|
|
|
|
// TakenAt returns the XMP document taken date.
|
|
func (doc *XmpDocument) TakenAt(timeZone string) time.Time {
|
|
taken := time.Time{} // Unknown
|
|
|
|
s := SanitizeString(doc.RDF.Description.DateCreated)
|
|
|
|
if s == "" {
|
|
return taken
|
|
}
|
|
|
|
if dateTime := txt.ParseTime(s, timeZone); !dateTime.IsZero() {
|
|
return dateTime
|
|
}
|
|
|
|
return taken
|
|
}
|
|
|
|
// Keywords returns the XMP document keywords.
|
|
func (doc *XmpDocument) Keywords() string {
|
|
s := doc.RDF.Description.Subject.Seq.Li
|
|
|
|
return strings.Join(s, ", ")
|
|
}
|
|
|
|
// Favorite returns a favorite status in the XMP document.
|
|
func (doc *XmpDocument) Favorite() bool {
|
|
fstop := doc.RDF.Description.FStopFavorite
|
|
if fstop == "1" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|