Skip to content

Commit

Permalink
Refactor XMP read and write to write immich image properties using a …
Browse files Browse the repository at this point in the history
…specific namespace: xmlns:immichgo="http://ns.immich-go.com/immich-go/1.0/"
  • Loading branch information
simulot committed Nov 6, 2024
1 parent 70463e9 commit d421520
Show file tree
Hide file tree
Showing 10 changed files with 380 additions and 250 deletions.
14 changes: 14 additions & 0 deletions internal/xmp/convert/bool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package convert

import "strings"

func BoolToString(b bool) string {
if b {
return "True"
}
return "False"
}

func StringToBool(s string) bool {
return strings.ToLower(s) == "true"
}
22 changes: 21 additions & 1 deletion internal/xmp/convert/gps.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ import (
"math"
)

/*
GPSCoordinate
A Text value in the form “DDD,MM,SSk” or “DDD,MM.mmk”, where:
DDD is a number of degrees
MM is a number of minutes
SS is a number of seconds
mm is a fraction of minutes
k is a single character N, S, E, or W indicating a direction (north, south, east, west)
Leading zeros are not necessary for the for DDD, MM, and SS values. The DDD,MM.mmk form should be used
when any of the native EXIF component rational values has a denominator other than 1. There can be any
number of fractional digits.
*/

