diff --git a/android/src/main/java/com/lodev09/exify/ExifyModule.kt b/android/src/main/java/com/lodev09/exify/ExifyModule.kt index f6fbe8d..6d8fb86 100644 --- a/android/src/main/java/com/lodev09/exify/ExifyModule.kt +++ b/android/src/main/java/com/lodev09/exify/ExifyModule.kt @@ -42,7 +42,7 @@ class ExifyModule( // On Android Q+, request ACCESS_MEDIA_LOCATION for unredacted GPS data if (scheme == "content" && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_MEDIA_LOCATION) != - PackageManager.PERMISSION_GRANTED + PackageManager.PERMISSION_GRANTED ) { val activity = context.currentActivity as? PermissionAwareActivity if (activity != null) { @@ -156,14 +156,46 @@ class ExifyModule( tags.hasKey(ExifInterface.TAG_GPS_LATITUDE) && tags.hasKey(ExifInterface.TAG_GPS_LONGITUDE) ) { - exif.setLatLong( - tags.getDouble(ExifInterface.TAG_GPS_LATITUDE), - tags.getDouble(ExifInterface.TAG_GPS_LONGITUDE), - ) + val hasExplicitRef = + tags.hasKey(ExifInterface.TAG_GPS_LATITUDE_REF) || + tags.hasKey(ExifInterface.TAG_GPS_LONGITUDE_REF) + + if (hasExplicitRef) { + val lat = tags.getDouble(ExifInterface.TAG_GPS_LATITUDE) + val lng = tags.getDouble(ExifInterface.TAG_GPS_LONGITUDE) + val latRef = + if (tags.hasKey(ExifInterface.TAG_GPS_LATITUDE_REF)) { + tags.getString(ExifInterface.TAG_GPS_LATITUDE_REF) + } else { + if (lat >= 0) "N" else "S" + } + val lngRef = + if (tags.hasKey(ExifInterface.TAG_GPS_LONGITUDE_REF)) { + tags.getString(ExifInterface.TAG_GPS_LONGITUDE_REF) + } else { + if (lng >= 0) "E" else "W" + } + + exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE, ExifyUtils.decimalToDms(Math.abs(lat))) + exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, latRef) + exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, ExifyUtils.decimalToDms(Math.abs(lng))) + exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, lngRef) + } else { + exif.setLatLong( + tags.getDouble(ExifInterface.TAG_GPS_LATITUDE), + tags.getDouble(ExifInterface.TAG_GPS_LONGITUDE), + ) + } } if (tags.hasKey(ExifInterface.TAG_GPS_ALTITUDE)) { - exif.setAltitude(tags.getDouble(ExifInterface.TAG_GPS_ALTITUDE)) + val alt = tags.getDouble(ExifInterface.TAG_GPS_ALTITUDE) + if (tags.hasKey(ExifInterface.TAG_GPS_ALTITUDE_REF)) { + exif.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, ExifyUtils.decimalToRational(Math.abs(alt))) + exif.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, tags.getInt(ExifInterface.TAG_GPS_ALTITUDE_REF).toString()) + } else { + exif.setAltitude(alt) + } } params.putString("uri", uri) diff --git a/android/src/main/java/com/lodev09/exify/ExifyUtils.kt b/android/src/main/java/com/lodev09/exify/ExifyUtils.kt index 3e331ee..6421bd1 100644 --- a/android/src/main/java/com/lodev09/exify/ExifyUtils.kt +++ b/android/src/main/java/com/lodev09/exify/ExifyUtils.kt @@ -46,4 +46,19 @@ object ExifyUtils { return tags } + + @JvmStatic + fun decimalToDms(decimal: Double): String { + val degrees = decimal.toInt() + val minutesDecimal = (decimal - degrees) * 60 + val minutes = minutesDecimal.toInt() + val seconds = ((minutesDecimal - minutes) * 60 * 10000).toLong() + return "$degrees/1,$minutes/1,$seconds/10000" + } + + @JvmStatic + fun decimalToRational(value: Double): String { + val numerator = (value * 10000).toLong() + return "$numerator/10000" + } } diff --git a/example/src/App.tsx b/example/src/App.tsx index 46d1d6c..f8a54d6 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -40,8 +40,10 @@ export default function App() { const writeExif = async (uri: string) => { const [lng, lat] = mockPosition(); const tags: ExifTags = { - GPSLatitude: lat, - GPSLongitude: lng, + GPSLatitude: Math.abs(lat), + GPSLatitudeRef: lat >= 0 ? 'N' : 'S', + GPSLongitude: Math.abs(lng), + GPSLongitudeRef: lng >= 0 ? 'E' : 'W', GPSTimeStamp: '10:10:10', GPSDateStamp: '2024:10:10', GPSDOP: 5.0, @@ -61,6 +63,18 @@ export default function App() { return result; }; + const readWriteRoundTrip = async (uri: string) => { + const tags = await Exify.read(uri); + if (!tags) return; + + console.log('roundTrip read:', json(tags)); + const result = await Exify.write(uri, tags); + console.log('roundTrip write:', json(result)); + + const verify = await Exify.read(uri); + console.log('roundTrip verify:', json(verify)); + }; + const takePhoto = async () => { if (!cameraRef.current) return; @@ -79,27 +93,42 @@ export default function App() { await readExif(asset.uri); }; - const openLibrary = async () => { + const pickImage = async () => { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], quality: 1, }); - if (result.canceled) return; + if (result.canceled) return null; const asset = result.assets[0]; - if (!asset) return; + if (!asset) return null; const uri = Platform.OS === 'ios' && asset.assetId ? `ph://${asset.assetId}` : asset.uri; - console.log('openLibrary:', uri); setPreview(asset.uri); + return uri; + }; + + const openLibrary = async () => { + const uri = await pickImage(); + if (!uri) return; + + console.log('openLibrary:', uri); await readExif(uri); }; + const testRoundTrip = async () => { + const uri = await pickImage(); + if (!uri) return; + + console.log('testRoundTrip:', uri); + await readWriteRoundTrip(uri); + }; + const openUrl = async () => { const defaultUrl = 'https://raw.githubusercontent.com/ianare/exif-samples/master/jpg/gps/DSCN0010.jpg'; @@ -139,7 +168,11 @@ export default function App() { - + {preview && ( )} @@ -147,7 +180,7 @@ export default function App() { - + URL @@ -180,11 +213,15 @@ const styles = StyleSheet.create({ backgroundColor: '#000', width: '100%', }, - previewButton: { + sideButton: { width: 50, height: 50, borderRadius: 8, backgroundColor: '#333', + borderWidth: 1, + borderColor: '#555', + alignItems: 'center', + justifyContent: 'center', overflow: 'hidden', }, preview: { @@ -206,14 +243,6 @@ const styles = StyleSheet.create({ borderRadius: 29, backgroundColor: '#fff', }, - urlButton: { - width: 50, - height: 50, - borderRadius: 8, - backgroundColor: '#333', - alignItems: 'center', - justifyContent: 'center', - }, urlLabel: { color: '#fff', fontSize: 13, diff --git a/ios/Exify.mm b/ios/Exify.mm index ee850ad..dbfc4e7 100644 --- a/ios/Exify.mm +++ b/ios/Exify.mm @@ -3,14 +3,13 @@ static NSSet *tiffKeys; __attribute__((constructor)) static void initTiffKeys(void) { - tiffKeys = [NSSet setWithObjects:@"Make", @"Model", @"Software", @"DateTime", - @"Artist", @"Copyright", @"ImageDescription", - @"Orientation", @"XResolution", - @"YResolution", @"ResolutionUnit", - @"Compression", @"PhotometricInterpretation", - @"TransferFunction", @"WhitePoint", - @"PrimaryChromaticities", @"HostComputer", - nil]; + tiffKeys = [NSSet + setWithObjects:@"Make", @"Model", @"Software", @"DateTime", @"Artist", + @"Copyright", @"ImageDescription", @"Orientation", + @"XResolution", @"YResolution", @"ResolutionUnit", + @"Compression", @"PhotometricInterpretation", + @"TransferFunction", @"WhitePoint", + @"PrimaryChromaticities", @"HostComputer", nil]; } #pragma mark - Helpers @@ -122,6 +121,11 @@ static void addTagEntries(CFStringRef dictionary, NSDictionary *metadata, kCGImagePropertyGPSDictionary] ?: @{}]; + // Pre-read explicit GPS ref values so coordinate handlers can use them + NSString *latRef = tags[@"GPSLatitudeRef"]; + NSString *lngRef = tags[@"GPSLongitudeRef"]; + NSNumber *altRef = tags[@"GPSAltitudeRef"]; + for (NSString *key in tags) { id value = tags[key]; @@ -129,18 +133,21 @@ static void addTagEntries(CFStringRef dictionary, NSDictionary *metadata, double lat = [value doubleValue]; gpsDict[(__bridge NSString *)kCGImagePropertyGPSLatitude] = @(fabs(lat)); gpsDict[(__bridge NSString *)kCGImagePropertyGPSLatitudeRef] = - lat >= 0 ? @"N" : @"S"; + latRef ?: (lat >= 0 ? @"N" : @"S"); } else if ([key isEqualToString:@"GPSLongitude"]) { double lng = [value doubleValue]; - gpsDict[(__bridge NSString *)kCGImagePropertyGPSLongitude] = - @(fabs(lng)); + gpsDict[(__bridge NSString *)kCGImagePropertyGPSLongitude] = @(fabs(lng)); gpsDict[(__bridge NSString *)kCGImagePropertyGPSLongitudeRef] = - lng >= 0 ? @"E" : @"W"; + lngRef ?: (lng >= 0 ? @"E" : @"W"); } else if ([key isEqualToString:@"GPSAltitude"]) { double alt = [value doubleValue]; gpsDict[(__bridge NSString *)kCGImagePropertyGPSAltitude] = @(fabs(alt)); gpsDict[(__bridge NSString *)kCGImagePropertyGPSAltitudeRef] = - @(alt >= 0 ? 0 : 1); + altRef ?: @(alt >= 0 ? 0 : 1); + } else if ([key isEqualToString:@"GPSLatitudeRef"] || + [key isEqualToString:@"GPSLongitudeRef"] || + [key isEqualToString:@"GPSAltitudeRef"]) { + // Already handled above } else if ([key hasPrefix:@"GPS"]) { gpsDict[[key substringFromIndex:3]] = value; } else if ([tiffKeys containsObject:key]) {