/*

    Support for embedded (JPEG) Exif-GPS information.

    Copyright (C) 2008 Olaf Klein, o.b.klein@gpsbabel.org

    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
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111 USA

 */

#include <ctype.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>

#include "defs.h"
#include "garmin_tables.h"
#include "jeeps/gpsmath.h"
#include "strptime.h"

#define MYNAME "exif"

// #define EXIF_DBG

#define UNKNOWN_TIMESTAMP 999999999

#define IFD0		0
#define IFD1		1
#define EXIF_IFD	2		/* dummy index */
#define GPS_IFD		3		/* dummy index */
#define INTER_IFD	4		/* dummy index */

#define EXIF_TYPE_BYTE		1
#define EXIF_TYPE_ASCII		2
#define EXIF_TYPE_SHORT		3
#define EXIF_TYPE_LONG		4
#define EXIF_TYPE_RAT		5
/* TIFF 6.0 */
#define EXIF_TYPE_SBYTE		6
#define EXIF_TYPE_UNK		7
#define EXIF_TYPE_SSHORT	8
#define EXIF_TYPE_SLONG		9
#define EXIF_TYPE_SRAT		10
#define EXIF_TYPE_FLOAT		11
#define EXIF_TYPE_DOUBLE	12
#define EXIF_TYPE_IFD		13
#define EXIF_TYPE_UNICODE	14
#define EXIF_TYPE_COMPLEX	15
#define EXIF_TYPE_LONG8		16
#define EXIF_TYPE_SLONG8	17
#define EXIF_TYPE_IFD8		18

#define BYTE_TYPE(a) ( (a==EXIF_TYPE_BYTE) || (a==EXIF_TYPE_ASCII) || (a==EXIF_TYPE_UNK) )
#define WORD_TYPE(a) ( (a==EXIF_TYPE_SHORT) || (a==EXIF_TYPE_SSHORT) )
#define LONG_TYPE(a) ( (a==EXIF_TYPE_LONG) || (a==EXIF_TYPE_SLONG) || (a==EXIF_TYPE_IFD) )

#define IFD0_TAG_EXIF_IFD_OFFS		0x8769
#define IFD0_TAG_GPS_IFD_OFFS		0x8825

#define IFD1_TAG_STRIP_OFFS		0x0111
#define IFD1_TAG_JPEG_OFFS		0x0201
#define IFD1_TAG_JPEG_SIZE		0x0202

#define EXIF_IFD_TAG_USER_CMT		0x9286
#define EXIF_IFD_TAG_INTER_IFD_OFFS	0xA005

#define GPS_IFD_TAG_VERSION		0x0000
#define GPS_IFD_TAG_LATREF		0x0001
#define GPS_IFD_TAG_LAT			0x0002
#define GPS_IFD_TAG_LONREF		0x0003
#define GPS_IFD_TAG_LON			0x0004
#define GPS_IFD_TAG_ALTREF		0x0005
#define GPS_IFD_TAG_ALT			0x0006
#define GPS_IFD_TAG_TIMESTAMP		0x0007
#define GPS_IFD_TAG_SAT			0x0008
#define GPS_IFD_TAG_MODE		0x000A
#define GPS_IFD_TAG_DOP			0x000B
#define GPS_IFD_TAG_SPEEDREF		0x000C
#define GPS_IFD_TAG_SPEED		0x000D
#define GPS_IFD_TAG_DATUM		0x0012
#define GPS_IFD_TAG_DATESTAMP		0x001D

typedef struct exif_tag_s {
  queue Q;
  gbuint16 id;
  gbuint16 type;
  gbuint32 count;
  gbuint32 value;
  gbuint32 origin;
  gbuint32 size;
#ifdef EXIF_DBG
  gbuint32 offs;
#endif
  unsigned char data_is_dynamic:1;
  void* data;
} exif_tag_t;

typedef struct exif_ifd_s {
  queue Q;
  gbuint32 next_ifd;
  gbuint16 nr;
  gbuint16 count;
  queue tags;
} exif_ifd_t, *exif_ifd_p;

typedef struct exif_app_s {
  queue Q;
  gbuint16 marker;
  gbsize_t len;
  gbfile* fcache;
  gbfile* fexif;
  queue ifds;
} exif_app_t;

static gbfile* fin, *fout;
static queue exif_apps;
static exif_app_t* exif_app;
const waypoint* exif_wpt_ref;
time_t exif_time_ref;
static char exif_success;
static char* exif_fout_name;

static char* opt_filename, *opt_overwrite, *opt_frame, *opt_name;

arglist_t exif_args[] = {
  { "filename", &opt_filename, "Set waypoint name to source filename", "Y", ARGTYPE_BOOL, ARG_NOMINMAX },
  { "frame", &opt_frame, "Time-frame (in seconds)", "10", ARGTYPE_INT, "0", NULL },
  { "name", &opt_name, "Locate waypoint for tagging by this name", NULL, ARGTYPE_STRING, ARG_NOMINMAX },
  { "overwrite", &opt_overwrite, "!OVERWRITE! the original file. Default=N", "N", ARGTYPE_BOOL, ARG_NOMINMAX },
  ARG_TERMINATOR
};

#ifdef EXIF_DBG
static void
print_buff(const char* buf, int sz, const char* cmt)
{
  int i;

  printf("%s: ", cmt);
  for (i = 0; i < sz; i++) {
    printf("%02x ", buf[i] & 0xFF);
  }
  for (i = 0; i < sz; i++) {
    char c = buf[i];
    if (isspace(c)) {
      c = ' ';
    } else if (! isprint(c)) {
      c = '.';
    }
    printf("%c", c);
  }
}
#endif

static gbuint16
exif_type_size(const gbuint16 type)
{
  gbuint16 size;

  switch (type) {
  case EXIF_TYPE_BYTE:
  case EXIF_TYPE_ASCII:
  case EXIF_TYPE_UNK:
    size = 1;
    break;

  case EXIF_TYPE_SHORT:
  case EXIF_TYPE_SSHORT:
  case EXIF_TYPE_UNICODE:
    size = 2;
    break;
  case EXIF_TYPE_IFD:
  case EXIF_TYPE_LONG:
  case EXIF_TYPE_SLONG:
  case EXIF_TYPE_FLOAT:
    size = 4;
    break;

  case EXIF_TYPE_RAT:
  case EXIF_TYPE_SRAT:
  case EXIF_TYPE_DOUBLE:
  case EXIF_TYPE_LONG8:
  case EXIF_TYPE_SLONG8:
  case EXIF_TYPE_IFD8:
    size = 8;
    break;

  default:
    fatal(MYNAME ": Unknown data type %d! Please report.\n", type);
  }
  return size;
}

