Files
iNaturalistReactNative/src/sharedHelpers/parseExif.js
budowski fac15eb1e6 Use latest react-native-exif-reader package version; use writeLocation instead of writeExif (#1268)
* Use latest react-native-exif-reader package version; use writeLocation instead of writeExif
* Use react-native-exif-reader v0.4.0
2024-05-21 13:58:42 -07:00

107 lines
3.3 KiB
JavaScript

// @flow
import { utcToZonedTime } from "date-fns-tz";
import { readExif, writeLocation } from "react-native-exif-reader";
import * as RNLocalize from "react-native-localize";
import { formatISONoTimezone } from "sharedHelpers/dateAndTime";
import { log } from "../../react-native-logs.config";
class UsePhotoExifDateFormatError extends Error {}
// https://wbinnssmith.com/blog/subclassing-error-in-modern-javascript/
Object.defineProperty( UsePhotoExifDateFormatError.prototype, "name", {
value: "UsePhotoExifDateFormatError"
} );
const logger = log.extend( "parseExif" );
// Parses EXIF date time into a date object
export const parseExifDateToLocalTimezone = ( datetime: string ): ?Date => {
if ( !datetime ) return null;
const isoDate = `${datetime}Z`;
// Use local timezone from device
const timeZone = RNLocalize.getTimeZone( );
const zonedDate = utcToZonedTime( isoDate, timeZone );
if ( !zonedDate || zonedDate.toString( ).match( /invalid/i ) ) {
throw new UsePhotoExifDateFormatError( "Date was not formatted correctly" );
}
return zonedDate;
};
// Parses EXIF date time into a date object
export const parseExif = async ( photoUri: ?string ): Promise<Object> => {
try {
return readExif( photoUri );
} catch ( e ) {
console.error( e, "Couldn't parse EXIF" );
return null;
}
};
interface ExifToWrite {
latitude?: number;
longitude?: number;
positional_accuracy?: number;
}
export const writeExifToFile = async ( photoUri: ?string, exif: ExifToWrite ): Promise<Object> => {
logger.debug( "writeExifToFile, photoUri: ", photoUri );
try {
return writeLocation( photoUri, exif );
} catch ( e ) {
console.error( e, "Couldn't write EXIF" );
return null;
}
};
export const formatExifDateAsString = ( datetime: string ): string => {
const zonedDate = parseExifDateToLocalTimezone( datetime );
// this returns a string, in the same format as photos which fall back to the
// photo timestamp instead of exif data
return formatISONoTimezone( zonedDate );
};
// Parse the EXIF of all photos - fill out details (lat/lng/date) from all of these,
// in case the first photo is missing EXIF
export const readExifFromMultiplePhotos = async ( photoUris: Array<string> ): Promise<Object> => {
const unifiedExif = {};
const responses = await Promise.allSettled( photoUris.map( parseExif ) );
const allExifPhotos: Array<{
latitude: number,
longitude: number,
positional_accuracy: number,
date: string
// Flow will complain that value is undefined, but the filter call ensures
// that it isn't
// $FlowIgnore
}> = responses.filter( r => r.value ).map( r => r.value );
allExifPhotos.filter( x => x ).forEach(
currentPhotoExif => {
const {
latitude, longitude, positional_accuracy: positionalAccuracy, date
} = currentPhotoExif;
if ( !unifiedExif.latitude ) {
unifiedExif.latitude = latitude;
}
if ( !unifiedExif.longitude ) {
unifiedExif.longitude = longitude;
}
if ( !unifiedExif.observed_on_string ) {
unifiedExif.observed_on_string = formatExifDateAsString( date ) || null;
}
if ( positionalAccuracy && !unifiedExif.positional_accuracy ) {
unifiedExif.positional_accuracy = positionalAccuracy;
}
}
);
return unifiedExif;
};