/*****************************************************************************
                                jpegtopnm
******************************************************************************
  This program is part of the Netpbm package.

  This program converts from the JFIF format, which is based on JPEG, to
  the fundamental ppm or pgm format (depending on whether the JFIF 
  image is gray scale or color).

  This program is by Bryan Henderson on 2000.03.20, but is derived
  with permission from the program djpeg, which is in the Independent
  Jpeg Group's JPEG library package.  Under the terms of that permission,
  redistribution of this software is restricted as described in the 
  file README.JPEG.

  Copyright (C) 1991-1998, Thomas G. Lane.

  TODO:

    See if additional decompressor options effects signficant speedup.
    grayscale output of color image, downscaling, color quantization, and
    dithering are possibilities.  Djpeg's man page says they make it faster.

  IMPLEMENTATION NOTE - PRECISION

    There are two versions of the JPEG library.  One handles only JPEG
    files with 8 bit samples; the other handles only 12 bit files.
    This program may be compiled and linked against either, and run
    dynamically linked to either at runtime independently.  It uses
    the precisions information from the file header.  Note that when
    the input has 12 bit precision, this program generates PPM files
    with two-byte samples, but when the input has 8 bit precision, it
    generates PPM files with one-byte samples.  One should use
    Ppmdepth to reduce precision to 8 bits if one-byte-sample output
    is essential.
    
*****************************************************************************/

#define _BSD_SOURCE 1
    /* Make sure strdup() is in string.h */

#include <ctype.h>		/* to declare isprint() */
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <jpeglib.h>
#include "pnm.h"
#include "shhopt.h"

#define EXIT_WARNING 2  /* Goes with EXIT_FAILURE, EXIT_SUCCESS in stdlib.h */

static const char * progname;	/* program name for error messages */

struct cmdline_info {
    /* All the information the user supplied in the command line,
       in a form easy for the program to use.
    */
    char *input_filename;
    int verbose;
    int nosmooth;
    J_DCT_METHOD dct_method;
    int grayscale;
    long int max_memory_to_use;
    unsigned int trace_level;
} cmdline;



static void 
interpret_maxmemory (const char * const maxmemory, 
                     long int * const max_memory_to_use_p) { 
/*----------------------------------------------------------------------------
   Interpret the "maxmemory" command line option.
-----------------------------------------------------------------------------*/
    long int lval;
    char ch;
    
    if (maxmemory == NULL) {
        *max_memory_to_use_p = -1;  /* unspecified */
    } else if (sscanf(maxmemory, "%ld%c", &lval, &ch) < 1) {
        pm_error("Invalid value for --maxmemory option: '%s'.", maxmemory);
    } else {
        if (ch == 'm' || ch == 'M') lval *= 1000L;
        *max_memory_to_use_p = lval * 1000L;
    }
}



static void
parse_command_line(const int argc, char ** argv,
                   struct cmdline_info *cmdline_p) {
/*----------------------------------------------------------------------------
   Note that many of the strings that this function returns in the
   *cmdline_p structure are actually in the supplied argv array.  And
   sometimes, one of these strings is actually just a suffix of an entry
   in argv!

   On the other hand, unlike other option processing functions, we do
   not change argv at all.
-----------------------------------------------------------------------------*/

    int i;  /* local loop variable */

    char *dctval;
    char *maxmemory;

    optStruct *option_def = malloc(100*sizeof(optStruct));
    /* Instructions to OptParseOptions on how to parse our options.
     */
    unsigned int option_def_index;

    int argc_parse;       /* argc, except we modify it as we parse */
    char ** const argv_parse = malloc(argc*sizeof(char *));
    /* argv, except we modify it as we parse */

    option_def_index = 0;   /* incremented by OPTENTRY */
    OPTENTRY(0, "verbose",     OPT_FLAG,   &cmdline_p->verbose,        0);
    OPTENTRY(0, "dct",         OPT_STRING, &dctval,                    0);
    OPTENTRY(0, "maxmemory",   OPT_STRING, &maxmemory,                 0); 
    OPTENTRY(0, "nosmooth",    OPT_FLAG,   &cmdline_p->nosmooth,       0);
    OPTENTRY(0, "tracelevel",  OPT_UINT,   &cmdline_p->trace_level,    0);

    /* Set the defaults */
    cmdline_p->verbose = FALSE;
    cmdline_p->nosmooth = FALSE;
    dctval = NULL;
    maxmemory = NULL;
    cmdline_p->trace_level = 0;

  /* Make private copy of arguments for optParseOptions to corrupt */
    argc_parse = argc;
    for (i=0; i < argc; i++) argv_parse[i] = argv[i];

    pm_optParseOptions(&argc_parse, argv_parse, option_def, 0);
    /* Uses and sets argc_parse, argv_parse and all of *cmdline_p. */

    if (argc_parse - 1 == 0)
        cmdline_p->input_filename = NULL;  /* he wants stdin */
    else if (argc_parse - 1 == 1)
        cmdline_p->input_filename = strdup(argv_parse[1]);
    else 
        pm_error("Too many arguments.  The only argument accepted\n"
                 "is the input file specificaton");

    if (dctval == NULL)
        cmdline_p->dct_method = JDCT_DEFAULT;
    else {
        if (strcmp(dctval, "int") == 0)
            cmdline_p->dct_method = JDCT_ISLOW;
        else if (strcmp(dctval, "fast") == 0)
            cmdline_p->dct_method = JDCT_IFAST;
        else if (strcmp(dctval, "float") == 0)
            cmdline_p->dct_method = JDCT_FLOAT;
        else pm_error("Invalid value for the --dct option: '%s'.", dctval);
    }

    interpret_maxmemory(maxmemory, &cmdline_p->max_memory_to_use);

    free(argv_parse);
}


