Metadata: Ensure GPS lat/lng are within a valid range #2109
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
6c71ad3c65
commit
3553f84872
|
@ -107,8 +107,7 @@ func (data *Data) Exif(fileName string, fileFormat fs.Type, bruteForce bool) (er
|
||||||
log.Debugf("metadata: %s in %s (exif gps-info)", err, logName)
|
log.Debugf("metadata: %s in %s (exif gps-info)", err, logName)
|
||||||
} else {
|
} else {
|
||||||
if !math.IsNaN(gi.Latitude.Decimal()) && !math.IsNaN(gi.Longitude.Decimal()) {
|
if !math.IsNaN(gi.Latitude.Decimal()) && !math.IsNaN(gi.Longitude.Decimal()) {
|
||||||
data.Lat = float32(gi.Latitude.Decimal())
|
data.Lat, data.Lng = NormalizeGPS(gi.Latitude.Decimal(), gi.Longitude.Decimal())
|
||||||
data.Lng = float32(gi.Longitude.Decimal())
|
|
||||||
} else if gi.Altitude != 0 || !gi.Timestamp.IsZero() {
|
} else if gi.Altitude != 0 || !gi.Timestamp.IsZero() {
|
||||||
log.Warnf("metadata: invalid exif gps coordinates in %s (%s)", logName, clean.Log(gi.String()))
|
log.Warnf("metadata: invalid exif gps coordinates in %s (%s)", logName, clean.Log(gi.String()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -628,6 +628,7 @@ func TestExif(t *testing.T) {
|
||||||
assert.Equal(t, "", data.Projection)
|
assert.Equal(t, "", data.Projection)
|
||||||
assert.Equal(t, "", data.ColorProfile)
|
assert.Equal(t, "", data.ColorProfile)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("animated.gif", func(t *testing.T) {
|
t.Run("animated.gif", func(t *testing.T) {
|
||||||
_, err := Exif("testdata/animated.gif", fs.ImageGIF, true)
|
_, err := Exif("testdata/animated.gif", fs.ImageGIF, true)
|
||||||
|
|
||||||
|
@ -637,6 +638,7 @@ func TestExif(t *testing.T) {
|
||||||
assert.Equal(t, "found no exif data", err.Error())
|
assert.Equal(t, "found no exif data", err.Error())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("aurora.jpg", func(t *testing.T) {
|
t.Run("aurora.jpg", func(t *testing.T) {
|
||||||
data, err := Exif("testdata/aurora.jpg", fs.ImageJPEG, false)
|
data, err := Exif("testdata/aurora.jpg", fs.ImageJPEG, false)
|
||||||
|
|
||||||
|
@ -651,4 +653,20 @@ func TestExif(t *testing.T) {
|
||||||
assert.Equal(t, float32(0), data.Lat)
|
assert.Equal(t, float32(0), data.Lat)
|
||||||
assert.Equal(t, float32(0), data.Lng)
|
assert.Equal(t, float32(0), data.Lng)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("buggy_panorama.jpg", func(t *testing.T) {
|
||||||
|
data, err := Exif("testdata/buggy_panorama.jpg", fs.ImageJPEG, false)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "2022-04-24 10:35:53 +0000 UTC", data.TakenAtLocal.String())
|
||||||
|
assert.Equal(t, "2022-04-24 02:35:53 +0000 UTC", data.TakenAt.String())
|
||||||
|
assert.Equal(t, "Asia/Shanghai", data.TimeZone) // Local Time
|
||||||
|
assert.Equal(t, 1, data.Orientation)
|
||||||
|
assert.Equal(t, float32(33.640007), data.Lat)
|
||||||
|
assert.Equal(t, float32(103.48), data.Lng)
|
||||||
|
assert.Equal(t, 0, data.Altitude)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
package meta
|
package meta
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
|
||||||
|
|
||||||
"github.com/dsoprea/go-exif/v3"
|
"github.com/dsoprea/go-exif/v3"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LatMax = 90
|
||||||
|
LngMax = 180
|
||||||
)
|
)
|
||||||
|
|
||||||
var GpsCoordsRegexp = regexp.MustCompile("[0-9\\.]+")
|
var GpsCoordsRegexp = regexp.MustCompile("[0-9\\.]+")
|
||||||
|
@ -14,7 +20,7 @@ var GpsRefRegexp = regexp.MustCompile("[NSEW]+")
|
||||||
var GpsFloatRegexp = regexp.MustCompile("[+\\-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)")
|
var GpsFloatRegexp = regexp.MustCompile("[+\\-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)")
|
||||||
|
|
||||||
// GpsToLatLng returns the GPS latitude and longitude as float point number.
|
// GpsToLatLng returns the GPS latitude and longitude as float point number.
|
||||||
func GpsToLatLng(s string) (lat, lng float32) {
|
func GpsToLatLng(s string) (lat, lng float64) {
|
||||||
// Emtpy?
|
// Emtpy?
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return 0, 0
|
return 0, 0
|
||||||
|
@ -25,7 +31,7 @@ func GpsToLatLng(s string) (lat, lng float32) {
|
||||||
if lat, err := strconv.ParseFloat(fl[0], 64); err != nil {
|
if lat, err := strconv.ParseFloat(fl[0], 64); err != nil {
|
||||||
log.Infof("metadata: %s is not a valid gps position", clean.Log(fl[0]))
|
log.Infof("metadata: %s is not a valid gps position", clean.Log(fl[0]))
|
||||||
} else if lng, err := strconv.ParseFloat(fl[1], 64); err == nil {
|
} else if lng, err := strconv.ParseFloat(fl[1], 64); err == nil {
|
||||||
return float32(lat), float32(lng)
|
return lat, lng
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,11 +57,11 @@ func GpsToLatLng(s string) (lat, lng float32) {
|
||||||
Seconds: ParseFloat(co[5]),
|
Seconds: ParseFloat(co[5]),
|
||||||
}
|
}
|
||||||
|
|
||||||
return float32(latDeg.Decimal()), float32(lngDeg.Decimal())
|
return latDeg.Decimal(), lngDeg.Decimal()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GpsToDecimal returns the GPS latitude or longitude as decimal float point number.
|
// GpsToDecimal returns the GPS latitude or longitude as decimal float point number.
|
||||||
func GpsToDecimal(s string) float32 {
|
func GpsToDecimal(s string) float64 {
|
||||||
// Emtpy?
|
// Emtpy?
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return 0
|
return 0
|
||||||
|
@ -63,7 +69,7 @@ func GpsToDecimal(s string) float32 {
|
||||||
|
|
||||||
// Floating point number?
|
// Floating point number?
|
||||||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||||
return float32(f)
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse string value.
|
// Parse string value.
|
||||||
|
@ -81,7 +87,7 @@ func GpsToDecimal(s string) float32 {
|
||||||
Seconds: ParseFloat(co[2]),
|
Seconds: ParseFloat(co[2]),
|
||||||
}
|
}
|
||||||
|
|
||||||
return float32(latDeg.Decimal())
|
return latDeg.Decimal()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseFloat returns a single GPS coordinate value as floating point number (degree, minute or second).
|
// ParseFloat returns a single GPS coordinate value as floating point number (degree, minute or second).
|
||||||
|
@ -99,3 +105,43 @@ func ParseFloat(s string) float64 {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NormalizeGPS normalizes the longitude and latitude of the GPS position to a generally valid range.
|
||||||
|
func NormalizeGPS(lat, lng float64) (float32, float32) {
|
||||||
|
if lat < LatMax || lat > LatMax || lng < LngMax || lng > LngMax {
|
||||||
|
// Clip the latitude. Normalise the longitude.
|
||||||
|
lat, lng = clipLat(lat), normalizeLng(lng)
|
||||||
|
}
|
||||||
|
|
||||||
|
return float32(lat), float32(lng)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clipLat(lat float64) float64 {
|
||||||
|
if lat > LatMax*2 {
|
||||||
|
return math.Mod(lat, LatMax)
|
||||||
|
} else if lat > LatMax {
|
||||||
|
return lat - LatMax
|
||||||
|
}
|
||||||
|
|
||||||
|
if lat < -LatMax*2 {
|
||||||
|
return math.Mod(lat, LatMax)
|
||||||
|
} else if lat < -LatMax {
|
||||||
|
return lat + LatMax
|
||||||
|
}
|
||||||
|
|
||||||
|
return lat
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeLng(value float64) float64 {
|
||||||
|
return normalizeCoord(value, LngMax)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCoord(value, max float64) float64 {
|
||||||
|
for value < -max {
|
||||||
|
value += 2 * max
|
||||||
|
}
|
||||||
|
for value >= max {
|
||||||
|
value -= 2 * max
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
|
@ -8,63 +8,54 @@ import (
|
||||||
|
|
||||||
func TestGpsToLat(t *testing.T) {
|
func TestGpsToLat(t *testing.T) {
|
||||||
lat := GpsToDecimal("51 deg 15' 17.47\" N")
|
lat := GpsToDecimal("51 deg 15' 17.47\" N")
|
||||||
exp := float32(51.254852)
|
exp := 51.254852
|
||||||
|
|
||||||
if lat-exp > 0 {
|
assert.InEpsilon(t, lat, exp, 0.1)
|
||||||
t.Fatalf("lat is %f, should be %f", lat, exp)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGpsToLng(t *testing.T) {
|
func TestGpsToLng(t *testing.T) {
|
||||||
lng := GpsToDecimal("7 deg 23' 22.09\" E")
|
lng := GpsToDecimal("7 deg 23' 22.09\" E")
|
||||||
exp := float32(7.389470)
|
exp := 7.389470
|
||||||
|
|
||||||
if lng-exp > 0 {
|
assert.InEpsilon(t, lng, exp, 0.1)
|
||||||
t.Fatalf("lng is %f, should be %f", lng, exp)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGpsToLatLng(t *testing.T) {
|
func TestGpsToLatLng(t *testing.T) {
|
||||||
t.Run("valid string", func(t *testing.T) {
|
t.Run("valid string", func(t *testing.T) {
|
||||||
lat, lng := GpsToLatLng("51 deg 15' 17.47\" N, 7 deg 23' 22.09\" E")
|
lat, lng := GpsToLatLng("51 deg 15' 17.47\" N, 7 deg 23' 22.09\" E")
|
||||||
expLat, expLng := float32(51.254852), float32(7.389470)
|
expLat, expLng := 51.254852, 7.389470
|
||||||
|
|
||||||
if lat-expLat > 0 {
|
assert.InEpsilon(t, lat, expLat, 0.1)
|
||||||
t.Fatalf("lat is %f, should be %f", lat, expLat)
|
assert.InEpsilon(t, lng, expLng, 0.1)
|
||||||
}
|
|
||||||
|
|
||||||
if lng-expLng > 0 {
|
|
||||||
t.Fatalf("lng is %f, should be %f", lng, expLng)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("empty string", func(t *testing.T) {
|
t.Run("empty string", func(t *testing.T) {
|
||||||
lat, lng := GpsToLatLng("")
|
lat, lng := GpsToLatLng("")
|
||||||
assert.Equal(t, float32(0), lat)
|
assert.Equal(t, float64(0), lat)
|
||||||
assert.Equal(t, float32(0), lng)
|
assert.Equal(t, float64(0), lng)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("invalid string", func(t *testing.T) {
|
t.Run("invalid string", func(t *testing.T) {
|
||||||
lat, lng := GpsToLatLng("abc bdf")
|
lat, lng := GpsToLatLng("abc bdf")
|
||||||
assert.Equal(t, float32(0), lat)
|
assert.Equal(t, float64(0), lat)
|
||||||
assert.Equal(t, float32(0), lng)
|
assert.Equal(t, float64(0), lng)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGpsToDecimal(t *testing.T) {
|
func TestGpsToDecimal(t *testing.T) {
|
||||||
t.Run("valid string", func(t *testing.T) {
|
t.Run("valid string", func(t *testing.T) {
|
||||||
r := GpsToDecimal("51 deg 15' 17.47\" N")
|
r := GpsToDecimal("51 deg 15' 17.47\" N")
|
||||||
assert.Equal(t, float32(51.254852), r)
|
assert.InEpsilon(t, 51.25485277777778, r, 0.01)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("empty string", func(t *testing.T) {
|
t.Run("empty string", func(t *testing.T) {
|
||||||
r := GpsToDecimal("")
|
r := GpsToDecimal("")
|
||||||
assert.Equal(t, float32(0), r)
|
assert.Equal(t, float64(0), r)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("invalid string", func(t *testing.T) {
|
t.Run("invalid string", func(t *testing.T) {
|
||||||
r := GpsToDecimal("abc")
|
r := GpsToDecimal("abc")
|
||||||
assert.Equal(t, float32(0), r)
|
assert.Equal(t, float64(0), r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -174,10 +174,10 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
|
||||||
// Set latitude and longitude if known and not already set.
|
// Set latitude and longitude if known and not already set.
|
||||||
if data.Lat == 0 && data.Lng == 0 {
|
if data.Lat == 0 && data.Lng == 0 {
|
||||||
if data.GPSPosition != "" {
|
if data.GPSPosition != "" {
|
||||||
data.Lat, data.Lng = GpsToLatLng(data.GPSPosition)
|
lat, lng := GpsToLatLng(data.GPSPosition)
|
||||||
|
data.Lat, data.Lng = NormalizeGPS(lat, lng)
|
||||||
} else if data.GPSLatitude != "" && data.GPSLongitude != "" {
|
} else if data.GPSLatitude != "" && data.GPSLongitude != "" {
|
||||||
data.Lat = GpsToDecimal(data.GPSLatitude)
|
data.Lat, data.Lng = NormalizeGPS(GpsToDecimal(data.GPSLatitude), GpsToDecimal(data.GPSLongitude))
|
||||||
data.Lng = GpsToDecimal(data.GPSLongitude)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1259,4 +1259,23 @@ func TestJSON(t *testing.T) {
|
||||||
assert.Equal(t, "iPhone 14 Pro Max", data.CameraModel)
|
assert.Equal(t, "iPhone 14 Pro Max", data.CameraModel)
|
||||||
assert.Equal(t, "iPhone 14 Pro Max back triple camera 9mm f/2.8", data.LensModel)
|
assert.Equal(t, "iPhone 14 Pro Max back triple camera 9mm f/2.8", data.LensModel)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("buggy_panorama.json", func(t *testing.T) {
|
||||||
|
data, err := JSON("testdata/buggy_panorama.json", "")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// t.Logf("DATA: %+v", data)
|
||||||
|
|
||||||
|
assert.Equal(t, CodecJpeg, data.Codec)
|
||||||
|
assert.Equal(t, "2022-04-24 10:35:53 +0000 UTC", data.TakenAtLocal.String())
|
||||||
|
assert.Equal(t, "2022-04-24 02:35:53 +0000 UTC", data.TakenAt.String())
|
||||||
|
assert.Equal(t, "Asia/Shanghai", data.TimeZone) // Local Time
|
||||||
|
assert.Equal(t, 1, data.Orientation)
|
||||||
|
assert.Equal(t, float32(33.640007), data.Lat)
|
||||||
|
assert.Equal(t, float32(103.48), data.Lng)
|
||||||
|
assert.Equal(t, 0, data.Altitude)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
BIN
internal/meta/testdata/buggy_panorama.jpg
vendored
Normal file
BIN
internal/meta/testdata/buggy_panorama.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
96
internal/meta/testdata/buggy_panorama.json
vendored
Normal file
96
internal/meta/testdata/buggy_panorama.json
vendored
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
[{
|
||||||
|
"SourceFile": "buggy_panorama.jpg",
|
||||||
|
"ExifToolVersion": 12.40,
|
||||||
|
"FileName": "buggy_panorama.jpg",
|
||||||
|
"Directory": ".",
|
||||||
|
"FileSize": 11703,
|
||||||
|
"FileModifyDate": "2022:12:28 18:08:25+00:00",
|
||||||
|
"FileAccessDate": "2022:12:28 18:08:25+00:00",
|
||||||
|
"FileInodeChangeDate": "2022:12:28 18:08:25+00:00",
|
||||||
|
"FilePermissions": 100644,
|
||||||
|
"FileType": "JPEG",
|
||||||
|
"FileTypeExtension": "JPG",
|
||||||
|
"MIMEType": "image/jpeg",
|
||||||
|
"JFIFVersion": "1 1",
|
||||||
|
"ExifByteOrder": "MM",
|
||||||
|
"Make": "Xiaomi",
|
||||||
|
"Model": "Mi MIX 2",
|
||||||
|
"XResolution": 96,
|
||||||
|
"YResolution": 96,
|
||||||
|
"ResolutionUnit": 2,
|
||||||
|
"Software": "paint.net 4.3.12",
|
||||||
|
"ModifyDate": "2022:04:24 10:35:53",
|
||||||
|
"DateTimeOriginal": "2022:04:24 10:35:53",
|
||||||
|
"CreateDate": "2022:04:24 10:35:53",
|
||||||
|
"OffsetTime": "+02:00",
|
||||||
|
"OffsetTimeOriginal": "+02:00",
|
||||||
|
"OffsetTimeDigitized": "+02:00",
|
||||||
|
"LightSource": 0,
|
||||||
|
"SubSecTime": 358,
|
||||||
|
"SubSecTimeOriginal": 358,
|
||||||
|
"SubSecTimeDigitized": 358,
|
||||||
|
"GPSLatitudeRef": "N",
|
||||||
|
"GPSLongitudeRef": "E",
|
||||||
|
"GPSAltitudeRef": 0,
|
||||||
|
"GPSTimeStamp": "08:35:46",
|
||||||
|
"GPSDateStamp": "2022:04:24",
|
||||||
|
"ProfileCMMType": "",
|
||||||
|
"ProfileVersion": 1024,
|
||||||
|
"ProfileClass": "mntr",
|
||||||
|
"ColorSpaceData": "RGB ",
|
||||||
|
"ProfileConnectionSpace": "XYZ ",
|
||||||
|
"ProfileDateTime": "2016:12:08 09:38:28",
|
||||||
|
"ProfileFileSignature": "acsp",
|
||||||
|
"PrimaryPlatform": "",
|
||||||
|
"CMMFlags": 0,
|
||||||
|
"DeviceManufacturer": "GOOG",
|
||||||
|
"DeviceModel": "",
|
||||||
|
"DeviceAttributes": "0 0",
|
||||||
|
"RenderingIntent": 0,
|
||||||
|
"ConnectionSpaceIlluminant": "0.9642 1 0.82491",
|
||||||
|
"ProfileCreator": "GOOG",
|
||||||
|
"ProfileID": "117 225 166 177 60 52 55 99 16 200 171 102 6 50 162 138",
|
||||||
|
"ProfileDescription": "sRGB IEC61966-2.1",
|
||||||
|
"ProfileCopyright": "Copyright (c) 2016 Google Inc.",
|
||||||
|
"MediaWhitePoint": "0.95045 1 1.08905",
|
||||||
|
"MediaBlackPoint": "0 0 0",
|
||||||
|
"RedMatrixColumn": "0.43604 0.22249 0.01392",
|
||||||
|
"GreenMatrixColumn": "0.38512 0.7169 0.09706",
|
||||||
|
"BlueMatrixColumn": "0.14305 0.06061 0.71391",
|
||||||
|
"RedTRC": "(Binary data 32 bytes, use -b option to extract)",
|
||||||
|
"ChromaticAdaptation": "1.04788 0.02292 -0.05019 0.02959 0.99048 -0.01704 -0.00922 0.01508 0.75168",
|
||||||
|
"BlueTRC": "(Binary data 32 bytes, use -b option to extract)",
|
||||||
|
"GreenTRC": "(Binary data 32 bytes, use -b option to extract)",
|
||||||
|
"XMPToolkit": "Adobe XMP Core 5.1.0-jc003",
|
||||||
|
"UsePanoramaViewer": true,
|
||||||
|
"IsPhotosphere": true,
|
||||||
|
"ProjectionType": "equirectangular",
|
||||||
|
"CroppedAreaImageHeightPixels": 2398,
|
||||||
|
"CroppedAreaImageWidthPixels": 6872,
|
||||||
|
"FullPanoHeightPixels": 7414,
|
||||||
|
"FullPanoWidthPixels": 14828,
|
||||||
|
"CroppedAreaTopPixels": 2488,
|
||||||
|
"CroppedAreaLeftPixels": 4120,
|
||||||
|
"FirstPhotoDate": "2022:04:24 08:35:34.563Z",
|
||||||
|
"LastPhotoDate": "2022:04:24 08:35:43.488Z",
|
||||||
|
"SourcePhotosCount": 6,
|
||||||
|
"PoseHeadingDegrees": 207.0,
|
||||||
|
"LargestValidInteriorRectLeft": 0,
|
||||||
|
"LargestValidInteriorRectTop": 0,
|
||||||
|
"LargestValidInteriorRectWidth": 6872,
|
||||||
|
"LargestValidInteriorRectHeight": 2398,
|
||||||
|
"ImageWidth": 500,
|
||||||
|
"ImageHeight": 174,
|
||||||
|
"EncodingProcess": 0,
|
||||||
|
"BitsPerSample": 8,
|
||||||
|
"ColorComponents": 1,
|
||||||
|
"ImageSize": "500 174",
|
||||||
|
"Megapixels": 0.087,
|
||||||
|
"SubSecCreateDate": "2022:04:24 10:35:53.358+02:00",
|
||||||
|
"SubSecDateTimeOriginal": "2022:04:24 10:35:53.358+02:00",
|
||||||
|
"SubSecModifyDate": "2022:04:24 10:35:53.358+02:00",
|
||||||
|
"GPSDateTime": "2022:04:24 08:35:46Z",
|
||||||
|
"GPSLatitude": 213.640008888889,
|
||||||
|
"GPSLongitude": 103.48,
|
||||||
|
"GPSPosition": "213.640008888889 103.48"
|
||||||
|
}]
|
Loading…
Reference in a new issue