static char*
exif_time_str(const time_t time)
{
  struct tm tm;
  char* res;

  tm = *localtime(&time);
  tm.tm_year += 1900;
  tm.tm_mon += 1;
  xasprintf(&res, "%04d/%02d/%02d, %02d:%02d:%02d",
            tm.tm_year, tm.tm_mon, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);

  return res;
}

static char*
exif_read_str(exif_tag_t* tag)
{
  // Panasonic DMC-TZ10 stores datum with trailing spaces.
  char* buf = xstrndup((char*)tag->data, tag->size);
  rtrim(buf);
  return buf;
}

static double
exif_read_double(const exif_tag_t* tag, const int index)
{
  unsigned int num, den;
  gbint32* data = (gbint32*)tag->data;

  num = data[index * 2];
  den = data[(index * 2) + 1];
  if (den == 0) {
    den = 1;
  }

  return (double)num / (double)den;
}

static double
exif_read_coord(const exif_tag_t* tag)
{
  double res, min, sec;

  res = exif_read_double(tag, 0);
  if (tag->count == 1) {
    return res;
  }

  min = exif_read_double(tag, 1);
  res += (min / 60);
  if (tag->count == 2) {
    return res;
  }

  sec = exif_read_double(tag, 2);
  res += (sec / 3600);

  return res;
}

static time_t
exif_read_timestamp(const exif_tag_t* tag)
{
  double hour, min, sec;

  hour = exif_read_double(tag, 0);
  min = exif_read_double(tag, 1);
  sec = exif_read_double(tag, 2);

  return ((int)hour * SECONDS_PER_HOUR) + ((int)min * 60) + (int)sec;
}

