/* jpgcom.c:  print any comments present in the specified JPEG image files
 *
 * This program is a heavily-stripped version of the "rdjpgcom" program
 * distributed with version 9 of the Indendent JPEG Group's software.  The
 * original version (along with a copy of the Indendent JPEG Group's README
 * file) can be found in the directory orig/.
 *
 * Copyright (C) 1994-1997, Thomas G. Lane.
 * Modified 2009 by Bill Allombert, Guido Vollbeding.
 * This file is part of the Independent JPEG Group's software.
 * For conditions of distribution and use, see the accompanying README file.
 *
 * This file contains a very simple stand-alone application that displays
 * the text in COM (comment) markers in a JFIF file.
 * This may be useful as an example of the minimum logic needed to parse
 * JPEG markers.
 *
 * Modifications Copyright (C) 2004-2026 by Brian Lindholm.  All rights
 * reserved except as specified above. */

#include <config.h>

#include <ctype.h>    /* to declare isupper(), tolower() */
#include <limits.h>   /* to determine PATH_MAX */
#ifdef HAVE_LOCALE_H
# include <locale.h>  /* Bill Allombert: use locale for isprint */
#endif
#ifdef HAVE_STDIO_H
# include <stdio.h>   /* other I/O functions */
#endif
#ifdef HAVE_STDLIB_H
# include <stdlib.h>  /* other I/O functions */
#endif
#ifdef HAVE_STRING_H
# include <string.h>  /* other I/O functions */
#endif
#ifdef HAVE_FCNTL_H
# include <fcntl.h>   /* to declare posix_fadvise() */
#endif

#ifdef HAVE_UNISTD_H
# include <unistd.h>
# define OPTEND -1
#else
# define OPTEND EOF
#endif
#ifdef HAVE_GETOPT_H
# include <getopt.h>
#endif

#ifdef __MINGW32__
extern int getopt(int argc, char * const *argv, const char *optstring);
extern char *optarg;
extern int optind;
#endif

#ifndef PATH_MAX
# define PATH_MAX 256
#endif

/* These macros are used to read the input file.
 * To reuse this code in another application, you might need to change these. */
static FILE * infile;  /* input JPEG file */
#define NEXTBYTE()  getc(infile)  /* Return next input byte, or EOF if no more */
#define ERREXIT(msg)  { fprintf(stderr, "jpgcom error: %s\n", msg); return(EOF); }  /* Error handler */

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

  c = NEXTBYTE();
  if (c == EOF) ERREXIT("Premature EOF in JPEG file");
  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) ERREXIT("Premature EOF in JPEG file");
  c2 = NEXTBYTE();
  if (c2 == EOF) ERREXIT("Premature EOF in JPEG file");
  return(((unsigned int) c1) << 8) + ((unsigned int) c2);
}

/* 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_APP0  0xE0  /* Application-specific marker, type N */
#define M_APP12 0xEC  /* (we don't bother to list all 16 APPn's) */
#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.
 * 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();
  if (c == EOF) return(EOF);
  while (c != 0xFF) {
    discarded_bytes++;
    c = read_1_byte();
    if (c == EOF) return(EOF);
  }
  /* 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();
    if (c == EOF) return(EOF);
  } while (c == 0xFF);
  if (discarded_bytes != 0) fprintf(stderr, "jpgcom warning: garbage data found in JPEG file\n");
  return(c);
}

/* 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... */
static int first_marker(void)
{
  int c1, c2;

  c1 = NEXTBYTE();
  if (c1 == EOF) ERREXIT("Not a JPEG file");
  c2 = NEXTBYTE();
  if (c2 == EOF) ERREXIT("Not a JPEG file");
  if (c1 != 0xFF || c2 != M_SOI) ERREXIT("Not a JPEG file");
  return(c2);
}

/* 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 int skip_variable(void)
/* Skip over an unknown or uninteresting variable-length marker */
{
  unsigned int length;
  int rc;

  /* Get the marker parameter length count */
  length = read_2_bytes();
  /* Length includes itself, so must be at least 2 */
  if (length < 2) ERREXIT("Erroneous JPEG marker length");
  length -= 2;
  /* Skip over the remaining bytes */
  rc = fseek(infile, (long) length, 1);
  if (rc < 0) ERREXIT("Erroneous JPEG marker length");
  return(0);
}

/* Process a COM marker.
 * We want to print out the marker contents as legible text;
 * we must guard against non-text junk and varying newline representations. */
