X-Git-Url: https://git.street.me.uk/andy/viking.git/blobdiff_plain/29c93cc57d2f7715c58b4e577440bc580a47bd8a..fef725dedc9c1ad332bf3bfad544ed09b6f14ddb:/src/geotag_exif.c diff --git a/src/geotag_exif.c b/src/geotag_exif.c index 2dcb0fae..9d6b3e87 100644 --- a/src/geotag_exif.c +++ b/src/geotag_exif.c @@ -2,7 +2,7 @@ /* * viking -- GPS Data and Topo Analyzer, Explorer, and Manager * - * Copyright (C) 2011, Rob Norris + * Copyright (C) 2011-2014, Rob Norris * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,23 +23,71 @@ /* * This uses EXIF information from images to create waypoints at those positions * - * For the implementation I have chosen to use libexif, which keeps Viking a pure C program - * For an alternative implementation (a la gpscorrelate), one could use libeviv2 but it appears to be C++ only. + * The initial implementation uses libexif, which keeps Viking a pure C program. + * Now libgexiv2 is available (in C as a wrapper around the more powerful libexiv2 [C++]) so this is the preferred build. + * The attentative reader will notice the use of gexiv2 is a lot simpler as well. + * For the time being the libexif code + build is still made available. */ #include #include "geotag_exif.h" +#include "config.h" #include "globals.h" #include "file.h" #include +#ifdef HAVE_UTIME_H #include +#endif #include #include #include #include +#include +#ifdef HAVE_LIBGEXIV2 +#include +#endif +#ifdef HAVE_LIBEXIF #include #include "libjpeg/jpeg-data.h" +#endif +#ifdef HAVE_LIBGEXIV2 +// Compatibility +static void metadata_free ( GExiv2Metadata *gemd ) +{ +#if GEXIV2_CHECK_VERSION (0,10,4) + g_object_unref ( gemd ); +#else + gexiv2_metadata_free ( gemd ); +#endif +} + +/** + * Attempt to get a single comment from the various exif fields + */ +static gchar* geotag_get_exif_comment ( GExiv2Metadata *gemd ) +{ + // + // Try various options to create a comment + // + if ( gexiv2_metadata_has_tag ( gemd, "Exif.Image.ImageDescription" ) ) + return g_strdup ( gexiv2_metadata_get_tag_interpreted_string ( gemd, "Exif.Image.ImageDescription" ) ); + + if ( gexiv2_metadata_has_tag ( gemd, "Exif.Image.XPComment" ) ) + return g_strdup ( gexiv2_metadata_get_tag_interpreted_string ( gemd, "Exif.Image.XPComment" ) ); + + if ( gexiv2_metadata_has_tag ( gemd, "Exif.Image.XPSubject" ) ) + return g_strdup ( gexiv2_metadata_get_tag_interpreted_string ( gemd, "Exif.Image.XPSubject" ) ); + + if ( gexiv2_metadata_has_tag ( gemd, "Exif.Image.DateTimeOriginal" ) ) + return g_strdup ( gexiv2_metadata_get_tag_interpreted_string ( gemd, "Exif.Image.DateTimeOriginal" ) ); + + // Otherwise nothing found + return NULL; +} +#endif + +#ifdef HAVE_LIBEXIF /** * Attempt to get a single comment from the various exif fields */ @@ -76,7 +124,7 @@ static gchar* geotag_get_exif_comment ( ExifData *ed ) exif_entry_get_value ( ee, str, 128 ); return g_strdup ( str ); } - + // Otherwise nothing found return NULL; } @@ -117,6 +165,99 @@ static gdouble Rational2Double ( unsigned char *data, int offset, ExifByteOrder return ans; } +static struct LatLon get_latlon ( ExifData *ed ) +{ + struct LatLon ll = { 0.0, 0.0 }; + const struct LatLon ll0 = { 0.0, 0.0 }; + + gchar str[128]; + ExifEntry *ee; + // + // Lat & Long is necessary to form a waypoint. + // + ee = exif_content_get_entry (ed->ifd[EXIF_IFD_GPS], EXIF_TAG_GPS_LATITUDE); + if ( ! ( ee && ee->components == 3 && ee->format == EXIF_FORMAT_RATIONAL ) ) + return ll0; + + ll.lat = Rational2Double ( ee->data, + exif_format_get_size(ee->format), + exif_data_get_byte_order(ed) ); + + ee = exif_content_get_entry (ed->ifd[EXIF_IFD_GPS], EXIF_TAG_GPS_LATITUDE_REF); + if ( ee ) { + exif_entry_get_value ( ee, str, 128 ); + if ( str[0] == 'S' ) + ll.lat = -ll.lat; + } + + ee = exif_content_get_entry (ed->ifd[EXIF_IFD_GPS], EXIF_TAG_GPS_LONGITUDE); + if ( ! ( ee && ee->components == 3 && ee->format == EXIF_FORMAT_RATIONAL ) ) + return ll0; + + ll.lon = Rational2Double ( ee->data, + exif_format_get_size(ee->format), + exif_data_get_byte_order(ed) ); + + ee = exif_content_get_entry (ed->ifd[EXIF_IFD_GPS], EXIF_TAG_GPS_LONGITUDE_REF); + if ( ee ) { + exif_entry_get_value ( ee, str, 128 ); + if ( str[0] == 'W' ) + ll.lon = -ll.lon; + } + + return ll; +} +#endif + +/** + * a_geotag_get_position: + * + * @filename: The (JPG) file with EXIF information in it + * + * Returns: The position in LatLon format. + * It will be 0,0 if some kind of failure occurs. + */ +struct LatLon a_geotag_get_position ( const gchar *filename ) +{ + struct LatLon ll = { 0.0, 0.0 }; + +#ifdef HAVE_LIBGEXIV2 + GExiv2Metadata *gemd = gexiv2_metadata_new (); + if ( gexiv2_metadata_open_path ( gemd, filename, NULL ) ) { + gdouble lat; + gdouble lon; + gdouble alt; + if ( gexiv2_metadata_get_gps_info ( gemd, &lon, &lat, &alt ) ) { + ll.lat = lat; + ll.lon = lon; + } + } + metadata_free ( gemd ); +#else +#ifdef HAVE_LIBEXIF + // open image with libexif + ExifData *ed = exif_data_new_from_file ( filename ); + + // Detect EXIF load failure + if ( !ed ) + return ll; + + ExifEntry *ee = exif_content_get_entry (ed->ifd[EXIF_IFD_GPS], EXIF_TAG_GPS_VERSION_ID); + // Confirm this has a GPS Id - normally "2.0.0.0" or "2.2.0.0" + if ( ! ( ee && ee->components == 4 ) ) + goto MyReturn0; + + ll = get_latlon ( ed ); + +MyReturn0: + // Finished with EXIF + exif_data_free ( ed ); +#endif +#endif + + return ll; +} + /** * a_geotag_create_waypoint_from_file: * @filename: The image file to process @@ -132,6 +273,38 @@ VikWaypoint* a_geotag_create_waypoint_from_file ( const gchar *filename, VikCoor *name = NULL; VikWaypoint *wp = NULL; +#ifdef HAVE_LIBGEXIV2 + GExiv2Metadata *gemd = gexiv2_metadata_new (); + if ( gexiv2_metadata_open_path ( gemd, filename, NULL ) ) { + gdouble lat; + gdouble lon; + gdouble alt; + if ( gexiv2_metadata_get_gps_info ( gemd, &lon, &lat, &alt ) ) { + struct LatLon ll; + ll.lat = lat; + ll.lon = lon; + + // + // Now create Waypoint with acquired information + // + wp = vik_waypoint_new(); + wp->visible = TRUE; + // Set info from exif values + // Location + vik_coord_load_from_latlon ( &(wp->coord), vcmode, &ll ); + // Altitude + wp->altitude = alt; + + if ( gexiv2_metadata_has_tag ( gemd, "Exif.Image.XPTitle" ) ) + *name = g_strdup ( gexiv2_metadata_get_tag_interpreted_string ( gemd, "Exif.Image.XPTitle" ) ); + wp->comment = geotag_get_exif_comment ( gemd ); + + vik_waypoint_set_image ( wp, filename ); + } + } + metadata_free ( gemd ); +#else +#ifdef HAVE_LIBEXIF // TODO use log? //ExifLog *log = NULL; @@ -156,51 +329,12 @@ VikWaypoint* a_geotag_create_waypoint_from_file ( const gchar *filename, VikCoor //if ( ! ( ee->data[0] == 2 && ee->data[2] == 0 && ee->data[3] == 0 ) ) // goto MyReturn; + ll = get_latlon ( ed ); - ee = exif_content_get_entry (ed->ifd[EXIF_IFD_GPS], EXIF_TAG_GPS_MAP_DATUM); - if ( ! ( ee && ee->components > 0 && ee->format == EXIF_FORMAT_ASCII ) ) - goto MyReturn; - - // If map datum specified - only deal in WGS-84 - the defacto standard - if ( ee && ee->components > 0 ) { - exif_entry_get_value ( ee, str, 128 ); - if ( strncmp (str, "WGS-84", 6) ) - goto MyReturn; - } - - // - // Lat & Long is necessary to form a waypoint. - // - ee = exif_content_get_entry (ed->ifd[EXIF_IFD_GPS], EXIF_TAG_GPS_LATITUDE); - if ( ! ( ee && ee->components == 3 && ee->format == EXIF_FORMAT_RATIONAL ) ) - goto MyReturn; - - ll.lat = Rational2Double ( ee->data, - exif_format_get_size(ee->format), - exif_data_get_byte_order(ed) ); - - ee = exif_content_get_entry (ed->ifd[EXIF_IFD_GPS], EXIF_TAG_GPS_LATITUDE_REF); - if ( ee ) { - exif_entry_get_value ( ee, str, 128 ); - if ( str[0] == 'S' ) - ll.lat = -ll.lat; - } - - ee = exif_content_get_entry (ed->ifd[EXIF_IFD_GPS], EXIF_TAG_GPS_LONGITUDE); - if ( ! ( ee && ee->components == 3 && ee->format == EXIF_FORMAT_RATIONAL ) ) + // Hopefully won't have valid images at 0,0! + if ( ll.lat == 0.0 && ll.lon == 0.0 ) goto MyReturn; - ll.lon = Rational2Double ( ee->data, - exif_format_get_size(ee->format), - exif_data_get_byte_order(ed) ); - - ee = exif_content_get_entry (ed->ifd[EXIF_IFD_GPS], EXIF_TAG_GPS_LONGITUDE_REF); - if ( ee ) { - exif_entry_get_value ( ee, str, 128 ); - if ( str[0] == 'W' ) - ll.lon = -ll.lon; - } - // // Not worried if none of the other fields exist, as can default the values to something // @@ -242,6 +376,8 @@ VikWaypoint* a_geotag_create_waypoint_from_file ( const gchar *filename, VikCoor MyReturn: // Finished with EXIF exif_data_free ( ed ); +#endif +#endif return wp; } @@ -270,6 +406,16 @@ VikWaypoint* a_geotag_waypoint_positioned ( const gchar *filename, VikCoord coor wp->coord = coord; wp->altitude = alt; +#ifdef HAVE_LIBGEXIV2 + GExiv2Metadata *gemd = gexiv2_metadata_new (); + if ( gexiv2_metadata_open_path ( gemd, filename, NULL ) ) { + wp->comment = geotag_get_exif_comment ( gemd ); + if ( gexiv2_metadata_has_tag ( gemd, "Exif.Image.XPTitle" ) ) + *name = g_strdup ( gexiv2_metadata_get_tag_interpreted_string ( gemd, "Exif.Image.XPTitle" ) ); + } + metadata_free ( gemd ); +#else +#ifdef HAVE_LIBEXIF ExifData *ed = exif_data_new_from_file ( filename ); // Set info from exif values @@ -288,6 +434,8 @@ VikWaypoint* a_geotag_waypoint_positioned ( const gchar *filename, VikCoord coor // Finished with EXIF exif_data_free ( ed ); } +#endif +#endif vik_waypoint_set_image ( wp, filename ); @@ -309,6 +457,21 @@ gchar* a_geotag_get_exif_date_from_file ( const gchar *filename, gboolean *has_G gchar* datetime = NULL; *has_GPS_info = FALSE; +#ifdef HAVE_LIBGEXIV2 + GExiv2Metadata *gemd = gexiv2_metadata_new (); + if ( gexiv2_metadata_open_path ( gemd, filename, NULL ) ) { + gdouble lat, lon; + *has_GPS_info = ( gexiv2_metadata_get_gps_longitude(gemd,&lon) && gexiv2_metadata_get_gps_latitude(gemd,&lat) ); + + // Prefer 'Photo' version over 'Image' + if ( gexiv2_metadata_has_tag ( gemd, "Exif.Photo.DateTimeOriginal" ) ) + datetime = g_strdup ( gexiv2_metadata_get_tag_interpreted_string ( gemd, "Exif.Photo.DateTimeOriginal" ) ); + else + datetime = g_strdup ( gexiv2_metadata_get_tag_interpreted_string ( gemd, "Exif.Image.DateTimeOriginal" ) ); + } + metadata_free ( gemd ); +#else +#ifdef HAVE_LIBEXIF ExifData *ed = exif_data_new_from_file ( filename ); // Detect EXIF load failure @@ -342,11 +505,13 @@ gchar* a_geotag_get_exif_date_from_file ( const gchar *filename, gboolean *has_G *has_GPS_info = FALSE; exif_data_free ( ed ); - +#endif +#endif return datetime; } +#ifdef HAVE_LIBEXIF /**! If the entry doesn't exist, create it. * Based on exif command line action_create_value function in exif 0.6.20 */ @@ -564,6 +729,7 @@ static void convert_to_entry (const char *set_value, gdouble gdvalue, ExifEntry if ( value_p ) g_warning (_("Warning; Too many components specified!")); } +#endif /** * a_geotag_write_exif_gps: @@ -581,8 +747,29 @@ gint a_geotag_write_exif_gps ( const gchar *filename, VikCoord coord, gdouble al // Save mtime for later use struct stat stat_save; if ( no_change_mtime ) - stat ( filename, &stat_save ); - + if ( stat ( filename, &stat_save ) != 0 ) + g_warning ( "%s couldn't read: %s", __FUNCTION__, filename ); + +#ifdef HAVE_LIBGEXIV2 + GExiv2Metadata *gemd = gexiv2_metadata_new (); + if ( gexiv2_metadata_open_path ( gemd, filename, NULL ) ) { + struct LatLon ll; + vik_coord_to_latlon ( &coord, &ll ); + if ( ! gexiv2_metadata_set_gps_info ( gemd, ll.lon, ll.lat, alt ) ) { + result = 1; // Failed + } + else { + GError *error = NULL; + if ( ! gexiv2_metadata_save_file ( gemd, filename, &error ) ) { + result = 2; + g_warning ( "Write EXIF failure:%s" , error->message ); + g_error_free ( error ); + } + } + } + metadata_free ( gemd ); +#else +#ifdef HAVE_LIBEXIF /* Appears libexif doesn't actually support writing EXIF data directly to files Thus embed command line exif writing method within Viking @@ -661,14 +848,21 @@ gint a_geotag_write_exif_gps ( const gchar *filename, VikCoord coord, gdouble al result = 2; } + exif_data_free ( ed ); +#endif +#endif + if ( no_change_mtime ) { // Restore mtime, using the saved value struct stat stat_tmp; struct utimbuf utb; - stat ( filename, &stat_tmp ); + (void)stat ( filename, &stat_tmp ); utb.actime = stat_tmp.st_atime; utb.modtime = stat_save.st_mtime; - utime ( filename, &utb ); + // Not security critical, thus potential Time of Check Time of Use race condition is not bad + // coverity[toctou] + if ( g_utime ( filename, &utb ) != 0 ) + g_warning ( "%s couldn't set time on: %s", __FUNCTION__, filename ); } return result;