mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 08:46:18 +01:00
Switch to use exiv2 for client side exif
This commit is contained in:
parent
f1704cc37d
commit
cebabdc6a8
9 changed files with 190 additions and 140 deletions
|
@ -1,9 +1,9 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:exifdart/exifdart.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
import 'package:np_exiv2/np_exiv2.dart';
|
||||
|
||||
part 'exif.g.dart';
|
||||
|
||||
|
@ -47,9 +47,9 @@ class Exif with EquatableMixin {
|
|||
.map((e) {
|
||||
dynamic jsonValue;
|
||||
if (e.value is Rational) {
|
||||
jsonValue = e.value.toJson();
|
||||
jsonValue = (e.value as Rational).toJson();
|
||||
} else if (e.value is List) {
|
||||
jsonValue = e.value.map((e) {
|
||||
jsonValue = (e.value as List).map((e) {
|
||||
if (e is Rational) {
|
||||
return e.toJson();
|
||||
} else {
|
||||
|
@ -69,11 +69,12 @@ class Exif with EquatableMixin {
|
|||
json.entries.map((e) {
|
||||
dynamic exifValue;
|
||||
if (e.value is Map) {
|
||||
exifValue = Rational.fromJson(e.value.cast<String, dynamic>());
|
||||
exifValue =
|
||||
_rationalFromJson((e.value as Map).cast<String, dynamic>());
|
||||
} else if (e.value is List) {
|
||||
exifValue = e.value.map((e) {
|
||||
exifValue = (e.value as List).map((e) {
|
||||
if (e is Map) {
|
||||
return Rational.fromJson(e.cast<String, dynamic>());
|
||||
return _rationalFromJson(e.cast<String, dynamic>());
|
||||
} else {
|
||||
return e;
|
||||
}
|
||||
|
@ -187,3 +188,15 @@ class Exif with EquatableMixin {
|
|||
|
||||
static final dateTimeFormat = DateFormat("yyyy:MM:dd HH:mm:ss");
|
||||
}
|
||||
|
||||
extension on Rational {
|
||||
Map<String, int> toJson() => {
|
||||
"n": numerator,
|
||||
"d": denominator,
|
||||
};
|
||||
}
|
||||
|
||||
Rational _rationalFromJson(Map<String, dynamic> json) {
|
||||
return Rational(
|
||||
json["n"] ?? json["numerator"], json["d"] ?? json["denominator"]);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:exifdart/exifdart.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:nc_photos/entity/exif.dart';
|
||||
import 'package:np_exiv2/np_exiv2.dart';
|
||||
|
||||
extension ExifExtension on Exif {
|
||||
double? get gpsLatitudeDeg {
|
||||
|
|
|
@ -140,5 +140,8 @@ final supportedVideoFormatMimes =
|
|||
|
||||
const _metadataSupportedFormatMimes = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/heic",
|
||||
"image/gif",
|
||||
];
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
import 'dart:io' as io;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:exifdart/exifdart.dart' as exifdart;
|
||||
import 'package:exifdart/exifdart_io.dart';
|
||||
import 'package:exifdart/exifdart_memory.dart';
|
||||
import 'package:image_size_getter/image_size_getter.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/entity/exif.dart';
|
||||
import 'package:nc_photos/entity/file.dart' as app;
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/file_extension.dart';
|
||||
import 'package:nc_photos/image_size_getter_util.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_collection/np_collection.dart';
|
||||
import 'package:np_exiv2/np_exiv2.dart' as exiv2;
|
||||
|
||||
part 'load_metadata.g.dart';
|
||||
|
||||
|
@ -24,8 +20,7 @@ class LoadMetadata {
|
|||
Account account, app.File file, Uint8List binary) {
|
||||
return _loadMetadata(
|
||||
mime: file.contentType ?? "",
|
||||
exifdartReaderBuilder: () => MemoryBlobReader(binary),
|
||||
imageSizeGetterInputBuilder: () => AsyncMemoryInput(binary),
|
||||
reader: () => exiv2.readBuffer(binary),
|
||||
filename: file.path,
|
||||
);
|
||||
}
|
||||
|
@ -37,90 +32,70 @@ class LoadMetadata {
|
|||
mime = mime ?? await file.readMime();
|
||||
return _loadMetadata(
|
||||
mime: mime ?? "",
|
||||
exifdartReaderBuilder: () => FileReader(file),
|
||||
imageSizeGetterInputBuilder: () => AsyncFileInput(file),
|
||||
reader: () => exiv2.readFile(file.path),
|
||||
filename: file.path,
|
||||
);
|
||||
}
|
||||
|
||||
Future<app.Metadata> _loadMetadata({
|
||||
required String mime,
|
||||
required exifdart.AbstractBlobReader Function() exifdartReaderBuilder,
|
||||
required AsyncImageInput Function() imageSizeGetterInputBuilder,
|
||||
required exiv2.ReadResult Function() reader,
|
||||
String? filename,
|
||||
}) async {
|
||||
var metadata = exifdart.Metadata();
|
||||
if (file_util.isMetadataSupportedMime(mime)) {
|
||||
try {
|
||||
metadata = await exifdart.readMetadata(exifdartReaderBuilder());
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout(
|
||||
"[_loadMetadata] Failed while readMetadata for $mime file: ${logFilename(filename)}",
|
||||
e,
|
||||
stacktrace);
|
||||
// ignore exif
|
||||
}
|
||||
final exiv2.ReadResult result;
|
||||
try {
|
||||
result = reader();
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout(
|
||||
"[_loadMetadata] Failed while readMetadata for $mime file: ${logFilename(filename)}",
|
||||
e,
|
||||
stacktrace);
|
||||
rethrow;
|
||||
}
|
||||
|
||||
int imageWidth = 0, imageHeight = 0;
|
||||
if (metadata.imageWidth == null || metadata.imageHeight == null) {
|
||||
try {
|
||||
final resolution =
|
||||
await AsyncImageSizeGetter.getSize(imageSizeGetterInputBuilder());
|
||||
// image size getter doesn't handle exif orientation
|
||||
if (metadata.exif?.containsKey("Orientation") == true &&
|
||||
metadata.exif!["Orientation"] >= 5 &&
|
||||
metadata.exif!["Orientation"] <= 8) {
|
||||
// 90 deg CW/CCW
|
||||
imageWidth = resolution.height;
|
||||
imageHeight = resolution.width;
|
||||
} else {
|
||||
imageWidth = resolution.width;
|
||||
imageHeight = resolution.height;
|
||||
}
|
||||
} catch (e, stacktrace) {
|
||||
// is this even an image file?
|
||||
_log.shout(
|
||||
"[_loadMetadata] Failed while getSize for $mime file: ${logFilename(filename)}",
|
||||
e,
|
||||
stacktrace);
|
||||
}
|
||||
} else {
|
||||
if (metadata.rotateAngleCcw != null &&
|
||||
metadata.rotateAngleCcw! % 180 != 0) {
|
||||
imageWidth = metadata.imageHeight!;
|
||||
imageHeight = metadata.imageWidth!;
|
||||
} else {
|
||||
imageWidth = metadata.imageWidth!;
|
||||
imageHeight = metadata.imageHeight!;
|
||||
}
|
||||
}
|
||||
|
||||
final map = {
|
||||
if (metadata.exif != null) "exif": metadata.exif,
|
||||
if (imageWidth > 0 && imageHeight > 0)
|
||||
"resolution": {
|
||||
"width": imageWidth,
|
||||
"height": imageHeight,
|
||||
},
|
||||
final metadata = {
|
||||
...result.iptcData
|
||||
.map((e) {
|
||||
try {
|
||||
return MapEntry(e.tagKey, e.value.asTyped());
|
||||
} catch (_) {
|
||||
_log.shout(
|
||||
"[_loadMetadata] Unable to convert IPTC tag: ${e.tagKey}, ${e.value.toDebugString()}");
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.nonNulls
|
||||
.toMap(),
|
||||
...result.exifData
|
||||
.map((e) {
|
||||
try {
|
||||
return MapEntry(e.tagKey, e.value.asTyped());
|
||||
} catch (_) {
|
||||
_log.shout(
|
||||
"[_loadMetadata] Unable to convert EXIF tag: ${e.tagKey}, ${e.value.toDebugString()}");
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.nonNulls
|
||||
.toMap(),
|
||||
};
|
||||
return _buildMetadata(map);
|
||||
}
|
||||
|
||||
app.Metadata _buildMetadata(Map<String, dynamic> map) {
|
||||
int? imageWidth, imageHeight;
|
||||
Exif? exif;
|
||||
if (map.containsKey("resolution")) {
|
||||
imageWidth = map["resolution"]["width"];
|
||||
imageHeight = map["resolution"]["height"];
|
||||
}
|
||||
if (map.containsKey("exif")) {
|
||||
exif = Exif(map["exif"]);
|
||||
var imageWidth = 0, imageHeight = 0;
|
||||
// exiv2 doesn't handle exif orientation
|
||||
if (metadata.containsKey("Orientation") &&
|
||||
metadata["Orientation"] as int >= 5 &&
|
||||
metadata["Orientation"] as int <= 8) {
|
||||
// 90 deg CW/CCW
|
||||
imageWidth = result.height;
|
||||
imageHeight = result.width;
|
||||
} else {
|
||||
imageWidth = result.width;
|
||||
imageHeight = result.height;
|
||||
}
|
||||
|
||||
return app.Metadata(
|
||||
imageWidth: imageWidth,
|
||||
imageHeight: imageHeight,
|
||||
exif: exif,
|
||||
exif: metadata.isNotEmpty ? Exif(metadata) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -397,6 +397,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
euc:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: euc
|
||||
sha256: "569b21c71ee5a3aa3e96f70512cd10d1fad7b438429fade65ec2a50038a09dc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6+8"
|
||||
event_bus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -405,15 +413,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
exifdart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "1.3.0"
|
||||
resolved-ref: c91db71fbb3d3cee1c79716502dd37eadc8327ff
|
||||
url: "https://gitlab.com/nc-photos/exifdart.git"
|
||||
source: git
|
||||
version: "1.3.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1148,6 +1147,20 @@ packages:
|
|||
relative: true
|
||||
source: path
|
||||
version: "1.0.0"
|
||||
np_exiv2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../np_exiv2"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.0.0"
|
||||
np_exiv2_lib:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../np_exiv2_lib"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
np_geocoder:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -56,10 +56,6 @@ dependencies:
|
|||
dynamic_color: ^1.7.0
|
||||
equatable: ^2.0.5
|
||||
event_bus: ^2.0.0
|
||||
exifdart:
|
||||
git:
|
||||
url: https://gitlab.com/nc-photos/exifdart.git
|
||||
ref: 1.3.0
|
||||
flex_seed_scheme: ^1.5.0
|
||||
fluttertoast: ^8.2.5
|
||||
flutter_background_service:
|
||||
|
@ -107,6 +103,10 @@ dependencies:
|
|||
path: ../np_datetime
|
||||
np_db:
|
||||
path: ../np_db
|
||||
np_exiv2:
|
||||
path: ../np_exiv2
|
||||
np_exiv2_lib:
|
||||
path: ../np_exiv2_lib
|
||||
np_geocoder:
|
||||
path: ../np_geocoder
|
||||
np_gps_map:
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:exifdart/exifdart.dart';
|
||||
import 'package:nc_photos/entity/exif.dart';
|
||||
import 'package:np_exiv2/np_exiv2.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
|
@ -96,7 +96,7 @@ void main() {
|
|||
|
||||
test("Rational", () {
|
||||
final exif = Exif(<String, dynamic>{
|
||||
"XResolution": Rational(72, 1),
|
||||
"XResolution": const Rational(72, 1),
|
||||
});
|
||||
expect(exif.toJson(), <String, dynamic>{
|
||||
"XResolution": {"n": 72, "d": 1},
|
||||
|
@ -114,7 +114,11 @@ void main() {
|
|||
|
||||
test("List<Rational>", () {
|
||||
final exif = Exif(<String, dynamic>{
|
||||
"GPSLatitude": [Rational(2, 1), Rational(3, 1), Rational(4, 100)],
|
||||
"GPSLatitude": [
|
||||
const Rational(2, 1),
|
||||
const Rational(3, 1),
|
||||
const Rational(4, 100),
|
||||
],
|
||||
});
|
||||
expect(exif.toJson(), <String, dynamic>{
|
||||
"GPSLatitude": [
|
||||
|
@ -164,12 +168,20 @@ void main() {
|
|||
}));
|
||||
});
|
||||
|
||||
test("Rational", () {
|
||||
test("Rational (legacy)", () {
|
||||
final json = <String, dynamic>{
|
||||
"XResolution": {"numerator": 72, "denominator": 1},
|
||||
};
|
||||
final Rational exif = Exif.fromJson(json)["XResolution"];
|
||||
expect(exif.makeComparable(), _Rational(72, 1));
|
||||
expect(exif.makeComparable(), const _Rational(72, 1));
|
||||
});
|
||||
|
||||
test("Rational", () {
|
||||
final json = <String, dynamic>{
|
||||
"XResolution": {"n": 72, "d": 1},
|
||||
};
|
||||
final Rational exif = Exif.fromJson(json)["XResolution"];
|
||||
expect(exif.makeComparable(), const _Rational(72, 1));
|
||||
});
|
||||
|
||||
test("List<int>", () {
|
||||
|
@ -193,8 +205,11 @@ void main() {
|
|||
};
|
||||
final List<Rational> exif =
|
||||
Exif.fromJson(json)["GPSLatitude"].cast<Rational>();
|
||||
expect(exif.map((e) => e.makeComparable()).toList(),
|
||||
[_Rational(2, 1), _Rational(3, 1), _Rational(4, 100)]);
|
||||
expect(exif.map((e) => e.makeComparable()).toList(), [
|
||||
const _Rational(2, 1),
|
||||
const _Rational(3, 1),
|
||||
const _Rational(4, 100)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -212,6 +227,21 @@ void main() {
|
|||
expect(exif.dateTimeOriginal, null);
|
||||
});
|
||||
});
|
||||
|
||||
group("from Nextcloud", () {
|
||||
test("Rational", () {
|
||||
final exif = Exif({
|
||||
"ExposureTime": "123/456",
|
||||
});
|
||||
expect(exif.exposureTime?.makeComparable(), const _Rational(123, 456));
|
||||
});
|
||||
test("int", () {
|
||||
final exif = Exif({
|
||||
"ISOSpeedRatings": "1234",
|
||||
});
|
||||
expect(exif.isoSpeedRatings, 1234);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -220,7 +250,7 @@ extension on Rational {
|
|||
}
|
||||
|
||||
class _Rational extends Rational with EquatableMixin {
|
||||
_Rational(super.numerator, super.denominator);
|
||||
const _Rational(super.numerator, super.denominator);
|
||||
|
||||
factory _Rational.of(Rational r) {
|
||||
return _Rational(r.numerator, r.denominator);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:exifdart/exifdart.dart';
|
||||
import 'package:nc_photos/entity/exif_util.dart';
|
||||
import 'package:np_exiv2/np_exiv2.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
|
@ -8,14 +8,14 @@ void main() {
|
|||
test("United Nations HQ", () {
|
||||
// 40° 44′ 58″ N, 73° 58′ 5″ W
|
||||
final lat = gpsDmsToDouble([
|
||||
Rational(40, 1),
|
||||
Rational(44, 1),
|
||||
Rational(58, 1),
|
||||
const Rational(40, 1),
|
||||
const Rational(44, 1),
|
||||
const Rational(58, 1),
|
||||
]);
|
||||
final lng = gpsDmsToDouble([
|
||||
Rational(73, 1),
|
||||
Rational(58, 1),
|
||||
Rational(5, 1),
|
||||
const Rational(73, 1),
|
||||
const Rational(58, 1),
|
||||
const Rational(5, 1),
|
||||
]);
|
||||
expect(lat, closeTo(40.749444, .00001));
|
||||
expect(lng, closeTo(73.968056, .00001));
|
||||
|
@ -24,14 +24,14 @@ void main() {
|
|||
test("East Cape Lighthouse", () {
|
||||
// 37° 41′ 20.2″ S, 178° 32′ 53.3″ E
|
||||
final lat = gpsDmsToDouble([
|
||||
Rational(37, 1),
|
||||
Rational(41, 1),
|
||||
Rational(202, 10),
|
||||
const Rational(37, 1),
|
||||
const Rational(41, 1),
|
||||
const Rational(202, 10),
|
||||
]);
|
||||
final lng = gpsDmsToDouble([
|
||||
Rational(178, 1),
|
||||
Rational(32, 1),
|
||||
Rational(533, 10),
|
||||
const Rational(178, 1),
|
||||
const Rational(32, 1),
|
||||
const Rational(533, 10),
|
||||
]);
|
||||
expect(lat, closeTo(37.688944, .00001));
|
||||
expect(lng, closeTo(178.548139, .00001));
|
||||
|
@ -46,17 +46,17 @@ void main() {
|
|||
expect(
|
||||
lat.map((e) => e.toString()),
|
||||
[
|
||||
Rational(40, 1).toString(),
|
||||
Rational(44, 1).toString(),
|
||||
Rational(5799, 100).toString(),
|
||||
const Rational(40, 1).toString(),
|
||||
const Rational(44, 1).toString(),
|
||||
const Rational(5799, 100).toString(),
|
||||
],
|
||||
);
|
||||
expect(
|
||||
lng.map((e) => e.toString()),
|
||||
[
|
||||
Rational(73, 1).toString(),
|
||||
Rational(58, 1).toString(),
|
||||
Rational(500, 100).toString(),
|
||||
const Rational(73, 1).toString(),
|
||||
const Rational(58, 1).toString(),
|
||||
const Rational(500, 100).toString(),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
@ -68,17 +68,17 @@ void main() {
|
|||
expect(
|
||||
lat.map((e) => e.toString()),
|
||||
[
|
||||
Rational(37, 1).toString(),
|
||||
Rational(41, 1).toString(),
|
||||
Rational(2019, 100).toString(),
|
||||
const Rational(37, 1).toString(),
|
||||
const Rational(41, 1).toString(),
|
||||
const Rational(2019, 100).toString(),
|
||||
],
|
||||
);
|
||||
expect(
|
||||
lng.map((e) => e.toString()),
|
||||
[
|
||||
Rational(178, 1).toString(),
|
||||
Rational(32, 1).toString(),
|
||||
Rational(5330, 100).toString(),
|
||||
const Rational(178, 1).toString(),
|
||||
const Rational(32, 1).toString(),
|
||||
const Rational(5330, 100).toString(),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
@ -88,21 +88,21 @@ void main() {
|
|||
test("<1000", () {
|
||||
expect(
|
||||
doubleToRational(123.456789123).toString(),
|
||||
Rational(12345678, 100000).toString(),
|
||||
const Rational(12345678, 100000).toString(),
|
||||
);
|
||||
});
|
||||
|
||||
test(">1000 <100000", () {
|
||||
expect(
|
||||
doubleToRational(12345.6789123).toString(),
|
||||
Rational(12345678, 1000).toString(),
|
||||
const Rational(12345678, 1000).toString(),
|
||||
);
|
||||
});
|
||||
|
||||
test(">100000", () {
|
||||
expect(
|
||||
doubleToRational(12345678.9123).toString(),
|
||||
Rational(12345678, 1).toString(),
|
||||
const Rational(12345678, 1).toString(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:exifdart/exifdart.dart' hide Metadata;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:nc_photos/entity/exif.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:np_common/or_null.dart';
|
||||
import 'package:np_exiv2/np_exiv2.dart';
|
||||
import 'package:np_string/np_string.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
|
@ -1266,11 +1266,19 @@ void _fromApiGpsPlace1() {
|
|||
expect(
|
||||
actual?.exif,
|
||||
_MetadataGpsMatcher(Exif({
|
||||
"GPSLatitude": [Rational(40, 1), Rational(44, 1), Rational(5799, 100)],
|
||||
"GPSLatitude": [
|
||||
const Rational(40, 1),
|
||||
const Rational(44, 1),
|
||||
const Rational(5799, 100),
|
||||
],
|
||||
"GPSLatitudeRef": "N",
|
||||
"GPSLongitude": [Rational(73, 1), Rational(58, 1), Rational(500, 100)],
|
||||
"GPSLongitude": [
|
||||
const Rational(73, 1),
|
||||
const Rational(58, 1),
|
||||
const Rational(500, 100),
|
||||
],
|
||||
"GPSLongitudeRef": "W",
|
||||
"GPSAltitude": Rational(1234567, 100000),
|
||||
"GPSAltitude": const Rational(1234567, 100000),
|
||||
"GPSAltitudeRef": 0,
|
||||
})),
|
||||
);
|
||||
|
@ -1292,11 +1300,19 @@ void _fromApiGpsPlace2() {
|
|||
expect(
|
||||
actual?.exif,
|
||||
_MetadataGpsMatcher(Exif({
|
||||
"GPSLatitude": [Rational(37, 1), Rational(41, 1), Rational(2019, 100)],
|
||||
"GPSLatitude": [
|
||||
const Rational(37, 1),
|
||||
const Rational(41, 1),
|
||||
const Rational(2019, 100),
|
||||
],
|
||||
"GPSLatitudeRef": "S",
|
||||
"GPSLongitude": [Rational(178, 1), Rational(32, 1), Rational(5330, 100)],
|
||||
"GPSLongitude": [
|
||||
const Rational(178, 1),
|
||||
const Rational(32, 1),
|
||||
const Rational(5330, 100),
|
||||
],
|
||||
"GPSLongitudeRef": "E",
|
||||
"GPSAltitude": Rational(1234567, 100000),
|
||||
"GPSAltitude": const Rational(1234567, 100000),
|
||||
"GPSAltitudeRef": 1,
|
||||
})),
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue