/** @file jpegcom.c
 * Read or write comments in JFIF or EXIF JPEG files
 * @author Marko Mäkelä <msmakela@nic.funet.fi>
 */

/* Copyright © 2003 Marko Mäkelä.

   This file is part of JPEGCOM, a program for reading and writing
   comments in digital photographs.

   JPEGCOM 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, or (at your option)
   any later version.

   JPEGCOM 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.

   The GNU General Public License is often shipped with GNU software, and
   is generally kept in a file called COPYING or LICENSE.  If you do not
   have a copy of the license, write to the Free Software Foundation,
   59 Temple Place, Suite 330, Boston, MA 02111 USA. */

#if defined WIN32 || defined __WIN32
# undef __STRICT_ANSI__
# include <sys/types.h>
# include <sys/utime.h>
#else
# include <sys/types.h>
# include <utime.h>
#endif
#include <sys/stat.h>
#include <unistd.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <locale.h>

/** Read 16 bits from file, most significant byte first
 * @param file	the file
 * @return	the data read
 */
static unsigned
read_word (FILE* file)
{
  unsigned word;
  word = (unsigned) getc (file);
  word <<= 8;
  word |= (unsigned) getc (file);
  return word;
}

/** Find the next JPEG marker code
 * @param file	the file
 * @return	the marker code found, or EOF
 */
static int
read_marker (FILE* file)
{
  int c;
  /* find the next 0xff */
  do {
    c = getc (file);
    if (c == EOF)
      return c;
  }
  while (c != 0xff);
  /* find the marker, ignoring subsequent 0xff bytes */
  do {
    c = getc (file);
    if (c == EOF)
      return c;
  }
  while (c == 0xff);
  return c;
}

/** Determine whether a character needs to be escaped
 * @param c	the character
 */
static int
is_printable (int c)
{
  return c != '"' && c != '\\' && (c == '\n' || c == '\t' || isprint (c));
}

/** Read image comments
 * @param in	the image file
 * @param com	the output file for the comments
 * @return	NULL on success, error message on failure
 */
static const char*
readcom (FILE* in,
	 FILE* com)
{
  if (read_word (in) != 0xffd8)
    return "not a JPEG image file";
  /* scan markers until start of scan (compressed data) */
  for (;;) {
    unsigned length;
    int marker = read_marker (in);
    if (marker == EOF) {
    eof:
      return "premature end of file";
    }
    else if (marker == 0xd9/* end of image */ ||
	     marker == 0xda/* start of scan (compressed data) */)
      return 0;
    length = read_word (in);
    if (length < 2)
      return "erroneous JPEG marker length";
    length -= 2;
    if (marker == 0xfe) {
      /* comment */
      if ('"' != putc ('"', com)) {
      output:
	return "write error";
      }
      while (length--) {
	int c = getc (in);
	if (c == EOF) {
	  fputs ("\"\n", com);
	  goto eof;
	}
	else if (is_printable (c)
		 ? (c != putc (c, com))
		 : (4 != fprintf (com, "\\%03o", c)))
	  goto output;
      }
      if (EOF == fputs ("\"\n", com))
	goto output;      
    }
    else
      while (length--)
	if (getc (in) == EOF)
	  goto eof;
  }
}

/** Write 16 bits to file, most significant byte first
 * @param word	the data
 * @param file	the file
 * @return	zero on error
 */
static int
write_word (unsigned word,
	    FILE* file)
{
  return
    putc (word >> 8, file) == EOF ||
    putc (word & 0xff, file) == EOF;
}

/** Read a double quoted string
 * @param file	the input file
 * @param s	the input string (optional, for reallocated storage)
 * @param size	(output) length of the string (optional)
 * @return	the string, or NULL on failure
 */
static char*
read_name (FILE* file,
	   char* s,
	   unsigned* size)
{
  /** allocated length of the string */
  unsigned len;
  /** reallocated string */
  char* t;
  /** character read from the file */
  int c;
  /* skip whitespace and read the leading quote */
  while ((c = getc (file)) != '"') {
    if (c == EOF) {
      fputs ("unexpected end of file while looking for string\n", stderr);
      return 0;
    }
    if (!isspace (c)) {
      fputs ("unexpected character while looking for string\n", stderr);
      return 0;
    }
  }
  if (!(t = realloc (s, len = 1))) {
  memory:
    fputs ("out of memory for the string\n", stderr);
    free (s);
    return 0;
  }
  s = t;
  /* read the string */
  while ((c = getc (file)) != EOF) {
  again:
    if (c == '"') {
      s[--len] = 0;
      if (size)
	*size = len;
      return s;
    }
    if (!(len & (len + 1))) {
      if (!(t = realloc (s, (len + 1) << 1)))
	goto memory;
      s = t;
    }
    if (c == '\\') {
      if ((c = getc (file)) == EOF)
	break;
      else if (c >= '0' && c <= '7') {
	/* got octal digit, read up to 3 digits */
	int outch = c - '0';
	if (EOF == (c = getc (file)))
	  break;
	if (c < '0' || c > '7')
	  goto done;
	outch <<= 3; outch |= c - '0';
	if (EOF == (c = getc (file)))
	  break;
	if (c < '0' || c > '7')
	  goto done;
	outch <<= 3; outch |= c - '0';
      done:
	s[len++ - 1] = outch;
	if (c < '0' || c > '7')
	  goto again;
	continue;
      }
      /* fall through */
    }
    s[len++ - 1] = c;
  }
  fputs ("unexpected end of file while reading string\n", stderr);
  free (s);
  return 0;
}

