import "dart:io"; import "package:computer/computer.dart"; import 'package:exif/exif.dart'; import 'package:intl/intl.dart'; import 'package:logging/logging.dart'; import 'package:photos/models/file/file.dart'; import "package:photos/models/location/location.dart"; import "package:photos/services/location_service.dart"; 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"); Future> getExif(EnteFile file) async { try { final File? originFile = await getFile(file, isOrigin: true); if (originFile == null) { throw Exception("Failed to fetch origin file"); } final exif = await readExifAsync(originFile); if (!file.isRemoteFile && Platform.isIOS) { await originFile.delete(); } return exif; } catch (e) { _logger.severe("failed to getExif", e); rethrow; } } Future?> getExifFromSourceFile(File originFile) async { try { final exif = await readExifAsync(originFile); return exif; } catch (e, s) { _logger.severe("failed to get exif from origin file", e, s); return null; } } Future getCreationTimeFromEXIF( File? file, Map? exifData, ) async { try { assert(file != null || exifData != null); final exif = exifData ?? await readExifAsync(file!); 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 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; } Location? locationFromExif(Map exif) { try { return gpsDataFromExif(exif).toLocationObj(); } catch (e, s) { _logger.severe("failed to get location from exif", e, s); return null; } } Future> _readExifArgs(Map args) { return readExifFromFile(args["file"]); } Future> readExifAsync(File file) async { return await Computer.shared().compute( _readExifArgs, param: {"file": file}, taskName: "readExifAsync", ); } GPSData gpsDataFromExif(Map exif) { final Map 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"], ); }