/*
 * Marker processor for COM and interesting APPn markers.
 * This replaces the library's built-in processor, which just skips the marker.
 * We want to print out the marker as text, to the extent possible.
 * Note this code relies on a non-suspending data source.
 */

static unsigned int
jpeg_getc (j_decompress_ptr cinfo)
/* Read next byte */
{
  struct jpeg_source_mgr * datasrc = cinfo->src;

  if (datasrc->bytes_in_buffer == 0) {
      if (! (*datasrc->fill_input_buffer) (cinfo)) 
          pm_error("Can't suspend here.");
  }
  datasrc->bytes_in_buffer--;
  return GETJOCTET(*datasrc->next_input_byte++);
}


static boolean
print_text_marker (j_decompress_ptr cinfo)
{
  const boolean traceit = (cinfo->err->trace_level >= 1);
  INT32 length;
  unsigned int ch;
  unsigned int lastch = 0;

  length = jpeg_getc(cinfo) << 8;
  length += jpeg_getc(cinfo);
  length -= 2;			/* discount the length word itself */

  if (traceit) {
    if (cinfo->unread_marker == JPEG_COM)
      fprintf(stderr, "Comment, length %ld:\n", (long) length);
    else			/* assume it is an APPn otherwise */
      fprintf(stderr, "APP%d, length %ld:\n",
	      cinfo->unread_marker - JPEG_APP0, (long) length);
  }

  while (--length >= 0) {
    ch = jpeg_getc(cinfo);
    if (traceit) {
      /* Emit the character in a readable form.
       * Nonprintables are converted to \nnn form,
       * while \ is converted to \\.
       * Newlines in CR, CR/LF, or LF form will be printed as one newline.
       */
      if (ch == '\r') {
	fprintf(stderr, "\n");
      } else if (ch == '\n') {
	if (lastch != '\r')
	  fprintf(stderr, "\n");
      } else if (ch == '\\') {
	fprintf(stderr, "\\\\");
      } else if (isprint(ch)) {
	putc(ch, stderr);
      } else {
	fprintf(stderr, "\\%03o", ch);
      }
      lastch = ch;
    }
  }

  if (traceit)
    fprintf(stderr, "\n");

  return TRUE;
}




typedef struct rgb {unsigned int r; unsigned int g; unsigned int b;} rgb_type;


static rgb_type *
read_rgb(JSAMPLE *ptr, J_COLOR_SPACE color_space, const unsigned int maxval) {
/*----------------------------------------------------------------------------
  Return the RGB triple corresponding to the color of the JPEG pixel at
  'ptr', which is in color space 'color_space'.  

  Assume 'maxval' is the maximum sample value in the input pixel, and also
  use it for the maximum sample value in the return value.
-----------------------------------------------------------------------------*/
    static rgb_type rgb;  /* Our return value */

    switch (color_space) {
    case JCS_RGB: {
        rgb.r = GETJSAMPLE(*(ptr+0));
        rgb.g = GETJSAMPLE(*(ptr+1)); 
        rgb.b = GETJSAMPLE(*(ptr+2)); 
    }
        break;
    case JCS_CMYK: {
        const int c = GETJSAMPLE(*(ptr+0));
        const int m = GETJSAMPLE(*(ptr+1));
        const int y = GETJSAMPLE(*(ptr+2));
        const int k = GETJSAMPLE(*(ptr+3));

        rgb.r = ((maxval-k)*(maxval-c))/maxval;
        rgb.g = ((maxval-k)*(maxval-y))/maxval;
        rgb.b = ((maxval-k)*(maxval-m))/maxval;
    }
        break;
    default:
        pm_error("Internal error: unknown color space %d passed to "
                 "read_rgb().", (int) color_space);
    }
    return(&rgb);
}



