exif_util.dart 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import "dart:io";
  2. import "package:computer/computer.dart";
  3. import 'package:exif/exif.dart';
  4. import 'package:intl/intl.dart';
  5. import 'package:logging/logging.dart';
  6. import 'package:photos/models/file/file.dart';
  7. import "package:photos/models/location/location.dart";
  8. import "package:photos/services/location_service.dart";
  9. import 'package:photos/utils/file_util.dart';
  10. const kDateTimeOriginal = "EXIF DateTimeOriginal";
  11. const kImageDateTime = "Image DateTime";
  12. const kExifOffSetKeys = [
  13. "EXIF OffsetTime",
  14. "EXIF OffsetTimeOriginal",
  15. "EXIF OffsetTimeDigitized",
  16. ];
  17. const kExifDateTimePattern = "yyyy:MM:dd HH:mm:ss";
  18. const kEmptyExifDateTime = "0000:00:00 00:00:00";
  19. final _logger = Logger("ExifUtil");
  20. Future<Map<String, IfdTag>> getExif(EnteFile file) async {
  21. try {
  22. final File? originFile = await getFile(file, isOrigin: true);
  23. if (originFile == null) {
  24. throw Exception("Failed to fetch origin file");
  25. }
  26. final exif = await readExifAsync(originFile);
  27. if (!file.isRemoteFile && Platform.isIOS) {
  28. await originFile.delete();
  29. }
  30. return exif;
  31. } catch (e) {
  32. _logger.severe("failed to getExif", e);
  33. rethrow;
  34. }
  35. }
  36. Future<Map<String, IfdTag>?> getExifFromSourceFile(File originFile) async {
  37. try {
  38. final exif = await readExifAsync(originFile);
  39. return exif;
  40. } catch (e, s) {
  41. _logger.severe("failed to get exif from origin file", e, s);
  42. return null;
  43. }
  44. }
  45. Future<DateTime?> getCreationTimeFromEXIF(
  46. File? file,
  47. Map<String, IfdTag>? exifData,
  48. ) async {
  49. try {
  50. assert(file != null || exifData != null);
  51. final exif = exifData ?? await readExifAsync(file!);
  52. final exifTime = exif.containsKey(kDateTimeOriginal)
  53. ? exif[kDateTimeOriginal]!.printable
  54. : exif.containsKey(kImageDateTime)
  55. ? exif[kImageDateTime]!.printable
  56. : null;
  57. if (exifTime != null && exifTime != kEmptyExifDateTime) {
  58. String? exifOffsetTime;
  59. for (final key in kExifOffSetKeys) {
  60. if (exif.containsKey(key)) {
  61. exifOffsetTime = exif[key]!.printable;
  62. break;
  63. }
  64. }
  65. return getDateTimeInDeviceTimezone(exifTime, exifOffsetTime);
  66. }
  67. } catch (e) {
  68. _logger.severe("failed to getCreationTimeFromEXIF", e);
  69. }
  70. return null;
  71. }
  72. DateTime getDateTimeInDeviceTimezone(String exifTime, String? offsetString) {
  73. final DateTime result = DateFormat(kExifDateTimePattern).parse(exifTime);
  74. if (offsetString == null) {
  75. return result;
  76. }
  77. try {
  78. final List<String> splitHHMM = offsetString.split(":");
  79. // Parse the offset from the photo's time zone
  80. final int offsetHours = int.parse(splitHHMM[0]);
  81. final int offsetMinutes =
  82. int.parse(splitHHMM[1]) * (offsetHours.isNegative ? -1 : 1);
  83. // Adjust the date for the offset to get the photo's correct UTC time
  84. final photoUtcDate =
  85. result.add(Duration(hours: -offsetHours, minutes: -offsetMinutes));
  86. // Getting the current device's time zone offset from UTC
  87. final now = DateTime.now();
  88. final localOffset = now.timeZoneOffset;
  89. // Adjusting the photo's UTC time to the device's local time
  90. final deviceLocalTime = photoUtcDate.add(localOffset);
  91. return deviceLocalTime;
  92. } catch (e, s) {
  93. _logger.severe("tz offset adjust failed $offsetString", e, s);
  94. }
  95. return result;
  96. }
  97. Location? locationFromExif(Map<String, IfdTag> exif) {
  98. try {
  99. return gpsDataFromExif(exif).toLocationObj();
  100. } catch (e, s) {
  101. _logger.severe("failed to get location from exif", e, s);
  102. return null;
  103. }
  104. }
  105. Future<Map<String, IfdTag>> _readExifArgs(Map<String, dynamic> args) {
  106. return readExifFromFile(args["file"]);
  107. }
  108. Future<Map<String, IfdTag>> readExifAsync(File file) async {
  109. return await Computer.shared().compute(
  110. _readExifArgs,
  111. param: {"file": file},
  112. taskName: "readExifAsync",
  113. );
  114. }
  115. GPSData gpsDataFromExif(Map<String, IfdTag> exif) {
  116. final Map<String, dynamic> exifLocationData = {
  117. "lat": null,
  118. "long": null,
  119. "latRef": null,
  120. "longRef": null,
  121. };
  122. if (exif["GPS GPSLatitude"] != null) {
  123. exifLocationData["lat"] = exif["GPS GPSLatitude"]!
  124. .values
  125. .toList()
  126. .map((e) => ((e as Ratio).numerator / e.denominator))
  127. .toList();
  128. }
  129. if (exif["GPS GPSLongitude"] != null) {
  130. exifLocationData["long"] = exif["GPS GPSLongitude"]!
  131. .values
  132. .toList()
  133. .map((e) => ((e as Ratio).numerator / e.denominator))
  134. .toList();
  135. }
  136. if (exif["GPS GPSLatitudeRef"] != null) {
  137. exifLocationData["latRef"] = exif["GPS GPSLatitudeRef"].toString();
  138. }
  139. if (exif["GPS GPSLongitudeRef"] != null) {
  140. exifLocationData["longRef"] = exif["GPS GPSLongitudeRef"].toString();
  141. }
  142. return GPSData(
  143. exifLocationData["latRef"],
  144. exifLocationData["lat"],
  145. exifLocationData["longRef"],
  146. exifLocationData["long"],
  147. );
  148. }