/** Write image comments
 * @param in	the input image file
 * @param out	the output image file
 * @param com	the input file for the comments
 * @return	NULL on success, error message on failure
 */
static const char*
writecom (FILE* in,
	  FILE* out,
	  FILE* com)
{
  if (read_word (in) != 0xffd8)
    return "not a JPEG image file";
  if (write_word (0xffd8, out)) {
  output:
    return "write error";
  }
  /* scan markers until start of scan (compressed data) */
  for (;;) {
    unsigned length;
    int marker = read_marker (in);
    if (marker == EOF) {
    eof:
      return "premature end of JPEG file";
    }
    else if (marker == 0xd9/* end of image */ ||
	     marker == 0xda/* start of scan (compressed data) */) {
      /* write the comments and copy the rest of in to out */
      /** comment string */
      char* comment = 0;
      for (;;) { /* write the comments */
	int c = getc (com);
	if (c == EOF)
	  break;
	if (isspace (c))
	  continue;
	ungetc (c, com);
	if (c != '"' || !(comment = read_name (com, comment, &length)))
	  break;
	if (write_word (0xfffe, out) ||
	    write_word (length + 2, out) ||
	    length != fwrite (comment, 1, length, out))
	  goto output;
      }
      free (comment);

      /* copy the rest of the input image to output */
      if (write_word (0xff00 | marker, out))
	goto output;
      for (;;) {
	static char buf[8192];
	size_t len = fread (buf, 1, sizeof buf, in);
	if (len != fwrite (buf, 1, len, out))
	  goto output;
	if (len < sizeof buf)
	  break;
      }
      return 0;
    }

    /* process the marker */
    length = read_word (in);
    if (length < 2)
      return "erroneous JPEG marker length";
    if (marker == 0xfe) {
      /* ignore comments in the input file */
      length -= 2;
      while (length--)
	if (getc (in) == EOF)
	  goto eof;
      continue;
    }
    /* copy other than comment markers */
    if (write_word (0xff00 | marker, out) ||
	write_word (length, out))
      goto output;
    length -= 2;
    while (length--) {
      int c = getc (in);
      if (c == EOF)
	goto eof;
      if (c != putc (c, out))
	goto output;
    }
  }
}

/** Write a name, escaped in C style
 * @param name	the name (NUL terminated)
 * @param file	the output file
 * @return	zero on success
 */
static int
write_name (const char* name,
	    FILE* file)
{
  for (; *name; *name++) {
    int c = (unsigned char) *name;
    if (is_printable (c)
	? (c != putc (c, file))
	: (4 != fprintf (file, "\\%03o", c)))
      return EOF;
  }
  return 0;
}

/** Copy the timestamp of a file to another
 * @param name	the file name whose timestamp is to be rewritten
 * @param ref	the reference file
 * @return	zero if successful
 */
static int
touch_file (const char* name,
	    const char* ref)
{
  int status;
  struct stat statbuf;
  if ((status = stat (ref, &statbuf))) {
    fputs (ref, stderr);
    perror (": stat");
  }
  else {
    struct utimbuf utim;
    utim.actime = utim.modtime = statbuf.st_mtime;
    if ((status = utime (name, &utim))) {
      fputs (ref, stderr);
      perror (": utime");
    }
  }
  return status;
}

/** Rename a file
 * @param old	the old file name
 * @param new	the new file name
 * @return	zero if successful
 */
static int
rename_file (const char* old,
	     const char* new)
{
  int status = rename (old, new);
  if (status) {
    fputs (old, stderr);
    fputs (" -> ", stderr);
    fputs (new, stderr);
    perror (": rename");
  }
  return status;
}

/** Remove a hard link to a file
 * @param name	the file name to be removed
 * @return	zero if successful
 */
static int
remove_file (const char* name)
{
  int status = unlink (name);
  if (status) {
    fputs (name, stderr);
    perror (": unlink");
  }
  return status;
}

