/*
 * setcomment.c
 *
 * Copyright 2002 Bryce Nesbitt
 *
 * Based on wrjpgcom.c, Copyright (C) 1994-1997, Thomas G. Lane.
 * Part of the Independent JPEG Group's software release 6b of 27-Mar-1998
 *
 * This file contains a very simple stand-alone application that inserts
 * user-supplied text as a COM (comment) marker in a JPEG/JFIF file.
 * This may be useful as an example of the minimum logic needed to parse
 * JPEG markers.
 *
 * There can be an arbitrary number of COM blocks in each jpeg file, with
 * up to 64K of data each.  We, however, write just one COM and blow away
 * the rest.
 */

#undef  STANDALONE_COMPILE

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>

extern int safe_copy_and_modify( const char * original_filename, const char * comment );
    
#ifdef DONT_USE_B_MODE          /* define mode parameters for fopen() */
#define READ_BINARY     "r"
#define WRITE_BINARY    "w"
#else
#ifdef VMS                      /* VMS is very nonstandard */
#define READ_BINARY     "rb", "ctx=stm"
#define WRITE_BINARY    "wb", "ctx=stm"
#else                           /* standard ANSI-compliant case */
#define READ_BINARY     "rb"
#define WRITE_BINARY    "wb"
#endif
#endif

#define WARNING_GARBAGE     1   /* Original file had some unspecified content */
#define ERROR_NOT_A_JPEG    5   /* Original file not a proper jpeg (must be 1st) */
#define ERROR_TEMP_FILE     6   /* Problem writing temporay file */
#define ERROR_SCREWUP       7   /* Original file is now damaged.  Ooops. */
#define ERROR_PREMATURE_EOF 8   /* Unexpected end of file */
#define ERROR_BAD_MARKER    9   /* Marker with illegal length */
#define ERROR_MARKER_ORDER  10  /* File seems to be mixed up */

static int          global_error;   /* global error flag.  Once set, we're dead. */

/****************************************************************************/
/*
 * These macros are used to read the input file and write the output file.
 * To reuse this code in another application, you might need to change these.
 */
static FILE * infile;           /* input JPEG file */

/* Return next input byte, or EOF if no more */
#define NEXTBYTE()  getc(infile)

static FILE * outfile;          /* output JPEG file */

/* Emit an output byte */
#define PUTBYTE(x)  putc((x), outfile)


/****************************************************************************/
/* Read one byte, testing for EOF */
static int
read_1_byte (void)
{
  int c;

  c = NEXTBYTE();
  if (c == EOF) {
    global_error = ERROR_PREMATURE_EOF;
    }
  return c;
}

/* Read 2 bytes, convert to unsigned int */
/* All 2-byte quantities in JPEG markers are MSB first */
static unsigned int
read_2_bytes (void)
{
  int c1, c2;

  c1 = NEXTBYTE();
  if (c1 == EOF)
    global_error = ERROR_PREMATURE_EOF;
  c2 = NEXTBYTE();
  if (c2 == EOF)
    global_error = ERROR_PREMATURE_EOF;
  return (((unsigned int) c1) << 8) + ((unsigned int) c2);
}


/****************************************************************************/
/* Routines to write data to output file */
static void
write_1_byte (int c)
{
  PUTBYTE(c);
}

static void
write_2_bytes (unsigned int val)
{
  PUTBYTE((val >> 8) & 0xFF);
  PUTBYTE(val & 0xFF);
}

static void
write_marker (int marker)
{
  PUTBYTE(0xFF);
  PUTBYTE(marker);
}

static void
copy_rest_of_file (void)
{
  int c;

  while ((c = NEXTBYTE()) != EOF)
    PUTBYTE(c);
}


/****************************************************************************/
/*
 * JPEG markers consist of one or more 0xFF bytes, followed by a marker
 * code byte (which is not an FF).  Here are the marker codes of interest
 * in this program.  (See jdmarker.c for a more complete list.)
 */

#define M_SOF0  0xC0            /* Start Of Frame N */
#define M_SOF1  0xC1            /* N indicates which compression process */
#define M_SOF2  0xC2            /* Only SOF0-SOF2 are now in common use */
#define M_SOF3  0xC3
#define M_SOF5  0xC5            /* NB: codes C4 and CC are NOT SOF markers */
#define M_SOF6  0xC6
#define M_SOF7  0xC7
#define M_SOF9  0xC9
#define M_SOF10 0xCA
#define M_SOF11 0xCB
#define M_SOF13 0xCD
#define M_SOF14 0xCE
#define M_SOF15 0xCF
#define M_SOI   0xD8            /* Start Of Image (beginning of datastream) */
#define M_EOI   0xD9            /* End Of Image (end of datastream) */
#define M_SOS   0xDA            /* Start Of Scan (begins compressed data) */
#define M_COM   0xFE            /* COMment */


/*
 * Find the next JPEG marker and return its marker code.
 * We expect at least one FF byte, possibly more if the compressor used FFs
 * to pad the file.  (Padding FFs will NOT be replicated in the output file.)
 * There could also be non-FF garbage between markers.  The treatment of such
 * garbage is unspecified; we choose to skip over it but emit a warning msg.
 * NB: this routine must not be used after seeing SOS marker, since it will
 * not deal correctly with FF/00 sequences in the compressed image data...
 */
static int
next_marker (void)
{
  int c;
  int discarded_bytes = 0;

  /* Find 0xFF byte; count and skip any non-FFs. */
  c = read_1_byte();
  while (c != 0xFF) {
    discarded_bytes++;
    c = read_1_byte();
  }
  /* Get marker code byte, swallowing any duplicate FF bytes.  Extra FFs
   * are legal as pad bytes, so don't count them in discarded_bytes.
   */
  do {
    c = read_1_byte();
  } while (c == 0xFF);

  if (discarded_bytes != 0) {
    global_error = WARNING_GARBAGE;
  }

  return c;
}


/*
 * Most types of marker are followed by a variable-length parameter segment.
 * This routine skips over the parameters for any marker we don't otherwise
 * want to process.
 * Note that we MUST skip the parameter segment explicitly in order not to
 * be fooled by 0xFF bytes that might appear within the parameter segment;
 * such bytes do NOT introduce new markers.
 */
static void
copy_variable (void)
/* Copy an unknown or uninteresting variable-length marker */
{
  unsigned int length;

  /* Get the marker parameter length count */
  length = read_2_bytes();
  write_2_bytes(length);
  /* Length includes itself, so must be at least 2 */
  if (length < 2) {
    global_error = ERROR_BAD_MARKER;
    length = 2;
    }
  length -= 2;
  /* Skip over the remaining bytes */
  while (length > 0) {
    write_1_byte(read_1_byte());
    length--;
  }
}

static void
skip_variable (void)
/* Skip over an unknown or uninteresting variable-length marker */
{
  unsigned int length;

  /* Get the marker parameter length count */
  length = read_2_bytes();
  /* Length includes itself, so must be at least 2 */
  if (length < 2) {
    global_error = ERROR_BAD_MARKER;
    length = 2;
    }
  length -= 2;
  /* Skip over the remaining bytes */
  while (length > 0) {
    (void) read_1_byte();
    length--;
  }
}


static int
scan_JPEG_header (int keep_COM)
/*
 * Parse & copy the marker stream until SOFn or EOI is seen;
 * copy data to output, but discard COM markers unless keep_COM is true.
 */
{
  int c1, c2;
  int marker;

  /*
   * Read the initial marker, which should be SOI.
   * For a JFIF file, the first two bytes of the file should be literally
   * 0xFF M_SOI.  To be more general, we could use next_marker, but if the
   * input file weren't actually JPEG at all, next_marker might read the whole
   * file and then return a misleading error message...
   */
  c1 = NEXTBYTE();
  c2 = NEXTBYTE();
  if (c1 != 0xFF || c2 != M_SOI) {
    global_error = ERROR_NOT_A_JPEG;
    return EOF;
    }

  write_marker(M_SOI);

  /* Scan miscellaneous markers until we reach SOFn. */
  for (;;) {
    marker = next_marker();
    switch (marker) {
      /* Note that marker codes 0xC4, 0xC8, 0xCC are not, and must not be,
       * treated as SOFn.  C4 in particular is actually DHT.
       */
      case M_SOF0:                /* Baseline */
      case M_SOF1:                /* Extended sequential, Huffman */
      case M_SOF2:                /* Progressive, Huffman */
      case M_SOF3:                /* Lossless, Huffman */
      case M_SOF5:                /* Differential sequential, Huffman */
      case M_SOF6:                /* Differential progressive, Huffman */
      case M_SOF7:                /* Differential lossless, Huffman */
      case M_SOF9:                /* Extended sequential, arithmetic */
      case M_SOF10:               /* Progressive, arithmetic */
      case M_SOF11:               /* Lossless, arithmetic */
      case M_SOF13:               /* Differential sequential, arithmetic */
      case M_SOF14:               /* Differential progressive, arithmetic */
      case M_SOF15:               /* Differential lossless, arithmetic */
      return marker;

    case M_SOS:                 /* should not see compressed data before SOF */
      global_error = ERROR_MARKER_ORDER;
      break;

    case M_EOI:                 /* in case it's a tables-only JPEG stream */
      return marker;

    case M_COM:                 /* Existing COM: conditionally discard */
      if (keep_COM) {
        write_marker(marker);
        copy_variable();
      } else {
        skip_variable();
      }
      break;

    default:                    /* Anything else just gets copied */
      write_marker(marker);
      copy_variable();          /* we assume it has a parameter count... */
      break;
    }
  } /* end loop */
}


/****************************************************************************/
/*
    Verify we know how to set the comment on this type of file.

    TODO: The actual check!  This should verify
    the image size promised in the headers matches the file,
    and that all markers are properly formatted.
*/
static int validate_image_file( const char * filename )
{
int status = 1;
int c1, c2;

  if ( (infile = fopen(filename, READ_BINARY)) ) {
    c1 = NEXTBYTE();
    c2 = NEXTBYTE();
    if (c1 != 0xFF || c2 != M_SOI) 
      status = ERROR_NOT_A_JPEG;
    else 
      status = 0;
    fclose( infile ); 
  }
  return( status );
}