static time_t
exif_read_datestamp(const exif_tag_t* tag)
{
  struct tm tm;
  char* str;

  memset(&tm, 0, sizeof(tm));
  str = xstrndup((char*)tag->data, tag->size);
  sscanf(str, "%d:%d:%d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday);
  xfree(str);

  tm.tm_year -= 1900;
  tm.tm_mon -= 1;

  return mkgmtime(&tm);
}

static void
exif_release_tag(exif_tag_t* tag)
{
  dequeue(&tag->Q);
  if (tag->data_is_dynamic) {
    xfree(tag->data);
  }
  xfree(tag);
}

static void
exif_release_ifd(exif_ifd_t* ifd)
{
  if (ifd != NULL) {
    queue* elem, *tmp;

    QUEUE_FOR_EACH(&ifd->tags, elem, tmp) {
      exif_release_tag((exif_tag_t*)elem);
    }
    xfree(ifd);
  }
}

static void
exif_release_apps(void)
{
  queue* e0, *t0;

  QUEUE_FOR_EACH(&exif_apps, e0, t0) {
    queue* e1, *t1;
    exif_app_t* app = (exif_app_t*)dequeue(e0);

    if (app->fcache) {
      gbfclose(app->fcache);
    }
    if (app->fexif) {
      gbfclose(app->fexif);
    }
    QUEUE_FOR_EACH(&app->ifds, e1, t1) {
      exif_ifd_t* ifd = (exif_ifd_t*)dequeue(e1);
      exif_release_ifd(ifd);
    }
    xfree(app);
  }
}

static gbuint32
exif_ifd_size(exif_ifd_t* ifd)
{
  queue* elem, *tmp;
  gbuint32 res = 6; 	/* nr of tags + next_ifd */

  res += (ifd->count * 12);
  QUEUE_FOR_EACH(&ifd->tags, elem, tmp) {
    exif_tag_t* tag = (exif_tag_t*)elem;
    if (tag->size > 4) {
      gbuint32 size = tag->size;
      if (size & 1) {
        size++;
      }
      res += size;
    }
  }

  return res;
}

static exif_app_t*
exif_load_apps(void)
{
  exif_app_t* exif_app = NULL;

  while (! gbfeof(fin)) {
    exif_app_t* app = (exif_app_t*) xcalloc(sizeof(*app), 1);

    ENQUEUE_TAIL(&exif_apps, &app->Q);
    QUEUE_INIT(&app->ifds);
    app->fcache = gbfopen(NULL, "wb", MYNAME);

    app->marker = gbfgetuint16(fin);
    app->len = gbfgetuint16(fin);
#ifdef EXIF_DBG
    printf(MYNAME ": api = %02X, len = %d, offs = %04X\n", app->marker & 0xFF, app->len, gbftell(fin));
#endif
    if (exif_app || (app->marker == 0xFFDA)) /* compressed data */ {
      gbfcopyfrom(app->fcache, fin, 0x7FFFFFFF);
#ifdef EXIF_DBG
      printf(MYNAME ": compressed data size = %d\n", gbftell(app->fcache));
#endif
    } else {
      gbfcopyfrom(app->fcache, fin, app->len - 2);
      if (app->marker == 0xFFE1) {
        exif_app = app;
      }
    }
  }

  return exif_app;
}

static exif_ifd_t*
exif_read_ifd(exif_app_t* app, const gbuint16 ifd_nr, gbsize_t offs,
              gbuint32* exif_ifd_ofs, gbuint32* gps_ifd_ofs, gbuint32* inter_ifd_ofs)
{
  queue* elem, *tmp;
  gbuint16 i;
  exif_ifd_t* ifd;
  gbfile* fin = app->fexif;

  ifd = (exif_ifd_t*) xcalloc(sizeof(*ifd), 1);
  QUEUE_INIT(&ifd->tags);
  ENQUEUE_TAIL(&app->ifds, &ifd->Q);
  ifd->nr = ifd_nr;

  gbfseek(fin, offs, SEEK_SET);
  ifd->count = gbfgetuint16(fin);

#ifdef EXIF_DBG
  {
    char* name;
    switch (ifd_nr) {
    case IFD0:
      name = "IFD0";
      break;
    case IFD1:
      name = "IFD1";
      break;
    case GPS_IFD:
      name = "GPS";
      break;
    case EXIF_IFD:
      name = "EXIF";
      break;
    case INTER_IFD:
      name = "INTER";
      break;
    default:
      name = "private";
      break;
    }
    printf(MYNAME "-offs 0x%04X: Number of items in IFD%d \"%s\" = %d (0x%2x)\n",
           offs, ifd_nr, name, ifd->count, ifd->count);
  }
#endif
  if (ifd->count == 0) {
    return ifd;
  }

  for (i = 0; i < ifd->count; i++) {
    exif_tag_t* tag;
    offs = gbftell(fin);

    tag = (exif_tag_t*) xcalloc(sizeof(*tag), 1);

    ENQUEUE_TAIL(&ifd->tags, &tag->Q);

    tag->id = gbfgetuint16(fin);
    tag->type = gbfgetuint16(fin);
    tag->count = gbfgetuint32(fin);
    tag->size = exif_type_size(tag->type) * tag->count;
    tag->data = &tag->value;
#ifdef EXIF_DBG
    tag->offs = offs;
#endif

    if (BYTE_TYPE(tag->type) && (tag->count <= 4)) {
      gbfread(tag->data, 4, 1, fin);
    } else {
      tag->value = gbfgetuint32(fin);
      tag->origin = tag->value;
    }

    if (ifd_nr == IFD0) {
      if (tag->id == IFD0_TAG_EXIF_IFD_OFFS) {
        *exif_ifd_ofs = tag->value;
      } else if (tag->id == IFD0_TAG_GPS_IFD_OFFS) {
        *gps_ifd_ofs = tag->value;
      }
    } else if (ifd_nr == EXIF_IFD) {
      if (tag->id == EXIF_IFD_TAG_INTER_IFD_OFFS) {
        *inter_ifd_ofs = tag->value;
      }
    }
  }

  ifd->next_ifd = gbfgetuint16(fin);

  QUEUE_FOR_EACH(&ifd->tags, elem, tmp) {
    exif_tag_t* tag = (exif_tag_t*)elem;
    if ((tag->size > 4) && (tag->value)) {
      gbuint16 i;
      char* ptr;

      tag->data = xmalloc(tag->size);
      tag->data_is_dynamic = 1;

      ptr = (char*) tag->data;
      gbfseek(fin, tag->value, SEEK_SET);

      if (BYTE_TYPE(tag->type)) {
        gbfread(ptr, tag->count, 1, fin);
      } else for (i = 0; i < tag->count; i++) {
          switch (tag->type) {
          case EXIF_TYPE_SHORT:
          case EXIF_TYPE_SSHORT:
            *(gbint16*)ptr = gbfgetuint16(fin);
            break;
          case EXIF_TYPE_IFD:
          case EXIF_TYPE_LONG:
          case EXIF_TYPE_SLONG:
            *(gbint32*)ptr = gbfgetuint32(fin);
            break;
          case EXIF_TYPE_RAT:
          case EXIF_TYPE_SRAT:
            *(gbint32*)ptr = gbfgetuint32(fin);
            *(gbint32*)(ptr+4) = gbfgetuint32(fin);
            break;
          case EXIF_TYPE_FLOAT:
            *(float*)ptr = gbfgetflt(fin);
            break;
          case EXIF_TYPE_DOUBLE:
            *(double*)ptr = gbfgetdbl(fin);
            break;
          default:
            gbfread(ptr, 1, 1, fin);
            break;
          }
          ptr += (tag->size / tag->count);
        }
    }
#ifdef EXIF_DBG
    printf(MYNAME "-offs 0x%04X: ifd=%d id=0x%04X t=0x%04X c=%4d s=%4d v=0x%08X",
           tag->offs, ifd->nr, tag->id, tag->type, tag->count, tag->size, tag->value);
    if (tag->type == EXIF_TYPE_ASCII) {
      printf(" \"%s\"", exif_read_str(tag));
    }
    printf("\n");
#endif
  }

  return ifd;
}

static void
exif_read_app(exif_app_t* app)
{
  gbsize_t offs;
  gbuint32 exif_ifd_ofs, gps_ifd_ofs, inter_ifd_ofs;
  exif_ifd_t* ifd;
  gbfile* fin = app->fexif;

#ifdef EXIF_DBG
  printf(MYNAME ": read_app...\n");
  print_buff((const char*)fin->handle.mem, 16, MYNAME);
  printf("\n");
#endif
  exif_ifd_ofs = gps_ifd_ofs = inter_ifd_ofs = 0;

  gbfseek(fin, 4, SEEK_SET);
  offs = gbfgetuint32(fin);

  ifd = exif_read_ifd(app, IFD0, offs, &exif_ifd_ofs, &gps_ifd_ofs, &inter_ifd_ofs);
  if (ifd == NULL) {
    return;
  }

  if (ifd->next_ifd) {
    ifd = exif_read_ifd(app, IFD1, ifd->next_ifd, &exif_ifd_ofs, &gps_ifd_ofs, &inter_ifd_ofs);
  }
  if (exif_ifd_ofs) {
    ifd = exif_read_ifd(app, EXIF_IFD, exif_ifd_ofs, NULL, NULL, &inter_ifd_ofs);
  }
  if (gps_ifd_ofs) {
    ifd = exif_read_ifd(app, 3, gps_ifd_ofs, NULL, NULL, NULL);
  }
  if (inter_ifd_ofs) {
    ifd = exif_read_ifd(app, 4, inter_ifd_ofs, NULL, NULL, NULL);
  }
}

static void
exif_examine_app(exif_app_t* app)
{
  gbuint16 endianess;
  gbuint32 ident;
  gbfile* ftmp = exif_app->fcache;
  int i;

  gbfrewind(ftmp);
  ident = gbfgetuint32(ftmp);
  is_fatal(ident != 0x66697845, MYNAME ": Invalid EXIF header magic.");
  is_fatal(gbfgetint16(ftmp) != 0, MYNAME ": Error in EXIF header.");
  endianess = gbfgetint16(ftmp);

#ifdef EXIF_DBG
  printf(MYNAME ": endianess = 0x%04X\n", endianess);
#endif
  if (endianess == 0x4949) {
    ftmp->big_endian = 0;
  } else if (endianess == 0x4D4D) {
    ftmp->big_endian = 1;
  } else {
    fatal(MYNAME ": Invalid endianess identifier 0x%04X!\n", endianess);
  }

  gbfseek(ftmp, 6, SEEK_SET);
  app->fexif = gbfopen(NULL, "wb", MYNAME);
  app->fexif->big_endian = ftmp->big_endian;
  i = gbfcopyfrom(app->fexif, ftmp, 0x7FFFFFFF);

  exif_read_app(exif_app);
}

static exif_ifd_t*
exif_find_ifd(exif_app_t* app, const gbuint16 ifd_nr)
{
  queue* e0, *t0;

  QUEUE_FOR_EACH(&app->ifds, e0, t0) {
    exif_ifd_t* ifd = (exif_ifd_t*)e0;

    if (ifd->nr == ifd_nr) {
      return ifd;
    }
  }
  return NULL;
}

static exif_tag_t*
exif_find_tag(exif_app_t* app, const gbuint16 ifd_nr, const gbuint16 tag_id)
{
  exif_ifd_t* ifd = exif_find_ifd(app, ifd_nr);
  if (ifd != NULL) {
    queue* elem, *tmp;
    QUEUE_FOR_EACH(&ifd->tags, elem, tmp) {
      exif_tag_t* tag = (exif_tag_t*)elem;
      if (tag->id == tag_id) {
        return tag;
      }
    }
  }
  return NULL;
}

static time_t
exif_get_exif_time(exif_app_t* app)
{
  exif_tag_t* tag;
  time_t res = 0;

  tag = exif_find_tag(app, EXIF_IFD, 0x9003);			/* DateTimeOriginal from EXIF */
  if (! tag) {
    tag = exif_find_tag(app, IFD0, 0x0132);  /* DateTime from IFD0 */
  }
  if (! tag) {
    tag = exif_find_tag(app, EXIF_IFD, 0x9004);  /* DateTimeDigitized from EXIF */
  }
  if (tag) {
    struct tm tm;
    char* c, *str;

    memset(&tm, 0, sizeof(tm));
    str = exif_read_str(tag);
    c = strptime(str, "%Y:%m:%d %H:%M:%S", &tm);
    if (c && (*c == '\0')) {
      res = mklocaltime(&tm);
    }

    xfree(str);
  }
  return res;
}

static waypoint*
exif_waypt_from_exif_app(exif_app_t* app)
{
  waypoint* wpt;
  queue* elem, *tmp;
  exif_ifd_t* ifd;
  exif_tag_t* tag;
  char lat_ref = '\0';
  char lon_ref = '\0';
  char alt_ref = 0;
  char speed_ref = '\0';
  char* datum = NULL;
  char mode = '\0';
  double gpsdop = unknown_alt;
  double alt = unknown_alt;
  time_t timestamp = UNKNOWN_TIMESTAMP;
  time_t datestamp = UNKNOWN_TIMESTAMP;

  ifd = exif_find_ifd(app, GPS_IFD);
  if (ifd == NULL) {
    return NULL;
  }

  wpt = waypt_new();

  wpt->latitude = unknown_alt;
  wpt->longitude = unknown_alt;

  QUEUE_FOR_EACH(&ifd->tags, elem, tmp) {
    tag = (exif_tag_t*)elem;

    switch (tag->id) {
    case GPS_IFD_TAG_VERSION:
      break;
    case GPS_IFD_TAG_LATREF:
      lat_ref = *(char*)tag->data;
      break;
    case GPS_IFD_TAG_LAT:
      wpt->latitude = exif_read_coord(tag);
      break;
    case GPS_IFD_TAG_LONREF:
      lon_ref = *(char*)tag->data;
      break;
    case GPS_IFD_TAG_LON:
      wpt->longitude = exif_read_coord(tag);
      break;
    case GPS_IFD_TAG_ALTREF:
      alt_ref = *(char*)tag->data;
      break;
    case GPS_IFD_TAG_ALT:
      alt = exif_read_double(tag, 0);
      break;
    case GPS_IFD_TAG_TIMESTAMP:
      timestamp = exif_read_timestamp(tag);
      break;
    case GPS_IFD_TAG_SAT:
      wpt->sat = atoi((char*)tag->data);
      break;
    case GPS_IFD_TAG_MODE:
      mode = *(char*)tag->data;
      break;
    case GPS_IFD_TAG_DOP:
      gpsdop = exif_read_double(tag, 0);
      break;
    case GPS_IFD_TAG_SPEEDREF:
      speed_ref = *(char*)tag->data;
      break;
    case GPS_IFD_TAG_SPEED:
      WAYPT_SET(wpt, speed, exif_read_double(tag, 0));
      break;
    case GPS_IFD_TAG_DATUM:
      datum = exif_read_str(tag);
      break;
    case GPS_IFD_TAG_DATESTAMP:
      datestamp = exif_read_datestamp(tag);
      break;
    }
  }

  if ((wpt->latitude == unknown_alt) || (wpt->longitude == unknown_alt)) {
    fatal(MYNAME ": Missing GPSLatitude and/or GPSLongitude!\n");
  }

  if (lat_ref == 'S') {
    wpt->latitude *= -1;
  } else if (lat_ref != 'N') {
    warning(MYNAME ": GPSLatitudeRef not set! Using N(orth).\n");
  }

  if (lon_ref == 'W') {
    wpt->longitude *= -1;
  } else if (lon_ref != 'E') {
    warning(MYNAME ": GPSLongitudeRef not set! Using E(east).\n");
  }

#ifdef EXIF_DBG
  printf(MYNAME "-GPSLatitude =  %12.7f\n", wpt->latitude);
  printf(MYNAME "-GPSLongitude = %12.7f\n", wpt->longitude);
#endif
  if (datum) {
    int idatum = gt_lookup_datum_index(datum, MYNAME);
    if (idatum < 0) {
      fatal(MYNAME ": Unknown GPSMapDatum \"%s\"!\n", datum);
    }
    if (idatum != DATUM_WGS84) {
      double alt;
      GPS_Math_WGS84_To_Known_Datum_M(wpt->latitude, wpt->longitude, 0.0,
                                      &wpt->latitude, &wpt->longitude, &alt, idatum);
    }
    xfree(datum);
  }

  if (alt != unknown_alt) {
    if (alt_ref != 0) {
      warning(MYNAME ": Invalid GPSAltitudeRef (%d)! Using 0 (= Sea level).\n", alt_ref);
    }
    wpt->altitude = alt;
#ifdef EXIF_DBG
    printf(MYNAME "-GPSAltitude =  %12.7f m\n", wpt->altitude);
#endif
  }

  if WAYPT_HAS(wpt, speed) {
    switch (speed_ref) {
    case 'K':
      wpt->speed = KPH_TO_MPS(wpt->speed);
      break;
    case 'M':
      wpt->speed = MPH_TO_MPS(wpt->speed);
      break;
    case 'N':
      wpt->speed = KNOTS_TO_MPS(wpt->speed);
      break;
    default:
      wpt->speed = 0;
      WAYPT_UNSET(wpt, speed);
      warning(MYNAME ": Unknown GPSSpeedRef unit %c (0x%02x)!\n", speed_ref, speed_ref);
    }
#ifdef EXIF_DBG
    if WAYPT_HAS(wpt, speed) {
      printf(MYNAME "-GPSSpeed = %12.2f m/s\n", wpt->speed);
    }
#endif
  }

  if (mode == '2') {
    wpt->fix = fix_2d;
    if (gpsdop != unknown_alt) {
      wpt->hdop = gpsdop;
    }
  } else if (mode == '3') {
    wpt->fix = fix_3d;
    if (gpsdop != unknown_alt) {
      wpt->pdop = gpsdop;
    }
  }

  if (timestamp != UNKNOWN_TIMESTAMP) {
    if (datestamp != UNKNOWN_TIMESTAMP) {
      timestamp += datestamp;
    }
  } else {
    timestamp = datestamp;
  }
  if (timestamp != UNKNOWN_TIMESTAMP) {
#ifdef EXIF_DBG
    char* str = exif_time_str(timestamp);
    printf(MYNAME "-GPSTimeStamp =   %s\n", str);
    xfree(str);
#endif
    wpt->creation_time = timestamp;
  } else {
    wpt->creation_time = exif_get_exif_time(app);
  }

  tag = exif_find_tag(app, EXIF_IFD, EXIF_IFD_TAG_USER_CMT); /* UserComment */
  if (tag && (tag->size > 8)) {
    char* str = NULL;
    if (memcmp(tag->data, "ASCII\0\0\0", 8) == 0) {
      str = xstrndup((char*)tag->data + 8, tag->size - 8);
    } else if (memcmp(tag->data, "UNICODE\0", 8) == 0) {
      int i, len = (tag->size - 8) / 2;
      gbint16* s = (gbint16*)((char*)tag->data + 8);
      for (i = 0; i < len; i++) {
        s[i] = be_read16(&s[i]);  /* always BE ? */
      }
      str = cet_str_uni_to_any(s, len, global_opts.charset);
    }
    if (str != NULL) {
      wpt->notes = str;
    }
  }

  if (opt_filename) {
    char* c, *cx;
    char* str = xstrdup(fin->name);

    cx = str;
    if ((c = strrchr(cx, ':'))) {
      cx = c + 1;
    }
    if ((c = strrchr(cx, '\\'))) {
      cx = c + 1;
    }
    if ((c = strrchr(cx, '/'))) {
      cx = c + 1;
    }
    if (((c = strchr(cx, '.'))) && (c != cx)) {
      *c = '\0';
    }

    if (wpt->shortname) {
      xfree(wpt->shortname);
    }
    wpt->shortname = xstrdup(cx);
    xfree(str);
  }

  return wpt;
}

static void
exif_dec2frac(double val, gbint32* num, gbint32* den)
{
  char sval[16], snum[16];
  char dot = 0;
  int den1 = 1;
  int num1, num2, den2, rem;
  char* cx;
  double vx;

  if (val < 0.000000001) {
    val = 0.0;
  } else if (val > 999999999.0) {
    fatal(MYNAME ": Value (%f) to big for a rational representation!\n", val);
  }

  num1 = 0;
  vx = fabs(val);
  while (vx > 1) {
    num1++;
    vx = vx / 10;
  }

  snprintf(sval, sizeof(sval), "%*.*f", 9, 9 - num1, fabs(val));
  snum[0] = '\0';

  cx = sval;
  while (*cx) {
    if (dot) {
      den1 *= 10;
    }
    if (*cx == '.') {
      dot = 1;
    } else {
      strncat(snum, cx, 1);
    }
    cx++;
  }

  num1 = atoi(snum);
  if (den1 == 1) {
    *num = num1;
    *den = den1;
  }

  num2 = num1;
  den2 = den1;
  rem  = 1;

  /* Euclid's Algorithm to find the gcd */
  while (num2 % den2) {
    rem = num2 % den2;
    num2 = den2;
    den2 = rem;
  }
  if (den2 != den1) {
    rem = den2;
  }

  *num = num1 / rem;
  *den = den1 / rem;
}

static exif_tag_t*
exif_put_value(const int ifd_nr, const gbuint16 tag_id, const gbuint16 type, const gbuint32 count, const int index, const void* data)
{
  exif_tag_t* tag = NULL;
  exif_ifd_t* ifd;
  gbuint16 item_size, size;

  ifd = exif_find_ifd(exif_app, ifd_nr);
  if (ifd == NULL) {
    ifd = (exif_ifd_t*) xcalloc(sizeof(*ifd), 1);
    ifd->nr = ifd_nr;
    QUEUE_INIT(&ifd->tags);
    ENQUEUE_TAIL(&exif_app->ifds, &ifd->Q);
  } else {
    tag = exif_find_tag(exif_app, ifd_nr, tag_id);
  }

  item_size = exif_type_size(type);

  if ((data == NULL) || (count < 1) || (index < 0)) {
    size = 0;
  } else {
    size = (index + count) * item_size;
  }

  if (tag == NULL) {
    if (size == 0) {
      return NULL;
    }

    tag = (exif_tag_t*) xcalloc(sizeof(*tag), 1);

    tag->id = tag_id;
    tag->type = type;
    tag->count = index + count;
    tag->size = size;
    tag->data = xcalloc((size < 4) ? 4 : size, 1);
    tag->data_is_dynamic = 1;
    ifd->count++;

    ENQUEUE_TAIL(&ifd->tags, &tag->Q);
  } else {
    if (size == 0) {	/* remove this element */
      ifd->count--;
      exif_release_tag(tag);
      return NULL;
    }
    if (tag->count < (index + count)) {
      if (! tag->data_is_dynamic) {
        void* tmp = xmalloc(tag->size < 4 ? 4 : tag->size);
        memcpy(tmp, tag->data, tag->size);
        tag->data = tmp;
        tag->data_is_dynamic = 1;
      }
      tag->size = size;
      tag->count = index + count;
      tag->data = xrealloc(tag->data, size < 4 ? 4 : size);
    }
  }

  switch (type) {
  case EXIF_TYPE_RAT:
  case EXIF_TYPE_SRAT: {
    double val = *(double*)data;
    gbuint32* dest = (gbuint32*) tag->data;

    if ((int)val == val) {
      dest[index * 2] = (int)val;
      dest[(index * 2) + 1] = 1;
    } else {
      gbint32 Nom, Den;
      exif_dec2frac(val, &Nom, &Den);
      if ((type == EXIF_TYPE_SRAT) && (val < 0.0)) {
        Nom *= -1;
      }
      dest[index * 2] = Nom;
      dest[(index * 2) + 1] = Den;
    }
  }
  break;
  default: {
    char* dest = (char*) tag->data;
    memcpy(&dest[index * item_size], data, count * item_size);
  }
  }
  return tag;
}


static void
exif_put_double(const int ifd_nr, const int tag_id, const int index, const double val)
{
  double d = fabs(val);
  exif_put_value(ifd_nr, tag_id, EXIF_TYPE_RAT, 1, index, &d);
}


static void
exif_put_str(const int ifd_nr, const int tag_id, const char* val)
{
  int len = (val) ? strlen(val) + 1 : 0;
  exif_put_value(ifd_nr, tag_id, EXIF_TYPE_ASCII, len, 0, val);
}

static void
exif_put_coord(const int ifd_nr, const int tag_id, const double val)
{
  double  vmin, vsec;
  int     vint;

  vint = abs((int) val);
  vmin = 60.0 * (fabs(val) - vint);
  vsec = 60.0 * (vmin - floor(vmin));
  vmin = floor(vmin);

  exif_put_double(ifd_nr, tag_id, 0, (double)vint);
  exif_put_double(ifd_nr, tag_id, 1, (double)vmin);
  exif_put_double(ifd_nr, tag_id, 2, (double)vsec);
}

static void
exif_put_long(const int ifd_nr, const int tag_id, const int index, const gbint32 val)
{
  exif_put_value(ifd_nr, tag_id, EXIF_TYPE_LONG, 1, index, &val);
}

static void
exif_remove_tag(const int ifd_nr, const int tag_id)
{
  exif_put_value(ifd_nr, tag_id, EXIF_TYPE_BYTE, 0, 0, NULL);
}

static void
exif_find_wpt_by_time(const waypoint* wpt)
{
  if (wpt->creation_time <= 0) {
    return;
  }

  if (exif_wpt_ref == NULL) {
    exif_wpt_ref = wpt;
  } else if (abs(exif_time_ref - wpt->creation_time) < abs(exif_time_ref - exif_wpt_ref->creation_time)) {
    exif_wpt_ref = wpt;
  }
}

static void
exif_find_wpt_by_name(const waypoint* wpt)
{
  if (exif_wpt_ref != NULL) {
    return;
  } else if ((wpt->shortname != NULL) && (case_ignore_strcmp(wpt->shortname, opt_name) == 0)) {
    exif_wpt_ref = wpt;
  }
}


static int
exif_sort_tags_cb(const queue* A, const queue* B)
{
  exif_tag_t* ta = (exif_tag_t*)A;
  exif_tag_t* tb = (exif_tag_t*)B;

  return ta->id - tb->id;
}

static int
exif_sort_ifds_cb(const queue* A, const queue* B)
{
  exif_ifd_t* ia = (exif_ifd_t*)A;
  exif_ifd_t* ib = (exif_ifd_t*)B;

  return ia->nr - ib->nr;
}

static void
exif_write_value(exif_tag_t* tag, gbfile* fout)
{
  if (tag->size > 4) {
    gbfputuint32(tag->value, fout);  /* offset to data */
  } else {
    char* data = (char*) tag->data;

    if BYTE_TYPE(tag->type) {
      gbfwrite(data, 4, 1, fout);
    } else if WORD_TYPE(tag->type) {
      gbfputuint16(*(gbuint16*)data, fout);
      gbfputuint16(*(gbuint16*)(data+2), fout);
    } else if LONG_TYPE(tag->type) {
      gbfputuint32(*(gbuint32*)data, fout);
    } else if (tag->type == EXIF_TYPE_FLOAT) {
      gbfputflt(*(float*)data, fout);
    } else {
      fatal(MYNAME ": Unknown data type %d!\n", tag->type);
    }
  }
}

static void
exif_write_ifd(const exif_ifd_t* ifd, const char next, gbfile* fout)
{
  gbsize_t offs;
  queue* elem, *tmp;

  gbfputuint16(ifd->count, fout);
  offs = gbftell(fout) + (ifd->count * 12) + 4;

  QUEUE_FOR_EACH(&ifd->tags, elem, tmp) {
    exif_tag_t* tag = (exif_tag_t*)elem;

    gbfputuint16(tag->id, fout);
    gbfputuint16(tag->type, fout);
    gbfputuint32(tag->count, fout);
    if (tag->size > 4) {
      tag->value = offs;
      offs += tag->size;
      if (offs & 1) {
        offs++;
      }
      gbfputuint32(tag->value, fout);
    } else {
      exif_write_value(tag, fout);
    }
  }

  if (next) {
    gbfputuint32(offs, fout);
  } else {
    gbfputuint32(0, fout);
  }

  QUEUE_FOR_EACH(&ifd->tags, elem, tmp) {
    exif_tag_t* tag = (exif_tag_t*)elem;

    if (tag->size > 4) {
      gbuint16 i;
      char* ptr = (char*) tag->data;

      if BYTE_TYPE(tag->type) {
        gbfwrite(tag->data, tag->size, 1, fout);
      } else for (i = 0; i < tag->count; i++) {
          switch (tag->type) {
          case EXIF_TYPE_SHORT:
          case EXIF_TYPE_SSHORT:
            gbfputuint16(*(gbint16*)ptr, fout);
            break;
          case EXIF_TYPE_LONG:
          case EXIF_TYPE_SLONG:
          case EXIF_TYPE_IFD:
            gbfputuint32(*(gbint32*)ptr, fout);
            break;
          case EXIF_TYPE_RAT:
          case EXIF_TYPE_SRAT:
            gbfputuint32(*(gbint32*)ptr, fout);
            gbfputuint32(*(gbint32*)(ptr+4), fout);
            break;
          case EXIF_TYPE_FLOAT:
            gbfputflt(*(float*)ptr, fout);
            break;
          case EXIF_TYPE_DOUBLE:
            gbfputdbl(*(double*)ptr, fout);
            break;
          default:
            gbfwrite(ptr, exif_type_size(tag->type), 1, fin);
            break;
          }
          ptr += (tag->size / tag->count);
        }
      if (gbftell(fout) & 1) {
        gbfputc(0, fout);
      }
    }
  }
}

static void
exif_write_apps(void)
{
  queue* e0, *t0;

  gbfputuint16(0xFFD8, fout);

  QUEUE_FOR_EACH(&exif_apps, e0, t0) {
    exif_app_t* app = (exif_app_t*)e0;

    gbfputuint16(app->marker, fout);

    if (app == exif_app) {
      queue* e1, *t1;
      gbuint16 len = 8;
      gbfile* ftmp;
      exif_tag_t* tag;

      exif_put_long(IFD0, IFD0_TAG_GPS_IFD_OFFS, 0, 0);
      exif_put_long(GPS_IFD, GPS_IFD_TAG_VERSION, 0, 2);

      sortqueue(&exif_app->ifds, exif_sort_ifds_cb);

      QUEUE_FOR_EACH(&app->ifds, e1, t1) {
        exif_ifd_t* ifd = (exif_ifd_t*)e1;

        if (ifd->nr == GPS_IFD) {
          exif_put_long(IFD0, IFD0_TAG_GPS_IFD_OFFS, 0, len);
        } else if (ifd->nr == EXIF_IFD) {
          exif_put_long(IFD0, IFD0_TAG_EXIF_IFD_OFFS, 0, len);
        } else if (ifd->nr == INTER_IFD) {
          exif_put_long(EXIF_IFD, EXIF_IFD_TAG_INTER_IFD_OFFS, 0, len);
        }

        len += exif_ifd_size(ifd);
      }

      len += 4; /* DWORD(0) after last ifd */

      if ((tag = exif_find_tag(app, IFD1, IFD1_TAG_JPEG_OFFS))) {
        exif_put_long(IFD1, IFD1_TAG_JPEG_OFFS, 0, len);
      }

      QUEUE_FOR_EACH(&app->ifds, e1, t1) {
        exif_ifd_t* ifd = (exif_ifd_t*)e1;
        sortqueue(&ifd->tags, exif_sort_tags_cb);
      }

      ftmp = gbfopen_be(NULL, "wb", MYNAME);
      ftmp->big_endian = app->fcache->big_endian;

      gbfwrite((ftmp->big_endian) ? "MM" : "II", 2, 1, ftmp);
      gbfputuint16(0x2A, ftmp);
      gbfputuint32(0x08, ftmp);	/* offset to first IFD */

      QUEUE_FOR_EACH(&app->ifds, e1, t1) {
        exif_ifd_t* ifd = (exif_ifd_t*)e1;
        exif_ifd_t* ifd_next = (exif_ifd_t*)t1;
        char next;

        if ((ifd->nr == IFD0) && (ifd_next->nr == IFD1)) {
          next = 1;
        } else {
          next = 0;
        }

        exif_write_ifd(ifd, next, ftmp);
        len = gbftell(ftmp);
      }

      gbfputuint32(0, ftmp); /* DWORD(0) after last ifd */

      if ((tag = exif_find_tag(app, IFD1, IFD1_TAG_JPEG_OFFS))) {
        gbsize_t offs = tag->origin;
        if ((tag = exif_find_tag(app, IFD1, IFD1_TAG_JPEG_SIZE))) {
          gbfseek(app->fexif, offs, SEEK_SET);
          gbfcopyfrom(ftmp, app->fexif, tag->value);
        }
      }

      len = gbftell(ftmp);
      gbfrewind(ftmp);
      gbfputuint16(len + 8, fout);
      gbfwrite("Exif\0\0", 6, 1, fout);
      gbfcopyfrom(fout, ftmp, len);

      gbfclose(ftmp);
    } else {
      gbfputuint16(app->len, fout);
      gbfrewind(app->fcache);
      gbfcopyfrom(fout, app->fcache, 0x7FFFFFFF);
    }
  }
}

/*******************************************************************************
* %%%        global callbacks called by gpsbabel main process              %%% *
*******************************************************************************/

static void
exif_rd_init(const char* fname)
{
  fin = gbfopen_be(fname, "rb", MYNAME);
  QUEUE_INIT(&exif_apps);
}

static void
exif_rd_deinit(void)
{
  exif_release_apps();
  gbfclose(fin);
}

static void
exif_read(void)
{
  gbuint16 soi;
  waypoint* wpt;

  soi = gbfgetuint16(fin);
  is_fatal(soi != 0xFFD8, MYNAME ": Unknown image file.");	/* only jpeg for now */

  exif_app = exif_load_apps();
  is_fatal(exif_app == NULL, MYNAME ": No EXIF header in source file \"%s\".", fin->name);

  exif_examine_app(exif_app);
  wpt = exif_waypt_from_exif_app(exif_app);
  if (wpt) {
    waypt_add(wpt);
  }
}

static void
exif_wr_init(const char* fname)
{
  gbuint16 soi;
  char* tmpname;

  exif_success = 0;
  exif_fout_name = xstrdup(fname);

  QUEUE_INIT(&exif_apps);

  fin = gbfopen_be(fname, "rb", MYNAME);
  is_fatal(fin->is_pipe, MYNAME ": Sorry, this format cannot be used with pipes!");

  soi = gbfgetuint16(fin);
  is_fatal(soi != 0xFFD8, MYNAME ": Unknown image file.");
  exif_app = exif_load_apps();
  is_fatal(exif_app == NULL, MYNAME ": No EXIF header found in source file \"%s\".", fin->name);
  exif_examine_app(exif_app);
  gbfclose(fin);

  exif_time_ref = exif_get_exif_time(exif_app);
  if (exif_time_ref == 0) {
    fatal(MYNAME ": No valid timestamp found in picture!\n");
  }

  xasprintf(&tmpname, "%s.jpg", fname);
  fout = gbfopen_be(tmpname, "wb", MYNAME);
  xfree(tmpname);
}

static void
exif_wr_deinit(void)
{
  char* tmpname;

  exif_release_apps();
  tmpname = xstrdup(fout->name);
  gbfclose(fout);

  if (exif_success) {
    if (*opt_overwrite == '1') {
      remove(exif_fout_name);
      rename(tmpname, exif_fout_name);
    }
  } else {
    remove(tmpname);
  }

  xfree(exif_fout_name);
  xfree(tmpname);
}

static void
exif_write(void)
{
  char alt_ref = 0;
  time_t frame;

  exif_wpt_ref = NULL;

  if (opt_name) {
    waypt_disp_all(exif_find_wpt_by_name);
    if (exif_wpt_ref == NULL) {
      route_disp_all(NULL, NULL, exif_find_wpt_by_name);
    }
    if (exif_wpt_ref == NULL) {
      track_disp_all(NULL, NULL, exif_find_wpt_by_name);
    }
    if (exif_wpt_ref == NULL) {
      warning(MYNAME ": No matching point with name \"%s\" found.\n", opt_name);
    }
  } else {
    char* str = exif_time_str(exif_time_ref);

    track_disp_all(NULL, NULL, exif_find_wpt_by_time);
    route_disp_all(NULL, NULL, exif_find_wpt_by_time);
    waypt_disp_all(exif_find_wpt_by_time);

    frame = atoi(opt_frame);

    if (exif_wpt_ref == NULL) {
      warning(MYNAME ": No point with a valid timestamp found.\n");
    } else if (abs(exif_time_ref - exif_wpt_ref->creation_time) > frame) {
      warning(MYNAME ": No matching point found for image date %s!\n", str);
      if (exif_wpt_ref != NULL) {
        char* str = exif_time_str(exif_wpt_ref->creation_time);
        warning(MYNAME ": Best is from %s, %d second(s) away.\n",
                str, abs(exif_time_ref - exif_wpt_ref->creation_time));
        xfree(str);
      }
      exif_wpt_ref = NULL;
    }
    xfree(str);
  }

  if (exif_wpt_ref != NULL) {
    const waypoint* wpt = exif_wpt_ref;

    exif_put_long(IFD0, IFD0_TAG_GPS_IFD_OFFS, 0, 0);
    exif_put_long(GPS_IFD, GPS_IFD_TAG_VERSION, 0, 2);
    exif_put_str(GPS_IFD, GPS_IFD_TAG_DATUM, "WGS-84");

    exif_put_str(GPS_IFD, GPS_IFD_TAG_LATREF, wpt->latitude < 0 ? "S" : "N");
    exif_put_coord(GPS_IFD, GPS_IFD_TAG_LAT, fabs(wpt->latitude));
    exif_put_str(GPS_IFD, GPS_IFD_TAG_LONREF, wpt->longitude < 0 ? "W" : "E");
    exif_put_coord(GPS_IFD, GPS_IFD_TAG_LON, fabs(wpt->longitude));

    if (wpt->altitude == unknown_alt) {
      exif_remove_tag(GPS_IFD, GPS_IFD_TAG_ALT);
      exif_remove_tag(GPS_IFD, GPS_IFD_TAG_ALTREF);
    } else {
      exif_put_value(GPS_IFD, GPS_IFD_TAG_ALTREF, EXIF_TYPE_BYTE, 1, 0, &alt_ref);
      exif_put_double(GPS_IFD, GPS_IFD_TAG_ALT, 0, wpt->altitude);
    }

    if (wpt->creation_time) {
      struct tm tm;
      char buf[32];

      tm = *gmtime(&wpt->creation_time);
      tm.tm_year += 1900;
      tm.tm_mon += 1;

      exif_put_double(GPS_IFD, GPS_IFD_TAG_TIMESTAMP, 0, tm.tm_hour);
      exif_put_double(GPS_IFD, GPS_IFD_TAG_TIMESTAMP, 1, tm.tm_min);
      exif_put_double(GPS_IFD, GPS_IFD_TAG_TIMESTAMP, 2, tm.tm_sec);

      snprintf(buf, sizeof(buf), "%04d:%02d:%02d", tm.tm_year, tm.tm_mon, tm.tm_mday);
      exif_put_str(GPS_IFD, GPS_IFD_TAG_DATESTAMP, buf);
    } else {
      exif_remove_tag(GPS_IFD, GPS_IFD_TAG_TIMESTAMP);
      exif_remove_tag(GPS_IFD, GPS_IFD_TAG_DATESTAMP);
    }

    if (wpt->sat > 0) {
      char buf[16];
      snprintf(buf, sizeof(buf), "%d", wpt->sat);
      exif_put_str(GPS_IFD, GPS_IFD_TAG_SAT, buf);
    } else {
      exif_remove_tag(GPS_IFD, GPS_IFD_TAG_SAT);
    }

    if (wpt->fix == fix_2d) {
      exif_put_str(GPS_IFD, GPS_IFD_TAG_MODE, "2");
    } else if (wpt->fix == fix_3d) {
      exif_put_str(GPS_IFD, GPS_IFD_TAG_MODE, "3");
    } else {
      exif_remove_tag(GPS_IFD, GPS_IFD_TAG_MODE);
    }

    if (wpt->hdop > 0) {
      exif_put_double(GPS_IFD, GPS_IFD_TAG_DOP, 0, wpt->hdop);
    } else {
      exif_remove_tag(GPS_IFD, GPS_IFD_TAG_DOP);
    }

    if WAYPT_HAS(wpt, speed) {
      exif_put_str(GPS_IFD, GPS_IFD_TAG_SPEEDREF, "K");
      exif_put_double(GPS_IFD, GPS_IFD_TAG_SPEED, 0, MPS_TO_KPH(wpt->speed));
    } else {
      exif_remove_tag(GPS_IFD, GPS_IFD_TAG_SPEEDREF);
      exif_remove_tag(GPS_IFD, GPS_IFD_TAG_SPEED);
    }

    exif_write_apps();	/* Success, write the new file */

    exif_success = 1;
  }

}

/**************************************************************************/

ff_vecs_t exif_vecs = {
  ff_type_file,
  {
    (ff_cap)(ff_cap_read | ff_cap_write)	/* waypoints */,
    ff_cap_none 			/* tracks */,
    ff_cap_none 			/* routes */
  },
  exif_rd_init,
  exif_wr_init,
  exif_rd_deinit,
  exif_wr_deinit,
  exif_read,
  exif_write,
  NULL,
  exif_args,
  CET_CHARSET_UTF8, 0
};

/**************************************************************************/