/* pnmbuffer is declared global because it is really local to 
   copy_pixel_row().  But it would be impractical to allocate and free
   the storage with every call to copy_pixel_row() and improper to pass
   the pointer as input to copy_pixel_row(), since it isn't logically 
   a parameter of the operation.
   */
xel *pnmbuffer;      /* Output buffer.  Input to pnm_writepnmrow() */

static void
copy_pixel_row(const JSAMPROW jpegbuffer, const int width, 
               const unsigned int samples_per_pixel, 
               const J_COLOR_SPACE color_space,
               const unsigned int maxval,
               FILE * const output_file, const int output_type) {
  JSAMPLE *ptr;
  unsigned int output_cursor;     /* Cursor into output buffer 'pnmbuffer' */

  ptr = jpegbuffer;  /* Start at beginning of input row */

  for (output_cursor = 0; output_cursor < width; output_cursor++) {
      xel current_pixel;
      if (samples_per_pixel >= 3) {
          const rgb_type * const rgb_p = read_rgb(ptr, color_space, maxval);
          PPM_ASSIGN(current_pixel, rgb_p->r, rgb_p->g, rgb_p->b);
      } else {
          PNM_ASSIGN1(current_pixel, GETJSAMPLE(*ptr));
      }
      ptr += samples_per_pixel;  /* move to next pixel of input */
      pnmbuffer[output_cursor] = current_pixel;
  }
  pnm_writepnmrow(output_file, pnmbuffer, width,
                  maxval, output_type, FALSE);
}



static void
set_color_spaces(const J_COLOR_SPACE jpeg_color_space,
                 int * const output_type_p,
                 J_COLOR_SPACE * const out_color_space_p) {
/*----------------------------------------------------------------------------
   Decide what type of output (PPM or PGM) we shall generate and what 
   color space we must request from the JPEG decompressor, based on the
   color space of the input JPEG image.

   Write to stderr a message telling which type we picked.

   Exit the process with EXIT_FAILURE completion code and a message to
   stderr if the input color space is beyond our capability.
-----------------------------------------------------------------------------*/
    /* Note that the JPEG decompressor is not capable of translating
       CMYK or YCCK to RGB, but can translate YCCK to CMYK.
    */

    switch (jpeg_color_space) {
    case JCS_UNKNOWN:
        pm_error("Input JPEG image has 'unknown' color space "
                 "(JCS_UNKNOWN).\n"
                 "We cannot interpret this image.");
        break;
    case JCS_GRAYSCALE:
        *output_type_p = PGM_TYPE;
        *out_color_space_p = JCS_GRAYSCALE;
        break;
    case JCS_RGB:
        *output_type_p = PPM_TYPE;
        *out_color_space_p = JCS_RGB;
        break;
    case JCS_YCbCr:
        *output_type_p = PPM_TYPE;
        *out_color_space_p = JCS_RGB;
        break;
    case JCS_CMYK:
        *output_type_p = PPM_TYPE;
        *out_color_space_p = JCS_CMYK;
        break;
    case JCS_YCCK:
        *output_type_p = PPM_TYPE;
        *out_color_space_p = JCS_CMYK;
        break;
    }
    pm_message("WRITING %s FILE", 
               *output_type_p == PPM_TYPE ? "PPM" : "PGM");
}



static void
setup_jpeg_input(FILE *input_file, 
                 struct jpeg_decompress_struct * const cinfo_p,
                 struct jpeg_error_mgr * const jerr_p, 
                 const boolean verbose, 
                 const int trace_level, 
                 const J_DCT_METHOD dct_method, 
                 const int max_memory_to_use, 
                 const boolean nosmooth) {
/*----------------------------------------------------------------------------
   Open the input file, read the header, create decompressor object (and
   allocate memory for it), set up decompressor.
-----------------------------------------------------------------------------*/
    /* Initialize the JPEG decompression object with default error handling. */
    cinfo_p->err = jpeg_std_error(jerr_p);
    jpeg_create_decompress(cinfo_p);
    if (trace_level == 0 && verbose) cinfo_p->err->trace_level = 1;
    else cinfo_p->err->trace_level = trace_level;

    /* Insert custom marker processor for COM and APP12.
     * APP12 is used by some digital camera makers for textual info,
     * so we provide the ability to display it as text.
     * If you like, additional APPn marker types can be selected for display,
     * but don't try to override APP0 or APP14 this way (see libjpeg.doc).
     */
    jpeg_set_marker_processor(cinfo_p, JPEG_COM, print_text_marker);
    jpeg_set_marker_processor(cinfo_p, JPEG_APP0+12, print_text_marker);

    /* Specify data source for decompression */
    jpeg_stdio_src(cinfo_p, input_file);

    /* Read file header, set default decompression parameters */
    (void) jpeg_read_header(cinfo_p, TRUE);

    cinfo_p->dct_method = dct_method;
    if (max_memory_to_use != -1)
        cinfo_p->mem->max_memory_to_use = max_memory_to_use;
    if (nosmooth)
        cinfo_p->do_fancy_upsampling = FALSE;

}