/****************************************************************************/
/*
    Modify the file in place, but be paranoid and safe about it.
    It's worth a few extra CPU cycles to make sure we never
    destory an original image:
    1) Validate the input file.
    2) Open a temporary file.
    3) Copy the data, writing a new comment block.
    4) Validate the temporary file.
    5) Move the temporary file over the original.

    To be even more paranoid & safe we could:
    5) Rename the original to a different temporary name.
    6) Rename the temporary to the original.
    7) Delete the original.
*/
extern int safe_copy_and_modify( const char * original_filename, const char * comment )
{
char *       temp_filename;
int          temp_filename_length;
int          comment_length  = 0;
int          marker;
int          i;
struct stat  statbuf;

  global_error  = 0;

  /*
   * Make sure we're dealing with a proper input file.  Safety first!
   */
  if( validate_image_file( original_filename ) ) {
    fprintf(stderr, "error validating original file %s\n", original_filename);
    return(ERROR_NOT_A_JPEG);
    }

  /* Get a unique temporary file in the same directory.  Hopefully 
   * if things go wrong, this file will still be left for recovery purposes.
   *
   * NB: I hate these stupid string functions in C... the buffer length is too
   * hard to manage...
   */
  outfile = NULL;
  temp_filename_length = strlen( original_filename) + 4;
  temp_filename = (char *)calloc( temp_filename_length, 1 );
  for( i=0; i<10; i++ ) {
    snprintf( temp_filename, temp_filename_length, "%s%d", original_filename, i );
    if( stat( temp_filename, &statbuf ) ) {
        outfile = fopen(temp_filename, WRITE_BINARY);
        break;
        }
    }
  if( !outfile ) {
    fprintf(stderr, "failed opening temporary file %s\n", temp_filename);
    return(ERROR_TEMP_FILE);
    }


  /*
   * Let's rock and roll! 
   */
  if ((infile = fopen(original_filename, READ_BINARY)) == NULL) {
    fprintf(stderr, "can't open input file %s\n", original_filename);
    return(ERROR_NOT_A_JPEG);
    }
  /* Copy JPEG headers until SOFn marker;
   * we will insert the new comment marker just before SOFn.
   * This (a) causes the new comment to appear after, rather than before,
   * existing comments; and (b) ensures that comments come after any JFIF
   * or JFXX markers, as required by the JFIF specification.
   *
   * :TODO: Verify this is robust and will handle new compression types
   * added to the specification.
   */
  marker = scan_JPEG_header(0);
  /* Insert the new COM marker, but only if nonempty text has been supplied */
  if (comment) {
    comment_length = strlen( comment );
    }
  if (comment_length > 0) {
    write_marker(M_COM);
    write_2_bytes(comment_length + 2);
    while (comment_length > 0) {
      write_1_byte(*comment++);
      comment_length--;
      }
    }
  /* Duplicate the remainder of the source file.
   * Note that any COM markers occuring after SOF will not be touched.
   *
   * :TODO: Discard COM markers occuring after SOF
   */
  write_marker(marker);
  copy_rest_of_file();
  fclose( infile );
  fsync( fileno( outfile) );    /* Make sure its really on disk first */
  if ( fclose( outfile ) ) {
    fprintf(stderr, "error in temporary file %s\n", temp_filename);
    return(ERROR_TEMP_FILE);
    }


  /*
   * Make sure we did it right .  Safety first!
   */
  if( validate_image_file( temp_filename ) ) {
    fprintf(stderr, "error in temporary file %s\n", temp_filename);
    return(ERROR_TEMP_FILE);
    }

  if( global_error >= ERROR_NOT_A_JPEG ) {
    fprintf(stderr, "error %d processing %s\n", global_error, original_filename);
    return(ERROR_NOT_A_JPEG);
    }

  if( rename( temp_filename, original_filename ) ) {
    fprintf(stderr, "error renaming %s to %s\n", temp_filename, original_filename);
    return(ERROR_TEMP_FILE);
    }

  return(0);
}


#ifdef  STANDALONE_COMPILE
int
main (int argc, char **argv)
{
    char * progname;       /* program name for error messages */
    char * filename;
    char * comment;
    FILE * fp;
    int    error;

    /* Process command line arguments... */
    progname = argv[0];
    if (progname == NULL || progname[0] == 0)
        progname = "writejpgcomment";  /* in case C library doesn't provide it */
    if( argc != 3) {
        fprintf(stderr, "Usage: %s <filename> \"<comment>\"\nOverwrites the comment in a image file with the given comment.\n", progname);
        return(5);
        }
    filename = argv[1];
    comment  = argv[2];


    /* Check if file is readable... */
    if ((fp = fopen(filename, READ_BINARY)) == NULL) {
      fprintf(stderr, "Error: Can't open file %s\n", filename);
      fclose(fp);
      return(5);
    }
    fclose(fp);

    /* Check if we really have a commentable image file here... */
    if( validate_image_file( filename ) ) {
      fprintf(stderr, "Error: file %s is not of a supported type\n", filename);
      return(5);
      }

    /* Lets do it... modify the comment in place */
    if ((error = safe_copy_and_modify( filename, comment ) )) {
        fprintf(stderr, "Error: %d setting jpg comment\n", error);
        return(10);
    }


    /* TODO: Read comment back out of jpg and display it */
    return( 0 );
}
#endif
