ente/mobile/lib/utils/exif_util.dart

160 lines
4.7 KiB
Dart
Raw Normal View History

2023-08-24 16:56:24 +00:00
import "dart:io";
2021-08-24 07:14:33 +00:00
2023-08-09 06:04:20 +00:00
import "package:computer/computer.dart";
2021-08-24 07:14:33 +00:00
import 'package:exif/exif.dart';
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
2023-08-25 04:39:30 +00:00
import 'package:photos/models/file/file.dart';
2023-04-18 06:27:55 +00:00
import "package:photos/models/location/location.dart";
import "package:photos/services/location_service.dart";
2021-08-24 07:14:33 +00:00
import 'package:photos/utils/file_util.dart';
const kDateTimeOriginal = "EXIF DateTimeOriginal";
const kImageDateTime = "Image DateTime";
const kExifOffSetKeys = [
"EXIF OffsetTime",
"EXIF OffsetTimeOriginal",
"EXIF OffsetTimeDigitized",
];
const kExifDateTimePattern = "yyyy:MM:dd HH:mm:ss";
const kEmptyExifDateTime = "0000:00:00 00:00:00";
final _logger = Logger("ExifUtil");
2023-08-24 16:56:24 +00:00
Future<Map<String, IfdTag>> getExif(EnteFile file) async {
2021-09-28 09:24:41 +00:00
try {
2023-08-24 16:56:24 +00:00
final File? originFile = await getFile(file, isOrigin: true);
2022-12-29 13:40:36 +00:00
if (originFile == null) {
throw Exception("Failed to fetch origin file");
}
2023-08-09 06:04:20 +00:00
final exif = await readExifAsync(originFile);
2023-08-24 16:56:24 +00:00
if (!file.isRemoteFile && Platform.isIOS) {
2021-09-28 09:24:41 +00:00
await originFile.delete();
}
return exif;
} catch (e) {
_logger.severe("failed to getExif", e);
2021-09-28 09:24:41 +00:00
rethrow;
2021-08-24 07:14:33 +00:00
}
}
2023-08-24 16:56:24 +00:00
Future<Map<String, IfdTag>?> getExifFromSourceFile(File originFile) async {
try {
2023-08-09 06:04:20 +00:00
final exif = await readExifAsync(originFile);
return exif;
} catch (e, s) {
_logger.severe("failed to get exif from origin file", e, s);
return null;
}
}
Future<DateTime?> getCreationTimeFromEXIF(
2023-08-24 16:56:24 +00:00
File? file,
Map<String, IfdTag>? exifData,
) async {
try {
assert(file != null || exifData != null);
2023-08-09 06:04:20 +00:00
final exif = exifData ?? await readExifAsync(file!);
2022-11-06 08:22:54 +00:00
final exifTime = exif.containsKey(kDateTimeOriginal)
? exif[kDateTimeOriginal]!.printable
: exif.containsKey(kImageDateTime)
? exif[kImageDateTime]!.printable
: null;
if (exifTime != null && exifTime != kEmptyExifDateTime) {
String? exifOffsetTime;
for (final key in kExifOffSetKeys) {
if (exif.containsKey(key)) {
exifOffsetTime = exif[key]!.printable;
break;
}
}
return getDateTimeInDeviceTimezone(exifTime, exifOffsetTime);
}
} catch (e) {
_logger.severe("failed to getCreationTimeFromEXIF", e);
}
return null;
}
DateTime getDateTimeInDeviceTimezone(String exifTime, String? offsetString) {
final DateTime result = DateFormat(kExifDateTimePattern).parse(exifTime);
if (offsetString == null) {
return result;
}
try {
final List<String> splitHHMM = offsetString.split(":");
// Parse the offset from the photo's time zone
final int offsetHours = int.parse(splitHHMM[0]);
final int offsetMinutes =
int.parse(splitHHMM[1]) * (offsetHours.isNegative ? -1 : 1);
// Adjust the date for the offset to get the photo's correct UTC time
final photoUtcDate =
result.add(Duration(hours: -offsetHours, minutes: -offsetMinutes));
// Getting the current device's time zone offset from UTC
final now = DateTime.now();
final localOffset = now.timeZoneOffset;
// Adjusting the photo's UTC time to the device's local time
final deviceLocalTime = photoUtcDate.add(localOffset);
return deviceLocalTime;
} catch (e, s) {
_logger.severe("tz offset adjust failed $offsetString", e, s);
}
return result;
}
2023-04-18 06:27:55 +00:00
Location? locationFromExif(Map<String, IfdTag> exif) {
2023-04-18 07:08:52 +00:00
try {
return gpsDataFromExif(exif).toLocationObj();
2023-04-18 07:08:52 +00:00
} catch (e, s) {
_logger.severe("failed to get location from exif", e, s);
return null;
}
2023-04-18 06:27:55 +00:00
}
2023-08-09 06:04:20 +00:00
Future<Map<String, IfdTag>> _readExifArgs(Map<String, dynamic> args) {
return readExifFromFile(args["file"]);
}
2023-08-24 16:56:24 +00:00
Future<Map<String, IfdTag>> readExifAsync(File file) async {
2023-08-09 06:04:20 +00:00
return await Computer.shared().compute(
_readExifArgs,
param: {"file": file},
taskName: "readExifAsync",
);
}
GPSData gpsDataFromExif(Map<String, IfdTag> exif) {
final Map<String, dynamic> exifLocationData = {
"lat": null,
"long": null,
"latRef": null,
"longRef": null,
};
if (exif["GPS GPSLatitude"] != null) {
exifLocationData["lat"] = exif["GPS GPSLatitude"]!
.values
.toList()
.map((e) => ((e as Ratio).numerator / e.denominator))
.toList();
}
if (exif["GPS GPSLongitude"] != null) {
exifLocationData["long"] = exif["GPS GPSLongitude"]!
.values
.toList()
.map((e) => ((e as Ratio).numerator / e.denominator))
.toList();
}
if (exif["GPS GPSLatitudeRef"] != null) {
exifLocationData["latRef"] = exif["GPS GPSLatitudeRef"].toString();
}
if (exif["GPS GPSLongitudeRef"] != null) {
exifLocationData["longRef"] = exif["GPS GPSLongitudeRef"].toString();
}
return GPSData(
exifLocationData["latRef"],
exifLocationData["lat"],
exifLocationData["longRef"],
exifLocationData["long"],
);
}