/** Main program
 * @param argc	argument count
 * @param argv	argument vector
 * @return	zero on success
 */
int
main (int argc, char** argv)
{
  setlocale (LC_ALL, "");

  if (argc > 1) {
    /* read the comments of the listed files */
    int i;
    if (fputs ("#This is a batch file for"
	       " commenting, renaming and copying image files.\n"
	       "#Comment lines start with '#'.\n"
	       "#Typical usage:"
	       " jpegcom *.jpg > file; $EDITOR file; jpegcom < file\n"
	       "#Files can be renamed or copied by editing this file.\n"
	       "#Comments are written after the second name.\n"
	       "#The characters '\"' and '\\'"
	       " must be written '\\\"' and '\\\\'.\n"
	       "#Example:\n"
	       "#\trename \"IMG_0001.JPG\"\t\"potato.jpg\"\n"
	       "#\t\"A \\\"beautiful\\\"\n"
	       "#\tflower\"\n"
	       "#\t\"Solanum tuberosum\"\n"
	       "#\tcopy \"IMG_0002.JPG\"\t\"onion.jpg\"\n"
	       "#\t\"Allium cepa\"\n", stdout) == EOF) {
    output:
      perror ("write(stdout)");
      return 1;
    }
    for (i = 1; i < argc; i++) {
      FILE* f = fopen (argv[i], "rb");
      const char* s;
      if (!f) {
	fputs (argv[i], stderr);
	perror (": open (reading)");
	return 1;
      }
      if (fputs ("copy \"", stdout) == EOF ||
	  write_name (argv[i], stdout) ||
	  fputs ("\"\t\"", stdout) == EOF ||
	  write_name (argv[i], stdout) ||
	  fputs ("\"\n", stdout) == EOF) {
	fclose (f);
	goto output;
      }
      if ((s = readcom (f, stdout))) {
	fputs (argv[i], stderr);
	fputs (": ", stderr);
	fputs (s, stderr);
	putc ('\n', stderr);
      }
      fclose (f);
    }
  }
  else {
    /* process the listed files */
    int c;
    while ((c = getc (stdin)) != EOF) {
      if (c == '#') {
	/* skip until end of line */
	for (;;) {
	  if ((c = getc (stdin)) == EOF)
	    return 0;
	  if (c == '\n')
	    break;
	}
      }
      else if (!isspace (c)) {
	/* look for "rename" or "copy" */
	static const char *rename = "ename", *copy = "opy";
	const char* r;
	int do_remove = c == 'r';
	char* name1; char* name2 = 0;
	if (c == 'r')
	  r = rename;
	else if (c == 'c')
	  r = copy;
	else {
	  fputs ("syntax error: expected \"rename\" or \"copy\", got", stderr);
	  goto syntax;
	}
	do {
	  if ((c = getc (stdin)) != *r) {
	    fputs ("syntax error: expected \"", stderr);
	    fputs (r, stderr);
	    fputs ("\", got ", stderr);
	  syntax:
	    if (c == EOF)
	      fputs ("EOF", stderr);
	    else
	      putc (c, stderr);
	    putc ('\n', stderr);
	    return 2;
	  }
	}
	while (*++r);
	if ((name1 = read_name (stdin, 0, 0)) &&
	    (name2 = read_name (stdin, 0, 0))) {
	  int same = !strcmp (name1, name2);
	  FILE* in;
	  FILE* out;
	  if (!(in = fopen (name1, "rb"))) {
	    fputs (name1, stderr);
	    perror (": open (reading)");
	    free (name1), free (name2);
	    return 1;
	  }
	  if (same) {
	    /* same name => change the last character of the name to '0' */
	    char* s;
	    for (s = name2; *s; s++);
	    s--;
	    *s = '0';
	  }
	  if (!(out = fopen (name2, "wb"))) {
	    fputs (name2, stderr);
	    perror (": open (writing)");
	    fclose (in);
	    free (name1), free (name2);
	    return 1;
	  }
	  if ((r = writecom (in, out, stdin))) {
	    fputs (name1, stderr);
	    fputs (": ", stderr);
	    fputs (r, stderr);
	    putc ('\n', stderr);
	    fclose (in), fclose (out);
	    free (name1), free (name2);
	    return 1;
	  }
	  fclose (in), fclose (out);
	  /* copy the timestamp and rename or remove the file */
	  if (touch_file (name2, name1) ||
	      (same
	       ? rename_file (name2, name1)
	       : (do_remove && remove_file (name1)))) {
	    free (name1), free (name2);
	    return 1;
	  }
	  free (name1), free (name2);
	}
	else {
	  free (name1), free (name2);
	  return 2;
	}
      }
    }
  }

  return 0;
}