static int process_COM(int raw)
{
  unsigned int length;
  int ch;

  /* Bill Allombert: set locale properly for isprint */
#ifdef HAVE_LOCALE_H
  setlocale(LC_CTYPE, "");
#endif
  /* Get the marker parameter length count */
  length = read_2_bytes();
  /* Length includes itself, so must be at least 2 */
  if (length < 2) ERREXIT("Erroneous JPEG marker length");
  length -= 2;
  fprintf(stdout, "\t");
  while (length > 0) {
    ch = read_1_byte();
    if (raw)
      putc(ch, stdout);
      /* Emit the character in a readable form.  Nonprintables are converted to hexidecimal form */
    else if (isprint(ch))
      putc(ch, stdout);
    else if (ch != 0)
      fprintf(stdout, "[%02x]", (unsigned int) ch);
    length--;
  }
  /* Bill Allombert: revert to C locale */
#ifdef HAVE_LOCALE_H
  setlocale(LC_CTYPE, "C");
#endif
  return(0);
}

/* Parse the marker stream until SOS or EOI is seen; display any COM markers.
 * While the companion program wrjpgcom will always insert COM markers before
 * SOFn, other implementations might not, so we scan to SOS before stopping. */
static int scan_JPEG_header(int raw)
{
  int marker;
  int rc = 0;

  /* Expect SOI at start of file */
  if (first_marker() != M_SOI) ERREXIT("Expected SOI marker first");
  /* Scan miscellaneous markers until we reach SOS. */
  for (;;) {
    marker = next_marker();
    switch (marker) {
      case M_SOS: return(marker);  /* stop before hitting compressed data */
      case M_EOI: return(marker);  /* in case it's a tables-only JPEG stream */
      case M_COM:
        rc = process_COM(raw);
        if (rc == EOF) return(EOF);
        break;
      case M_APP12:
        fprintf(stdout, "APP12: ");
        rc = process_COM(raw);
        if (rc == EOF) return(EOF);
        break;
      default:  /* Anything else just gets skipped */
        rc = skip_variable();  /* we assume it has a parameter count */
        if (rc == EOF) return(EOF);
        break;
    }
  }  /* end loop */
  return(0);
}

/* A simple help function. */
static void help(FILE *where)
{
  fprintf(where, "jpgcom " PACKAGE_VERSION "\n"
    "usage: jpgcom [-f file_list] [-h(elp)] [-p(ipe)] [-r(aw)] infile...\n");
}

/* Print the filename along with any comments. */
static void print_com(char *filename, int raw)
{
  if ((infile = fopen(filename, "rb")) == NULL)
    fprintf(stderr, "jpgcom error: can't open %s\n", filename);
  else {
#ifdef HAVE_POSIX_FADVISE
    posix_fadvise(fileno(infile), 0, 0, POSIX_FADV_RANDOM);
#endif
    fprintf(stdout, "%s", filename);
    scan_JPEG_header(raw);
    fprintf(stdout, "\n");
    (void) fclose(infile);
  }
}

/* The main program. */
int main(int argc, char **argv)
{
  FILE *infile;
  char filename[PATH_MAX], *listname, *newline, *rc;
  int argn, opt, raw, use_file, use_pipe;

  /* parse options */
  listname = "";
  use_file = 0;
  use_pipe = 0;
  raw = 0;
  while ((opt = getopt(argc, argv, "f:hpr")) != OPTEND) switch (opt) {
    case 'f':
      use_file = 1;
      listname = optarg;
      break;
    case 'h':
      help(stdout);
      return(0);
    case 'p':
      use_pipe = 1;
      break;
    case 'r':
      raw = 1;
      break;
    case '?':
      help(stderr);
      return(1);
  }
  if ((optind == argc) && (use_file == 0) && (use_pipe == 0)) {
    help(stdout);
    return(0);
  }
  /* process files in listed in file specified by -f option */
  if (use_file) {
    infile = fopen(listname, "r");
    if (infile == NULL)
      fprintf(stderr, "jpgcom error: can't open %s!\n", listname);
    else {
      while (!feof(infile)) {
        rc = fgets(filename, PATH_MAX - 1, infile);
        if (rc != NULL) {
          newline = strchr(filename, '\n');
          if (newline != NULL) *newline = '\0';
          if (strlen(filename) != 0) print_com(filename, raw);
        }
      }
      fclose(infile);
    }
  }
  /* process files listed on stdin (i.e., the -p option) */
  if (use_pipe) while (!feof(stdin)) {
    rc = fgets(filename, PATH_MAX - 1, stdin);
    if (rc != NULL) {
      newline = strchr(filename, '\n');
      if (newline != NULL) *newline = '\0';
      if (strlen(filename) != 0) print_com(filename, raw);
    }
  }
  /* process files given in the argument list */
  for (argn = 1; argn < argc; argn++) print_com(argv[argn], raw);
  /* All done. */
  return(0);
}