static void 
cleanup(FILE* input_file, FILE* output_file, xel *pnmbuffer, 
        struct jpeg_decompress_struct *cinfo_p) {
/*----------------------------------------------------------------------------
   Release memory, close out everything.
-----------------------------------------------------------------------------*/
    int rc;

    pnm_freerow(pnmbuffer);

    rc = fclose(output_file);
    if (rc == EOF) 
        pm_error("Error writing output file.  Errno = %s (%d).",
                 strerror(errno), errno);
    
    /* Finish decompression and release decompressor memory. */
    (void) jpeg_finish_decompress(cinfo_p);
    jpeg_destroy_decompress(cinfo_p);

    /* Close files, if we opened them */
    if (input_file != stdin) fclose(input_file);

    /* Program may have exited with non-zero completion code vie
       various function calls above.  
    */
}



int
main(int argc, char **argv) {
  struct jpeg_decompress_struct cinfo;
  struct jpeg_error_mgr jerr;
  FILE * input_file;
  FILE * output_file;
  int output_type;
    /* The type of output file, PGM or PPM.  Value is either PPM_TYPE
       or PGM_TYPE, which conveniently also pass as format values
       PPM_FORMAT and PGM_FORMAT.
    */
  JSAMPROW jpegbuffer;  /* Input buffer.  Filled by jpeg_scanlines() */
  unsigned int maxval;  
    /* The maximum value of a sample (color component), both in the input
       and the output.
       */

  pnm_init(&argc, argv);
  progname = argv[0];

  parse_command_line(argc, argv, &cmdline);

  /* Open the input file */
  if (cmdline.input_filename == NULL) 
      input_file = stdin;
  else {
      if ((input_file = fopen(cmdline.input_filename, "rb")) == NULL) 
          pm_error("Can't open %s.  Errno=%s(%d).", 
                   cmdline.input_filename, strerror(errno), errno);
      free(cmdline.input_filename);
  }

  output_file = stdout;

  setup_jpeg_input(input_file, &cinfo, &jerr, cmdline.verbose, 
                   cmdline.trace_level, cmdline.dct_method, 
                   cmdline.max_memory_to_use, cmdline.nosmooth);
                   
  set_color_spaces(cinfo.jpeg_color_space, &output_type, 
                   &cinfo.out_color_space);
  
  maxval = (1 << cinfo.data_precision) - 1;

  if (cmdline.verbose) {
      pm_message("Input image data precision = %d bits", cinfo.data_precision);
      pm_message("Output file will have format %c%c "
                 "with max sample value of %d.", 
                 (char) (output_type/256), (char) (output_type % 256),
                 maxval);
  }  
  /* Calculate output image dimensions so we can allocate space */
  jpeg_calc_output_dimensions(&cinfo);

  jpegbuffer = ((*cinfo.mem->alloc_sarray)
                ((j_common_ptr) &cinfo, JPOOL_IMAGE,
                 cinfo.output_width * cinfo.output_components, (JDIMENSION) 1)
               )[0];

  /* Start decompressor */
  jpeg_start_decompress(&cinfo);

  /* Write pnm output header */
  pnm_writepnminit(output_file, cinfo.output_width, cinfo.output_height,
                   maxval, output_type, FALSE);

  pnmbuffer = pnm_allocrow(cinfo.output_width);

  /* Process data */
  while (cinfo.output_scanline < cinfo.output_height) {
    jpeg_read_scanlines(&cinfo, &jpegbuffer, 1);
    copy_pixel_row(jpegbuffer, cinfo.output_width, cinfo.out_color_components,
                   cinfo.out_color_space, maxval, output_file, output_type);
  }

  cleanup(input_file, output_file, pnmbuffer, &cinfo); 
  
  exit(jerr.num_warnings > 0 ? EXIT_WARNING : EXIT_SUCCESS);

}





