Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions android/src/main/java/com/lodev09/exify/ExifyModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions android/src/main/java/com/lodev09/exify/ExifyUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
63 changes: 46 additions & 17 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;

Expand All @@ -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';
Expand Down Expand Up @@ -139,15 +168,19 @@ export default function App() {
<CameraView ref={cameraRef} style={styles.camera} facing="back" />
<PromptSheet ref={promptRef} />
<View style={styles.controls}>
<Pressable onPress={openLibrary} style={styles.previewButton}>
<Pressable
onPress={openLibrary}
onLongPress={testRoundTrip}
style={styles.sideButton}
>
{preview && (
<Image source={{ uri: preview }} style={styles.preview} />
)}
</Pressable>
<Pressable onPress={takePhoto} style={styles.captureButton}>
<View style={styles.captureInner} />
</Pressable>
<Pressable onPress={openUrl} style={styles.urlButton}>
<Pressable onPress={openUrl} style={styles.sideButton}>
<Text style={styles.urlLabel}>URL</Text>
</Pressable>
</View>
Expand Down Expand Up @@ -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: {
Expand All @@ -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,
Expand Down
33 changes: 20 additions & 13 deletions ios/Exify.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -122,25 +121,33 @@ 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];

if ([key isEqualToString:@"GPSLatitude"]) {
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]) {
Expand Down