Adapt to server side metadata

This commit is contained in:
Ming Ming 2024-11-10 18:50:38 +08:00
parent eaa8b2d907
commit bc9bbe9455
8 changed files with 335 additions and 36 deletions

View file

@ -17,6 +17,7 @@ import 'package:nc_photos/entity/tagged_file.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:np_api/np_api.dart' as api;
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/object_util.dart';
import 'package:np_string/np_string.dart';
part 'entity_converter.g.dart';
@ -49,9 +50,18 @@ class ApiFavoriteConverter {
}
class ApiFileConverter {
static File fromApi(api.File file) {
var metadata = file.customProperties?["com.nkming.nc_photos:metadata"]
?.run((obj) => Metadata.fromJson(
static Metadata? _metadataFromApi(api.File file) {
if (file.metadataPhotosSize != null) {
return Metadata.fromApi(
etag: file.etag,
ifd0: file.metadataPhotosIfd0,
exif: file.metadataPhotosExif,
gps: file.metadataPhotosGps,
size: file.metadataPhotosSize!,
);
} else {
return file.customProperties?["com.nkming.nc_photos:metadata"]
?.let((obj) => Metadata.fromJson(
jsonDecode(obj),
upgraderV1: MetadataUpgraderV1(
fileContentType: file.contentType,
@ -66,15 +76,10 @@ class ApiFileConverter {
logFilePath: file.href,
),
));
if (file.metadataPhotosIfd0 != null) {
final ifd0_metadata = Metadata.fromPhotosIfd0(file.metadataPhotosIfd0!);
if (metadata == null) {
metadata = ifd0_metadata;
} else {
metadata = metadata.copyWith(exif: ifd0_metadata.exif);
}
}
static File fromApi(api.File file) {
return File(
path: _hrefToPath(file.href),
contentLength: file.contentLength,
@ -90,7 +95,7 @@ class ApiFileConverter {
trashbinFilename: file.trashbinFilename,
trashbinOriginalLocation: file.trashbinOriginalLocation,
trashbinDeletionTime: file.trashbinDeletionTime,
metadata: metadata,
metadata: _metadataFromApi(file),
isArchived: file.customProperties?["com.nkming.nc_photos:is-archived"]
?.run((obj) => obj == "true"),
overrideDateTime: file

View file

@ -1,4 +1,5 @@
import 'package:exifdart/exifdart.dart';
import 'package:flutter/foundation.dart';
import 'package:nc_photos/entity/exif.dart';
extension ExifExtension on Exif {
@ -12,7 +13,7 @@ extension ExifExtension on Exif {
// invalid value
return null;
} else {
return _gpsDmsToDouble(gpsLatitude!) * (gpsLatitudeRef == "S" ? -1 : 1);
return gpsDmsToDouble(gpsLatitude!) * (gpsLatitudeRef == "S" ? -1 : 1);
}
}
@ -26,12 +27,23 @@ extension ExifExtension on Exif {
// invalid value
return null;
} else {
return _gpsDmsToDouble(gpsLongitude!) * (gpsLongitudeRef == "W" ? -1 : 1);
return gpsDmsToDouble(gpsLongitude!) * (gpsLongitudeRef == "W" ? -1 : 1);
}
}
}
double _gpsDmsToDouble(List<Rational> dms) {
List<Rational> gpsDoubleToDms(double src) {
var tmp = src.abs();
final d = tmp.floor();
tmp -= d;
final ss = (tmp * 3600 * 100).floor();
final s = ss % (60 * 100);
final m = (ss / (60 * 100)).floor();
return [Rational(d, 1), Rational(m, 1), Rational(s, 100)];
}
@visibleForTesting
double gpsDmsToDouble(List<Rational> dms) {
double product = dms[0].toDouble();
if (dms.length > 1) {
product += dms[1].toDouble() / 60;
@ -41,3 +53,14 @@ double _gpsDmsToDouble(List<Rational> dms) {
}
return product;
}
Rational doubleToRational(double src) {
final s = src.abs();
if (s < 1000) {
return Rational((s * 100000).truncate(), 100000);
} else if (s < 100000) {
return Rational((s * 1000).truncate(), 1000);
} else {
return Rational(s.truncate(), 1);
}
}

View file

@ -5,10 +5,12 @@ import 'package:equatable/equatable.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/exif.dart';
import 'package:nc_photos/entity/exif_util.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/json_util.dart' as json_util;
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/object_util.dart';
import 'package:np_common/or_null.dart';
import 'package:np_common/type.dart';
import 'package:np_string/np_string.dart';
@ -185,13 +187,40 @@ class Metadata with EquatableMixin {
);
}
static Metadata fromPhotosIfd0(Map<String, String> metadataPhotosIfd0) {
static Metadata? fromApi({
required String? etag,
Map<String, String>? ifd0,
Map<String, String>? exif,
Map<String, String>? gps,
required Map<String, String> size,
}) {
final lat = gps?["latitude"]?.let(double.tryParse);
final lng = gps?["longitude"]?.let(double.tryParse);
final alt = gps?["altitude"]?.let(double.tryParse);
return Metadata(
lastUpdated: null,
fileEtag: null,
imageWidth: null,
imageHeight: null,
exif: new Exif(metadataPhotosIfd0),
lastUpdated: clock.now().toUtc(),
fileEtag: etag,
imageWidth: int.parse(size["width"]!),
imageHeight: int.parse(size["height"]!),
exif: ifd0 != null || exif != null || gps != null
? Exif({
if (ifd0 != null) ...ifd0,
if (exif != null) ...exif,
if (lat != null && lng != null) ...{
"GPSLatitude": gpsDoubleToDms(lat),
"GPSLatitudeRef": lat.isNegative ? "S" : "N",
"GPSLongitude": gpsDoubleToDms(lng),
"GPSLongitudeRef": lng.isNegative ? "W" : "E",
if (alt != null) ...{
"GPSAltitude": doubleToRational(alt),
"GPSAltitudeRef": alt.isNegative ? 1 : 0,
}
}
}..removeWhere((key, value) =>
key == "MakerNote" ||
key == "UserComment" ||
key == "ImageDescription"))
: null,
);
}

View file

@ -56,6 +56,9 @@ class FileWebdavDataSource implements FileDataSource {
trashbinOriginalLocation: 1,
trashbinDeletionTime: 1,
metadataPhotosIfd0: 1,
metadataPhotosExif: 1,
metadataPhotosGps: 1,
metadataPhotosSize: 1,
customNamespaces: {
"com.nkming.nc_photos": "app",
},
@ -277,6 +280,9 @@ class FileWebdavDataSource implements FileDataSource {
trashbinOriginalLocation,
trashbinDeletionTime,
metadataPhotosIfd0,
metadataPhotosExif,
metadataPhotosGps,
metadataPhotosSize,
Map<String, String>? customNamespaces,
List<String>? customProperties,
}) async {
@ -305,6 +311,9 @@ class FileWebdavDataSource implements FileDataSource {
trashbinOriginalLocation: trashbinOriginalLocation,
trashbinDeletionTime: trashbinDeletionTime,
metadataPhotosIfd0: metadataPhotosIfd0,
metadataPhotosExif: metadataPhotosExif,
metadataPhotosGps: metadataPhotosGps,
metadataPhotosSize: metadataPhotosSize,
customNamespaces: customNamespaces,
customProperties: customProperties,
);

View file

@ -5,7 +5,7 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/connectivity_util.dart' as connectivity_util;
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/exif_extension.dart';
import 'package:nc_photos/entity/exif_util.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/exception.dart';

View file

@ -14,7 +14,7 @@ import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/collection.dart';
import 'package:nc_photos/entity/collection/adapter.dart';
import 'package:nc_photos/entity/collection_item.dart';
import 'package:nc_photos/entity/exif_extension.dart';
import 'package:nc_photos/entity/exif_util.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;

View file

@ -0,0 +1,110 @@
import 'package:exifdart/exifdart.dart';
import 'package:nc_photos/entity/exif_util.dart';
import 'package:test/test.dart';
void main() {
group("exif_util", () {
group("gpsDmsToDouble", () {
test("United Nations HQ", () {
// 40° 44 58 N, 73° 58 5 W
final lat = gpsDmsToDouble([
Rational(40, 1),
Rational(44, 1),
Rational(58, 1),
]);
final lng = gpsDmsToDouble([
Rational(73, 1),
Rational(58, 1),
Rational(5, 1),
]);
expect(lat, closeTo(40.749444, .00001));
expect(lng, closeTo(73.968056, .00001));
});
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),
]);
final lng = gpsDmsToDouble([
Rational(178, 1),
Rational(32, 1),
Rational(533, 10),
]);
expect(lat, closeTo(37.688944, .00001));
expect(lng, closeTo(178.548139, .00001));
});
});
group("gpsDoubleToDms", () {
test("United Nations HQ", () {
// 40.749444, -73.968056
final lat = gpsDoubleToDms(40.749444);
final lng = gpsDoubleToDms(-73.968056);
expect(
lat.map((e) => e.toString()),
[
Rational(40, 1).toString(),
Rational(44, 1).toString(),
Rational(5799, 100).toString(),
],
);
expect(
lng.map((e) => e.toString()),
[
Rational(73, 1).toString(),
Rational(58, 1).toString(),
Rational(500, 100).toString(),
],
);
});
test("East Cape Lighthouse", () {
// -37.688944, 178.548139
final lat = gpsDoubleToDms(-37.688944);
final lng = gpsDoubleToDms(178.548139);
expect(
lat.map((e) => e.toString()),
[
Rational(37, 1).toString(),
Rational(41, 1).toString(),
Rational(2019, 100).toString(),
],
);
expect(
lng.map((e) => e.toString()),
[
Rational(178, 1).toString(),
Rational(32, 1).toString(),
Rational(5330, 100).toString(),
],
);
});
});
group("doubleToRational", () {
test("<1000", () {
expect(
doubleToRational(123.456789123).toString(),
Rational(12345678, 100000).toString(),
);
});
test(">1000 <100000", () {
expect(
doubleToRational(12345.6789123).toString(),
Rational(12345678, 1000).toString(),
);
});
test(">100000", () {
expect(
doubleToRational(12345678.9123).toString(),
Rational(12345678, 1).toString(),
);
});
});
});
}

View file

@ -1,4 +1,6 @@
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';
@ -245,6 +247,14 @@ void main() {
});
});
});
group("fromApi", () {
test("size", _fromApiSize);
group("gps", () {
test("place1", _fromApiGpsPlace1);
test("place2", _fromApiGpsPlace2);
});
});
});
group("MetadataUpgraderV1", () {
@ -1217,3 +1227,116 @@ void main() {
});
});
}
void _fromApiSize() {
withClock(
Clock(() => DateTime(2020, 1, 2, 3, 4, 5)),
() {
final actual = Metadata.fromApi(
etag: null,
size: {
"width": "1234",
"height": "5678",
},
);
expect(
actual,
Metadata(
imageWidth: 1234,
imageHeight: 5678,
),
);
},
);
}
void _fromApiGpsPlace1() {
final actual = Metadata.fromApi(
etag: null,
size: {
"width": "1234",
"height": "5678",
},
gps: {
"latitude": "40.749444",
"longitude": "-73.968056",
"altitude": "12.345678",
},
);
expect(
actual?.exif,
_MetadataGpsMatcher(Exif({
"GPSLatitude": [Rational(40, 1), Rational(44, 1), Rational(5799, 100)],
"GPSLatitudeRef": "N",
"GPSLongitude": [Rational(73, 1), Rational(58, 1), Rational(500, 100)],
"GPSLongitudeRef": "W",
"GPSAltitude": Rational(1234567, 100000),
"GPSAltitudeRef": 0,
})),
);
}
void _fromApiGpsPlace2() {
final actual = Metadata.fromApi(
etag: null,
size: {
"width": "1234",
"height": "5678",
},
gps: {
"latitude": "-37.688944",
"longitude": "178.5481396",
"altitude": "-12.345678",
},
);
expect(
actual?.exif,
_MetadataGpsMatcher(Exif({
"GPSLatitude": [Rational(37, 1), Rational(41, 1), Rational(2019, 100)],
"GPSLatitudeRef": "S",
"GPSLongitude": [Rational(178, 1), Rational(32, 1), Rational(5330, 100)],
"GPSLongitudeRef": "E",
"GPSAltitude": Rational(1234567, 100000),
"GPSAltitudeRef": 1,
})),
);
}
class _MetadataGpsMatcher extends Matcher {
const _MetadataGpsMatcher(this.expected);
@override
bool matches(Object? item, Map matchState) {
final actual = item as Exif;
final gpsLatitude = listEquals(
actual["GPSLatitude"]?.map((e) => e.toString()).toList(),
expected["GPSLatitude"]?.map((e) => e.toString()).toList(),
);
final gpsLatitudeRef =
actual["GPSLatitudeRef"] == expected["GPSLatitudeRef"];
final gpsLongitude = listEquals(
actual["GPSLongitude"]?.map((e) => e.toString()).toList(),
expected["GPSLongitude"]?.map((e) => e.toString()).toList(),
);
final gpsLongitudeRef =
actual["GPSLongitudeRef"] == expected["GPSLongitudeRef"];
final gpsAltitude = actual["GPSAltitude"]?.toString() ==
expected["GPSAltitude"]?.toString();
final gpsAltitudeRef =
actual["GPSAltitudeRef"] == expected["GPSAltitudeRef"];
return gpsLatitude &&
gpsLatitudeRef &&
gpsLongitude &&
gpsLongitudeRef &&
gpsAltitude &&
gpsAltitudeRef;
}
@override
Description describe(Description description) {
return description.add(expected.toString());
}
final Exif expected;
}