X-Git-Url: https://git.street.me.uk/andy/viking.git/blobdiff_plain/f75d0233efd33a6495d33698ee097c8464d67695..78ac928c4967374d4c7ad5b3024f188074eab36b:/src/geotag_exif.c diff --git a/src/geotag_exif.c b/src/geotag_exif.c index 37adc1f6..de446a15 100644 --- a/src/geotag_exif.c +++ b/src/geotag_exif.c @@ -22,9 +22,6 @@ /* * This uses EXIF information from images to create waypoints at those positions - * TODO: allow writing of image location: - * . Via correlation with a track (c.f. gpscorrelate) (multiple images) - * . Via screen position (individual image) on an existing waypoint * * 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. @@ -34,7 +31,14 @@ #include "globals.h" #include "file.h" +#include +#include +#include +#include +#include +#include #include +#include "libjpeg/jpeg-data.h" /** * Attempt to get a single comment from the various exif fields @@ -243,21 +247,26 @@ MyReturn: } /** - * a_geotag_create_waypoint_positioned: + * a_geotag_waypoint_positioned: * @filename: The image file to process * @coord: The location for positioning the Waypoint * @name: Returns a name for the Waypoint (can be NULL) + * @waypoint: An existing waypoint to update (can be NULL to generate a new waypoint) * - * Returns: An allocated Waypoint or NULL if Waypoint could not be generated + * Returns: An allocated waypoint if the input waypoint is NULL, + * otherwise the passed in waypoint is updated * * Here EXIF processing is used to get non position related information (i.e. just the comment) * */ -VikWaypoint* a_geotag_create_waypoint_positioned ( const gchar *filename, VikCoord coord, gdouble alt, gchar **name ) +VikWaypoint* a_geotag_waypoint_positioned ( const gchar *filename, VikCoord coord, gdouble alt, gchar **name, VikWaypoint *wp ) { *name = NULL; - VikWaypoint *wp = vik_waypoint_new(); - wp->visible = TRUE; + if ( wp == NULL ) { + // Need to create waypoint + wp = vik_waypoint_new(); + wp->visible = TRUE; + } wp->coord = coord; wp->altitude = alt; @@ -282,6 +291,375 @@ VikWaypoint* a_geotag_create_waypoint_positioned ( const gchar *filename, VikCoo vik_waypoint_set_image ( wp, filename ); - return wp; } + +/** + * a_geotag_get_exif_date_from_file: + * @filename: The image file to process + * @has_GPS_info: Returns whether the file has existing GPS information + * + * Returns: An allocated string with the date and time in EXIF_DATE_FORMAT, otherwise NULL if some kind of failure + * + * Here EXIF processing is used to get time information + * + */ +gchar* a_geotag_get_exif_date_from_file ( const gchar *filename, gboolean *has_GPS_info ) +{ + gchar* datetime = NULL; + + ExifData *ed = exif_data_new_from_file ( filename ); + + // Detect EXIF load failure + if ( !ed ) + return datetime; + + gchar str[128]; + ExifEntry *ee; + + ee = exif_content_get_entry (ed->ifd[EXIF_IFD_EXIF], EXIF_TAG_DATE_TIME_ORIGINAL); + if ( ee ) { + exif_entry_get_value ( ee, str, 128 ); + datetime = g_strdup ( str ); + } + + // Check GPS Info + *has_GPS_info = FALSE; + + 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 ) + *has_GPS_info = TRUE; + + exif_data_free ( ed ); + + return datetime; +} + + +/**! If the entry doesn't exist, create it. + * Based on exif command line action_create_value function in exif 0.6.20 + */ +static ExifEntry* my_exif_create_value (ExifData *ed, ExifTag tag, ExifIfd ifd) +{ + ExifEntry *e = exif_content_get_entry (ed->ifd[ifd], tag); + if ( !e ) { + e = exif_entry_new (); + exif_content_add_entry (ed->ifd[ifd], e); + + exif_entry_initialize (e, tag); + + // exif_entry_initialize doesn't seem to do much, especially for the GPS tags + // so have to setup fields ourselves: + e->tag = tag; + + if ( tag == EXIF_TAG_GPS_VERSION_ID ) { + e->format = EXIF_FORMAT_BYTE; + e->components = 4; + e->size = sizeof (char) * e->components; + if ( e->data ) + g_free (e->data); + e->data = g_malloc (e->size); + } + if ( tag == EXIF_TAG_GPS_MAP_DATUM || + tag == EXIF_TAG_GPS_LATITUDE_REF || tag == EXIF_TAG_GPS_LONGITUDE_REF || + tag == EXIF_TAG_GPS_PROCESSING_METHOD ) { + e->format = EXIF_FORMAT_ASCII; + // NB Allocation is handled later on when the actual string used is known + } + if ( tag == EXIF_TAG_GPS_LATITUDE || tag == EXIF_TAG_GPS_LONGITUDE ) { + e->format = EXIF_FORMAT_RATIONAL; + e->components = 3; + e->size = sizeof (ExifRational) * e->components; + if ( e->data ) + g_free (e->data); + e->data = g_malloc (e->size); + } + if ( tag == EXIF_TAG_GPS_ALTITUDE ) { + e->format = EXIF_FORMAT_RATIONAL; + e->components = 1; + e->size = sizeof (ExifRational) * e->components; + if ( e->data ) + g_free (e->data); + e->data = g_malloc (e->size); + } + if ( tag == EXIF_TAG_GPS_ALTITUDE_REF ) { + e->components = 1; + e->size = sizeof (char) * e->components; + if ( e->data ) + g_free (e->data); + e->data = g_malloc (e->size); + } + /* The entry has been added to the IFD, so we can unref it */ + //exif_entry_unref(e); + // Crashes later on, when saving to jpeg if the above unref is enabled!! + // ?Some other malloc problem somewhere? + } + return e; +} + +/** Heavily based on convert_arg_to_entry from exif command line tool. + * But without ExifLog, exitting, use of g_* io functions + * and can take a gdouble value instead of a string + */ +static void convert_to_entry (const char *set_value, gdouble gdvalue, ExifEntry *e, ExifByteOrder o) +{ + unsigned int i, numcomponents; + char *value_p = NULL; + char *buf = NULL; + /* + * ASCII strings are handled separately, + * since they don't require any conversion. + */ + if (e->format == EXIF_FORMAT_ASCII || + e->tag == EXIF_TAG_USER_COMMENT) { + if (e->data) g_free (e->data); + e->components = strlen (set_value) + 1; + if (e->tag == EXIF_TAG_USER_COMMENT) + e->components += 8 - 1; + e->size = sizeof (char) * e->components; + e->data = g_malloc (e->size); + if (!e->data) { + g_warning (_("Not enough memory.")); + return; + } + if (e->tag == EXIF_TAG_USER_COMMENT) { + /* assume ASCII charset */ + /* TODO: get this from the current locale */ + memcpy ((char *) e->data, "ASCII\0\0\0", 8); + memcpy ((char *) e->data + 8, set_value, + strlen (set_value)); + } else + strcpy ((char *) e->data, set_value); + return; + } + + /* + * Make sure we can handle this entry + */ + if ((e->components == 0) && *set_value) { + g_warning (_("Setting a value for this tag is unsupported!")); + return; + } + + gboolean use_string = (set_value != NULL); + if ( use_string ) { + /* Copy the string so we can modify it */ + buf = g_strdup (set_value); + if (!buf) + return; + value_p = strtok (buf, " "); + } + + numcomponents = e->components; + for (i = 0; i < numcomponents; ++i) { + unsigned char s; + + if ( use_string ) { + if (!value_p) { + g_warning (_("Too few components specified (need %d, found %d)\n"), numcomponents, i); + return; + } + if (!isdigit(*value_p) && (*value_p != '+') && (*value_p != '-')) { + g_warning (_("Numeric value expected\n")); + return; + } + } + + s = exif_format_get_size (e->format); + switch (e->format) { + case EXIF_FORMAT_ASCII: + g_warning (_("This shouldn't happen!")); + return; + break; + case EXIF_FORMAT_SHORT: + exif_set_short (e->data + (s * i), o, atoi (value_p)); + break; + case EXIF_FORMAT_SSHORT: + exif_set_sshort (e->data + (s * i), o, atoi (value_p)); + break; + case EXIF_FORMAT_RATIONAL: { + ExifRational er; + + double val = 0.0 ; + if ( use_string && value_p ) + val = fabs (atol (value_p)); + else + val = fabs (gdvalue); + + if ( i == 0 ) { + // One (or first) part rational + + // Sneak peek into tag as location tags need rounding down to give just the degrees part + if ( e->tag == EXIF_TAG_GPS_LATITUDE || e->tag == EXIF_TAG_GPS_LONGITUDE ) { + er.numerator = (ExifLong) floor ( val ); + er.denominator = 1.0; + } + else { + // I don't see any point in doing anything too complicated here, + // such as trying to work out the 'best' denominator + // For the moment use KISS principle. + // Fix a precision of 1/100 metre as that's more than enough for GPS accuracy especially altitudes! + er.denominator = 100.0; + er.numerator = (ExifLong) (val * er.denominator); + } + } + + // Now for Location 3 part rationals do Mins and Seconds format + + // Rounded down minutes + if ( i == 1 ) { + er.denominator = 1.0; + er.numerator = (ExifLong) ( (int) floor ( ( val - floor (val) ) * 60.0 ) ); + } + + // Finally seconds + if ( i == 2 ) { + er.denominator = 100.0; + + // Fractional minute. + double FracPart = ((val - floor(val)) * 60) - (double)(int) floor ( ( val - floor (val) ) * 60.0 ); + er.numerator = (ExifLong) ( (int)floor(FracPart * 6000) ); // Convert to seconds. + } + exif_set_rational (e->data + (s * i), o, er ); + break; + } + case EXIF_FORMAT_LONG: + exif_set_long (e->data + (s * i), o, atol (value_p)); + break; + case EXIF_FORMAT_SLONG: + exif_set_slong (e->data + (s * i), o, atol (value_p)); + break; + case EXIF_FORMAT_BYTE: + case EXIF_FORMAT_SBYTE: + case EXIF_FORMAT_UNDEFINED: /* treat as byte array */ + e->data[s * i] = atoi (value_p); + break; + case EXIF_FORMAT_FLOAT: + case EXIF_FORMAT_DOUBLE: + case EXIF_FORMAT_SRATIONAL: + default: + g_warning (_("Not yet implemented!")); + return; + } + + if ( use_string ) + value_p = strtok (NULL, " "); + + } + + g_free (buf); + + if ( use_string ) + if ( value_p ) + g_warning (_("Warning; Too many components specified!")); +} + +/** + * a_geotag_write_exif_gps: + * @filename: The image file to save information in + * @coord: The location + * @alt: The elevation + * + * Returns: A value indicating success: 0, or some other value for failure + * + */ +gint a_geotag_write_exif_gps ( const gchar *filename, VikCoord coord, gdouble alt, gboolean no_change_mtime ) +{ + gint result = 0; // OK so far... + + // Save mtime for later use + struct stat stat_save; + if ( no_change_mtime ) + stat ( filename, &stat_save ); + + /* + Appears libexif doesn't actually support writing EXIF data directly to files + Thus embed command line exif writing method within Viking + (for example this is done by Enlightment - http://www.enlightenment.org/ ) + This appears to be JPEG only, but is probably 99% of our use case + Alternatively consider using libexiv2 and C++... + */ + + // Actual EXIF settings here... + JPEGData *jdata; + + /* Parse the JPEG file. */ + jdata = jpeg_data_new (); + jpeg_data_load_file (jdata, filename); + + // Get current values + ExifData *ed = exif_data_new_from_file ( filename ); + if ( !ed ) + ed = exif_data_new (); + + // Update ExifData with our new settings + ExifEntry *ee; + // + // I don't understand it, but when saving the 'ed' nothing gets set after putting in the GPS ID tag - so it must come last + // (unless of course there is some bug in the setting of the ID, that prevents subsequent tags) + // + + ee = my_exif_create_value (ed, EXIF_TAG_GPS_ALTITUDE, EXIF_IFD_GPS); + convert_to_entry ( NULL, alt, ee, exif_data_get_byte_order(ed) ); + + // byte 0 meaning "sea level" or 1 if the value is negative. + ee = my_exif_create_value (ed, EXIF_TAG_GPS_ALTITUDE_REF, EXIF_IFD_GPS); + convert_to_entry ( alt < 0.0 ? "1" : "0", 0.0, ee, exif_data_get_byte_order(ed) ); + + ee = my_exif_create_value (ed, EXIF_TAG_GPS_PROCESSING_METHOD, EXIF_IFD_GPS); + // see http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/GPS.html + convert_to_entry ( "MANUAL", 0.0, ee, exif_data_get_byte_order(ed) ); + + ee = my_exif_create_value (ed, EXIF_TAG_GPS_MAP_DATUM, EXIF_IFD_GPS); + convert_to_entry ( "WGS-84", 0.0, ee, exif_data_get_byte_order(ed) ); + + struct LatLon ll; + vik_coord_to_latlon ( &coord, &ll ); + + ee = my_exif_create_value (ed, EXIF_TAG_GPS_LATITUDE_REF, EXIF_IFD_GPS); + // N or S + convert_to_entry ( ll.lat < 0.0 ? "S" : "N", 0.0, ee, exif_data_get_byte_order(ed) ); + + ee = my_exif_create_value (ed, EXIF_TAG_GPS_LATITUDE, EXIF_IFD_GPS); + convert_to_entry ( NULL, ll.lat, ee, exif_data_get_byte_order(ed) ); + + ee = my_exif_create_value (ed, EXIF_TAG_GPS_LONGITUDE_REF, EXIF_IFD_GPS); + // E or W + convert_to_entry ( ll.lon < 0.0 ? "W" : "E", 0.0, ee, exif_data_get_byte_order(ed) ); + + ee = my_exif_create_value (ed, EXIF_TAG_GPS_LONGITUDE, EXIF_IFD_GPS); + convert_to_entry ( NULL, ll.lon, ee, exif_data_get_byte_order(ed) ); + + ee = my_exif_create_value (ed, EXIF_TAG_GPS_VERSION_ID, EXIF_IFD_GPS); + //convert_to_entry ( "2 0 0 0", 0.0, ee, exif_data_get_byte_order(ed) ); + convert_to_entry ( "2 2 0 0", 0.0, ee, exif_data_get_byte_order(ed) ); + + jpeg_data_set_exif_data (jdata, ed); + + if ( jdata ) { + /* Save the modified image. */ + result = jpeg_data_save_file (jdata, filename); + + // Convert result from 1 for success, 0 for failure into our scheme + result = !result; + + jpeg_data_unref (jdata); + } + else { + // Epic fail - file probably not a JPEG + result = 2; + } + + if ( no_change_mtime ) { + // Restore mtime, using the saved value + struct stat stat_tmp; + struct utimbuf utb; + stat ( filename, &stat_tmp ); + utb.actime = stat_tmp.st_atime; + utb.modtime = stat_save.st_mtime; + utime ( filename, &utb ); + } + + return result; +}