// GPSFloatToString converts a float GPS coordinate to a string in the format "48,55.68405768N"
func GPSFloatToString(coordinate float64, isLatitude bool) string {
neg := coordinate < 0
Expand Down Expand Up @@ -34,7 +50,11 @@ func GPTStringToFloat(coordinate string) (float64, error) {
var minutes float64
var direction string

_, err := fmt.Sscanf(coordinate, "%d,%f%s", &degrees, &minutes, &direction)
if len(coordinate) > 0 {
direction = string(coordinate[len(coordinate)-1])
coordinate = coordinate[:len(coordinate)-1]
}
_, err := fmt.Sscanf(coordinate, "%d,%f", &degrees, &minutes)
if err != nil {
return 0, err
}
Expand Down
12 changes: 12 additions & 0 deletions internal/xmp/convert/int.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package convert

import "strconv"

func IntToString(i int) string {
return strconv.Itoa(i)
}

func StringToInt(s string) int {
i, _ := strconv.Atoi(s)
return i
}
52 changes: 52 additions & 0 deletions internal/xmp/convert/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,58 @@ package convert

import "time"

/*
exif:DateTimeOriginalDateInternal
EXIF tags 36867, 0x9003 (primary) and 37521, 0x9291 (subseconds). Date and time when original image was generated, in ISO 8601 format. Includes the EXIF
SubSecTimeOriginal data.
Note that EXIF date-time values have no time zone information.
exif:GPSTimeStampDateInternalGPS tag 29 (date), 0x1D, and, and GPS tag 7 (time), 0x07.
Time stamp of GPS data, in Coordinated Universal
Time.
The GPSDateStamp tag is new in EXIF 2.2. The GPS
timestamp in EXIF 2.1 does not include a date. If not
present, the date component for the XMP should be
taken from exif:DateTimeOriginal, or if that is also
lacking from exif:DateTimeDigitized. If no date is
available, do not write exif:GPSTimeStamp to XMP.
*/

/*
Date
A date-time value which is represented using a subset of ISO RFC 8601 formatting, as described in
http://www.w3.org/TR/Note-datetime.html. The following formats are supported:
YYYY
YYYY-MM
YYYY-MM-DD
YYYY-MM-DDThh:mmTZD
YYYY-MM-DDThh:mm:ssTZD
YYYY-MM-DDThh:mm:ss.sTZD
YYYY = four-digit year
MM = two-digit month (01=January)
DD = two-digit day of month (01 through 31)
hh = two digits of hour (00 through 23)
mm = two digits of minute (00 through 59)
ss = two digits of second (00 through 59)
s = one or more digits representing a decimal fraction of a second
TZD = time zone designator (Z or +hh:mm or -hh:mm)
The time zone designator is optional in XMP. When not present, the time zone is unknown, and software
should not assume anything about the missing time zone.
It is recommended, when working with local times, that you use a time zone designator of +hh:mm or
-hh:mm instead of Z, to aid human readability. For example, if you know a file was saved at noon on
October 23 a timestamp of 2004-10-23T12:00:00-06:00 is more understandable than
2004-10-23T18:00:00Z.
*/

const xmpTimeLayout = "2006-01-02T15:04:05.000-07:00"

func TimeStringToTime(t string, l *time.Location) (time.Time, error) {
Expand Down
88 changes: 34 additions & 54 deletions internal/xmp/xmpreader/DATA/image01.jpg.xmp
Original file line number Diff line number Diff line change
@@ -1,55 +1,35 @@
<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 12.97'>
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>

<rdf:Description rdf:about=''
xmlns:dc='http://purl.org/dc/elements/1.1/'>
<dc:description>
<rdf:Alt>
<rdf:li xml:lang='x-default'>C&#39;est une &lt;grotte&gt;</rdf:li>
</rdf:Alt>
</dc:description>
</rdf:Description>

<rdf:Description rdf:about=''
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
<digiKam:TagsList>
<rdf:Seq>
<rdf:li>activities/spleleo</rdf:li>
<rdf:li>airshow</rdf:li>
</rdf:Seq>
</digiKam:TagsList>
</rdf:Description>

<rdf:Description rdf:about=''
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
<exif:GPSLatitude>16,33.10142023S</exif:GPSLatitude>
<exif:GPSLongitude>62,40.48970971W</exif:GPSLongitude>
</rdf:Description>

<rdf:Description rdf:about=''
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
<tiff:ImageDescription>
<rdf:Alt>
<rdf:li xml:lang='x-default'>C&#39;est une &lt;grotte&gt;</rdf:li>
</rdf:Alt>
</tiff:ImageDescription>
</rdf:Description>

<rdf:Description rdf:about=''
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
<xmp:Rating>5</xmp:Rating>
</rdf:Description>

<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:relation>
<rdf:Bag>
<rdf:li>Vacation 2024</rdf:li>
<rdf:li>Family Reunion</rdf:li>
</rdf:Bag>
</dc:relation>
</rdf:Description>

</rdf:RDF>
</x:xmpmeta>
<?xpacket end='w'?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:exif="http://ns.adobe.com/exif/1.0/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:tiff="http://ns.adobe.com/tiff/1.0/" xmlns:digikam="http://www.digikam.org/ns/1.0/" xmlns:immichgo="http://ns.immich-go.com/immich-go/1.0/" x:xmptk="immich-go version:dev, commit:none, date:unknown">
<rdf:RDF>
<rdf:Description>
<immichgo:ImmichGoProperties>
<immichgo:title>This is a title</immichgo:title>
<immichgo:DateTimeOriginal>2023-10-10T01:11:00.000-04:00</immichgo:DateTimeOriginal>
<immichgo:trashed>False</immichgo:trashed>
<immichgo:archived>False</immichgo:archived>
<immichgo:fromPartner>False</immichgo:fromPartner>
<immichgo:favorite>True</immichgo:favorite>
<immichgo:rating>3</immichgo:rating>
<immichgo:albums>
<rdf:Bag>
<rdf:Li>
<immichgo:album>
<immichgo:title>Vacation 2024</immichgo:title>
<immichgo:description>Vacation 2024 hawaii and more</immichgo:description>
<immichgo:latitude>19,49.23661N</immichgo:latitude>
<immichgo:longitude>155,28.39525W</immichgo:longitude>
</immichgo:album>
</rdf:Li>
<rdf:Li>
<immichgo:album>
<immichgo:title>Family Reunion</immichgo:title>
<immichgo:latitude>48,51.50221N</immichgo:latitude>
<immichgo:longitude>2,17.51406E</immichgo:longitude>
</immichgo:album>
</rdf:Li>
</rdf:Bag>
</immichgo:albums>
</immichgo:ImmichGoProperties>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta><?xpacket end='w'?>
55 changes: 17 additions & 38 deletions internal/xmp/xmpreader/DATA/image02.jpg.xmp
Original file line number Diff line number Diff line change
@@ -1,38 +1,17 @@
<x:xmpmeta xmlns:x="adobe:ns:meta/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:exif="http://ns.adobe.com/exif/1.0/"
xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:tiff="http://ns.adobe.com/tiff/1.0/"
xmlns:digikam="http://www.digikam.org/ns/1.0/" x:xmptk="immich-go v0.22">
<rdf:RDF>
<rdf:Description rdf:about="">
<dc:description>
<rdf:Alt>
<rdf:li xml:lang="x-default">This a description</rdf:li>
</rdf:Alt>
</dc:description>
</rdf:Description>
<rdf:Description rdf:about="">
<tiff:ImageDescription>
<rdf:Alt>
<rdf:li xml:lang="x-default">This a description</rdf:li>
</rdf:Alt>
</tiff:ImageDescription>
</rdf:Description>
<rdf:Description rdf:about="">
<digikam:TagsList>
<rdf:Seq>
<rdf:li xml:lang="x-default">tag1</rdf:li>
<rdf:li xml:lang="x-default">tag2/tag21</rdf:li>
<rdf:li xml:lang="x-default">tag3</rdf:li>
</rdf:Seq>
</digikam:TagsList>
</rdf:Description>
<rdf:Description rdf:about="">
<exif:DateTimeOriginal>2023-10-10T01:11:00.000-04:00</exif:DateTimeOriginal>
<exif:GPSLatitude>16,33.10142023S</exif:GPSLatitude>
<exif:GPSLongitude>62,40.48970971W</exif:GPSLongitude>
</rdf:Description>
<rdf:Description rdf:about="">
<xmp:Rating>3</xmp:Rating>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:exif="http://ns.adobe.com/exif/1.0/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:tiff="http://ns.adobe.com/tiff/1.0/" xmlns:digikam="http://www.digikam.org/ns/1.0/" xmlns:immichgo="http://ns.immich-go.com/immich-go/1.0/" x:xmptk="immich-go version:dev, commit:none, date:unknown">
<rdf:RDF>
<rdf:Description>
<immichgo:ImmichGoProperties>
<immichgo:DateTimeOriginal>0001-01-01T00:00:00.000+00:00</immichgo:DateTimeOriginal>
<immichgo:trashed>False</immichgo:trashed>
<immichgo:archived>False</immichgo:archived>
<immichgo:fromPartner>False</immichgo:fromPartner>
<immichgo:favorite>False</immichgo:favorite>
<immichgo:rating>5</immichgo:rating>
<immichgo:latitude>16,33.10142S</immichgo:latitude>
<immichgo:longitude>62,40.48971W</immichgo:longitude>
</immichgo:ImmichGoProperties>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta><?xpacket end='w'?>
79 changes: 57 additions & 22 deletions internal/xmp/xmpreader/read.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package xmpreader

import (
"fmt"
"io"
"regexp"
"strconv"
"strings"
"time"

"github.com/clbanning/mxj/v2"
Expand All @@ -27,11 +30,12 @@ func walk(m mxj.Map, a *assets.Asset, path string) {
walk(v, a, path+"/"+key)
case []interface{}:
path = path + "/" + key
for _, item := range v {
for i, item := range v {
p := fmt.Sprintf("%s[%d]", path, i)
if itemMap, ok := item.(map[string]interface{}); ok {
walk(itemMap, a, path)
walk(itemMap, a, p)
} else {
filter(a, path, item.(string))
filter(a, p, item.(string))
}
}
default:
Expand All @@ -40,26 +44,57 @@ func walk(m mxj.Map, a *assets.Asset, path string) {
}
}

var reAlbum = regexp.MustCompile(`/xmpmeta/RDF/Description/ImmichGoProperties/albums/Bag/Li\[(\d+)\](.*)`)

func filter(a *assets.Asset, path string, value string) {
// fmt.Printf("filter: %s, %s\n", path, value)
var err error
switch path {
case "/xmpmeta/RDF/Description/TagsList/Seq/Li":
// a.Tags = append(a.Tags, value)
case "/xmpmeta/RDF/Description/description/Alt/li/#text":
switch {
case path == "/xmpmeta/RDF/Description/ImmichGoProperties/title":
a.Title = value
case "/xmpmeta/RDF/Description/GPSLatitude":
a.Latitude, err = convert.GPTStringToFloat(value)
case "/xmpmeta/RDF/Description/GPSLongitude":
a.Longitude, err = convert.GPTStringToFloat(value)
case "/xmpmeta/RDF/Description/DateTimeOriginal":
a.CaptureDate, err = convert.TimeStringToTime(value, time.UTC)
case "/xmpmeta/RDF/Description/Rating":
a.Stars, err = strconv.Atoi(value)
case "/xmpmeta/RDF/Description/relation/Bag/li":
a.Albums = append(a.Albums, assets.Album{
Title: value,
})
case path == "/xmpmeta/RDF/Description/ImmichGoProperties/favorite":
a.Favorite = convert.StringToBool(value)
case path == "/xmpmeta/RDF/Description/ImmichGoProperties/rating":
a.Rating = convert.StringToInt(value)
case path == "/xmpmeta/RDF/Description/ImmichGoProperties/trashed":
a.Trashed = convert.StringToBool(value)
case path == "/xmpmeta/RDF/Description/ImmichGoProperties/archived":
a.Archived = convert.StringToBool(value)
case path == "/xmpmeta/RDF/Description/ImmichGoProperties/fromPartner":
a.FromPartner = convert.StringToBool(value)
case path == "/xmpmeta/RDF/Description/ImmichGoProperties/latitude":
if f, err := convert.GPTStringToFloat(value); err == nil {
a.Latitude = f
}
case path == "/xmpmeta/RDF/Description/ImmichGoProperties/longitude":
if f, err := convert.GPTStringToFloat(value); err == nil {
a.Longitude = f
}
case path == "/xmpmeta/RDF/Description/ImmichGoProperties/DateTimeOriginal":
if d, err := convert.TimeStringToTime(value, time.UTC); err == nil {
a.CaptureDate = d
}
case strings.HasPrefix(path, "/xmpmeta/RDF/Description/ImmichGoProperties/albums/Bag/Li["):
// Extract the index and the remaining pathHi,
matches := reAlbum.FindStringSubmatch(path)
if len(matches) == 3 {
index, _ := strconv.Atoi(matches[1])
remainingPath := matches[2]
if len(a.Albums) <= index {
a.Albums = append(a.Albums, make([]assets.Album, index-len(a.Albums)+1)...)
}
switch remainingPath {
case "/album/title":
a.Albums[index].Title = value
case "/album/description":
a.Albums[index].Description = value
case "/album/latitude":
if f, err := convert.GPTStringToFloat(value); err == nil {
a.Albums[index].Latitude = f
}
case "/album/longitude":
if f, err := convert.GPTStringToFloat(value); err == nil {
a.Albums[index].Longitude = f
}
}
}
}
_ = err
}
Loading

0 comments on commit d421520

Please sign in to comment.