/*
 * ppmtobmp.c - Converts from a PPM file to a Microsoft Windows or OS/2
 * .BMP file.
 *
 * Copyright (C) 1992 by David W. Sanderson.
 *
 * Permission to use, copy, modify, and distribute this software and its
 * documentation for any purpose and without fee is hereby granted,
 * provided that the above copyright notice appear in all copies and
 * that both that copyright notice and this permission notice appear
 * in supporting documentation.  This software is provided "as is"
 * without express or implied warranty.
 *
 */

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

#include <assert.h>
#include <string.h>
#include "bmp.h"
#include "ppm.h"
#include "shhopt.h"
#include "bitio.h"

#define MAXCOLORS 256

enum colortype {TRUECOLOR, PALETTE};

typedef struct {
/*----------------------------------------------------------------------------
   A color map for a BMP file.
-----------------------------------------------------------------------------*/
    unsigned int count;
        /* Number of colors in the map.  The first 'count' elements of these
           arrays are defined; all others are not.
        */
    colorhash_table cht;

    /* Indices in the following arrays are the same as in 'cht', above. */
    unsigned char red[MAXCOLORS];
    unsigned char grn[MAXCOLORS];
    unsigned char blu[MAXCOLORS];
} colorMap;



static void
freeColorMap(const colorMap * const colorMapP) {

    if (colorMapP->cht)
        ppm_freecolorhash(colorMapP->cht);
}



static 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 class;  /* C_WIN or C_OS2 */
    unsigned int bppSpec;
    unsigned int bpp;
} cmdline;


static void
parse_command_line(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!
-----------------------------------------------------------------------------*/
    optEntry *option_def = malloc(100*sizeof(optEntry));
        /* Instructions to OptParseOptions3 on how to parse our options.
         */
    optStruct3 opt;

    unsigned int windowsSpec, os2Spec;

    unsigned int option_def_index;

    option_def_index = 0;   /* incremented by OPTENTRY */
    OPTENT3('w', "windows",   OPT_FLAG, NULL, &windowsSpec,            0);
    OPTENT3('o', "os2",       OPT_FLAG, NULL, &os2Spec,                0);
    OPTENT3(0,   "bpp",       OPT_UINT, &cmdline_p->bpp, 
            &cmdline_p->bppSpec,      0);

    opt.opt_table = option_def;
    opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
    opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */

    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);

    if (windowsSpec && os2Spec) 
        pm_error("Can't specify both -windows and -os2 options.");
    else if (windowsSpec) 
        cmdline_p->class = C_WIN;
    else if (os2Spec)
        cmdline_p->class = C_OS2;
    else 
        cmdline_p->class = C_WIN;


    if (cmdline_p->bppSpec) {
        if (cmdline_p->bpp != 1 && cmdline_p->bpp != 4 && 
            cmdline_p->bpp != 8 && cmdline_p->bpp != 24)
        pm_error("Invalid -bpp value specified: %u.  The only values valid\n"
                 "in the BMP format are 1, 4, 8, and 24 bits per pixel",
                 cmdline_p->bpp);
    }

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

}



static void
PutByte(FILE * const fp, unsigned char const v) {
    if (putc(v, fp) == EOF) 
        pm_error("Write of a byte to a file failed.");

    /* Note:  a Solaris/SPARC user reported on 2003.09.29 that the above
       putc() returned EOF when a former version of this code declared
       v as "char" instead of "unsigned char".  This was apparently due
       to a bug in his C library that caused 255 to look like -1 at some
       critical internal point.
    */
}



static void
PutShort(FILE * const fp, short const v) {
    if (pm_writelittleshort(fp, v) == -1) 
        pm_error("Write of a halfword to a file failed.");
}



static void
PutLong(FILE * const fp, long const v) {
    if (pm_writelittlelong(fp, v) == -1)
        pm_error("Write of a word to a file failed.");
}



/*
 * BMP writing
 */

static int
BMPwritefileheader(FILE *        const fp, 
                   int           const class, 
                   unsigned long const bitcount, 
                   unsigned long const x, 
                   unsigned long const y) {
/*----------------------------------------------------------------------------
  Return the number of bytes written, or -1 on error.
-----------------------------------------------------------------------------*/
    PutByte(fp, 'B');
    PutByte(fp, 'M');

    /* cbSize */
    PutLong(fp, BMPlenfile(class, bitcount, -1, x, y));
    
    /* xHotSpot */
    PutShort(fp, 0);
    
    /* yHotSpot */
    PutShort(fp, 0);
    
    /* offBits */
    PutLong(fp, BMPoffbits(class, bitcount, -1));
    
    return 14;
}



static int
BMPwriteinfoheader(FILE *        const fp, 
                   int           const class, 
                   unsigned long const bitcount, 
                   unsigned long const x, 
                   unsigned long const y) {
/*----------------------------------------------------------------------------
  Return the number of bytes written, or -1 on error.
----------------------------------------------------------------------------*/
    long cbFix;

    switch (class) {
    case C_WIN: {
        cbFix = 40;
        PutLong(fp, cbFix);

        PutLong(fp, x);         /* cx */
        PutLong(fp, y);         /* cy */
        PutShort(fp, 1);        /* cPlanes */
        PutShort(fp, bitcount); /* cBitCount */

        /*
         * We've written 16 bytes so far, need to write 24 more
         * for the required total of 40.
         */

        PutLong(fp, 0);   /* Compression */
        PutLong(fp, 0);   /* ImageSize */
        PutLong(fp, 0);   /* XpixelsPerMeter */
        PutLong(fp, 0);   /* YpixelsPerMeter */
        PutLong(fp, 0);   /* ColorsUsed */
        PutLong(fp, 0);   /* ColorsImportant */
    }
    break;
    case C_OS2: {
        cbFix = 12;
        PutLong(fp, cbFix);

        PutShort(fp, x);        /* cx */
        PutShort(fp, y);        /* cy */
        PutShort(fp, 1);        /* cPlanes */
        PutShort(fp, bitcount); /* cBitCount */
    }
    break;
    default:
        pm_error(er_internal, "BMPwriteinfoheader");
    }

    return cbFix;
}



static int
BMPwritergb(FILE * const fp, 
            int    const class, 
            pixval const R, 
            pixval const G, 
            pixval const B) {
/*----------------------------------------------------------------------------
  Return the number of bytes written, or -1 on error.
-----------------------------------------------------------------------------*/
    switch (class) {
    case C_WIN:
        PutByte(fp, B);
        PutByte(fp, G);
        PutByte(fp, R);
        PutByte(fp, 0);
        return 4;
    case C_OS2:
        PutByte(fp, B);
        PutByte(fp, G);
        PutByte(fp, R);
        return 3;
    default:
        pm_error(er_internal, "BMPwritergb");
    }
    return -1;
}



static int
BMPwritecolormap(FILE *           const ifP, 
                 int              const class, 
                 int              const bpp,
                 const colorMap * const colorMapP) {
/*----------------------------------------------------------------------------
  Return the number of bytes written, or -1 on error.
-----------------------------------------------------------------------------*/
    long const ncolors = (1 << bpp);

    unsigned int  nbyte;
    unsigned int  i;

    nbyte = 0;
    for (i = 0; i < colorMapP->count; ++i)
        nbyte += BMPwritergb(ifP, class,
                             colorMapP->red[i],
                             colorMapP->grn[i],
                             colorMapP->blu[i]);

    for (; i < ncolors; ++i)
        nbyte += BMPwritergb(ifP, class, 0, 0, 0);

    return nbyte;
}



static int
BMPwriterow_palette(FILE *          const fp, 
                    const pixel *   const row, 
                    unsigned long   const cx, 
                    unsigned short  const bpp, 
                    colorhash_table const cht) {
/*----------------------------------------------------------------------------
  Return the number of bytes written, or -1 on error.
-----------------------------------------------------------------------------*/
    BITSTREAM    b;
    int retval;
    
    b = pm_bitinit(fp, "w");
    if (b == NULL)
        retval = -1;
    else {
        unsigned int nbyte;
        unsigned int x;
        bool         error;
        
        nbyte = 0;      /* initial value */
        error = FALSE;  /* initial value */
        
        for (x = 0; x < cx && !error; ++x) {
            int rc;
            rc = pm_bitwrite(b, bpp, ppm_lookupcolor(cht, &row[x]));
            if (rc == -1)
                error = TRUE;
            else
                nbyte += rc;
        }
        if (error)
            retval = -1;
        else {
            int rc;

            rc = pm_bitfini(b);
            if (rc == -1)
                retval = -1;
            else {
                nbyte += rc;
                
                /* Make sure we write a multiple of 4 bytes.  */
                while (nbyte % 4 != 0) {
                    PutByte(fp, 0);
                    ++nbyte;
                }
                retval = nbyte;
            }
        }
    }
    return retval;
}



static int
BMPwriterow_truecolor(FILE *        const fp, 
                      const pixel * const row, 
                      unsigned long const cols,
                      pixval        const maxval) {
/*----------------------------------------------------------------------------
  Write a row of a truecolor BMP image to the file 'fp'.  The row is 
  'row', which is 'cols' columns long.

  Return the number of bytes written.

  On error, issue error message and exit program.
-----------------------------------------------------------------------------*/
    /* This works only for 24 bits per pixel.  To implement this for the
       general case (which is only hypothetical -- this program doesn't
       write any truecolor images except 24 bit and apparently no one
       else does either), you would move this function into 
       BMPwriterow_palette, which writes arbitrary bit strings.  But
       that would be a lot slower and less robust.
    */

    int nbyte;  /* Number of bytes we have written to file so far */
    int col;  
        
    nbyte = 0;  /* initial value */
    for (col = 0; col < cols; col++) {
        /* We scale to the BMP maxval, which is always 255. */
        PutByte(fp, PPM_GETB(row[col]) * 255 / maxval);
        PutByte(fp, PPM_GETG(row[col]) * 255 / maxval);
        PutByte(fp, PPM_GETR(row[col]) * 255 / maxval);
        nbyte += 3;
    }

    /*
     * Make sure we write a multiple of 4 bytes.
     */
    while (nbyte % 4) {
        PutByte(fp, 0);
        nbyte++;
    }
    
    return nbyte;
}



static int
BMPwritebits(FILE *          const fp, 
             unsigned long   const cx, 
             unsigned long   const cy, 
             enum colortype  const colortype,
             unsigned short  const cBitCount, 
             const pixel **  const pixels, 
             pixval          const maxval,
             colorhash_table const cht) {
/*----------------------------------------------------------------------------
  Return the number of bytes written, or -1 on error.
-----------------------------------------------------------------------------*/
    int  nbyte;
    long y;

    if (cBitCount > 24)
        pm_error("cannot handle cBitCount: %d", cBitCount);

    nbyte = 0;  /* initial value */

    /* The picture is stored bottom line first, top line last */

    for (y = cy - 1; y >= 0; --y) {
        int rc;
        if (colortype == PALETTE)
            rc = BMPwriterow_palette(fp, pixels[y], cx, 
                                     cBitCount, cht);
        else 
            rc = BMPwriterow_truecolor(fp, pixels[y], cx, maxval);

        if (rc == -1)
            pm_error("couldn't write row %ld", y);
        if (rc % 4 != 0)
            pm_error("row had bad number of bytes: %d", rc);
        nbyte += rc;
    }

    return nbyte;
}



static void
BMPEncode(FILE *           const ifP, 
          int              const class, 
          enum colortype   const colortype,
          int              const bpp,
          int              const x, 
          int              const y, 
          const pixel **   const pixels, 
          pixval           const maxval,
          const colorMap * const colorMapP) {
/*----------------------------------------------------------------------------
  Write a BMP file of the given class.
-----------------------------------------------------------------------------*/
    unsigned long nbyte;

    if (colortype == PALETTE)
        pm_message("Writing %d bits per pixel with a color palette", bpp);
    else
        pm_message("Writing %d bits per pixel truecolor (no palette)", bpp);

    nbyte = 0;  /* initial value */
    nbyte += BMPwritefileheader(ifP, class, bpp, x, y);
    nbyte += BMPwriteinfoheader(ifP, class, bpp, x, y);
    if (colortype == PALETTE)
        nbyte += BMPwritecolormap(ifP, class, bpp, colorMapP);

    if (nbyte != (BMPlenfileheader(class)
                  + BMPleninfoheader(class)
                  + BMPlencolormap(class, bpp, 0)))
        pm_error(er_internal, "BMPEncode 1");

    nbyte += BMPwritebits(ifP, x, y, colortype, bpp, pixels, maxval,
                          colorMapP->cht);
    if (nbyte != BMPlenfile(class, bpp, -1, x, y))
        pm_error(er_internal, "BMPEncode 2");
}



static void
makeBilevelColorMap(colorMap * const colorMapP) {

    colorMapP->count  = 2;
    colorMapP->cht    = NULL;
    colorMapP->red[0] = 0;
    colorMapP->grn[0] = 0;
    colorMapP->blu[0] = 0;
    colorMapP->red[1] = 255;
    colorMapP->grn[1] = 255;
    colorMapP->blu[1] = 255;
}



static void
BMPEncodePBM(FILE *           const ifP, 
             int              const class, 
             int              const cols, 
             int              const rows, 
             unsigned char ** const bitrow) {
/*----------------------------------------------------------------------------
  Write a bi-level BMP file of the given class.
-----------------------------------------------------------------------------*/
    /* Note:
       Only PBM input uses this routine.  Color images represented by 1 bpp via
       color palette use the general BMPEncode().
    */
    unsigned int const adjustedCols = (cols + 31) / 32 * 32;
    unsigned int const packedBytes  = adjustedCols / 8;

    unsigned long nbyte;
    colorMap bilevelColorMap;
    unsigned int row;
    
    /* colortype == PALETTE */
    pm_message("Writing 1 bit per pixel with a black-white palette");

    nbyte = 0;  /* initial value */
    nbyte += BMPwritefileheader(ifP, class, 1, cols, rows);
    nbyte += BMPwriteinfoheader(ifP, class, 1, cols, rows);

    makeBilevelColorMap(&bilevelColorMap);

    nbyte += BMPwritecolormap(ifP, class, 1, &bilevelColorMap);

    if (nbyte != (BMPlenfileheader(class)
                  + BMPleninfoheader(class)
                  + BMPlencolormap(class, 1, 0)))
        pm_error(er_internal, "BMPEncodePBM 1");
   
    for (row = 0; row < rows; ++row){
        size_t bytesWritten;

        bytesWritten = fwrite(bitrow[row], 1, packedBytes, ifP);
        if (bytesWritten != packedBytes){
            if (feof(ifP))
                pm_error("End of file writing row %u of BMP raster.", row);
            else 
                pm_error("Error writing BMP raster.  Errno=%d (%s)",
                         errno, strerror(errno));
        }  else
            nbyte += bytesWritten;
    }

    if (nbyte != BMPlenfile(class, 1, -1, cols, rows))
        pm_error(er_internal, "BMPEncodePBM 2");
}



static void
analyze_colors(const pixel **    const pixels, 
               int               const cols, 
               int               const rows, 
               pixval            const maxval, 
               int *             const minimum_bpp_p,
               colorMap *        const colorMapP) {
/*----------------------------------------------------------------------------
  Look at the colors in the image 'pixels' and compute values to use in
  representing those colors in a BMP image.  

  First of all, count the distinct colors.  Return as *minimum_bpp_p
  the minimum number of bits per pixel it will take to represent all
  the colors in BMP format.

  If there are few enough colors to represent with a palette, also
  return the number of colors as *colors_p and a suitable palette
  (colormap) and a hash table in which to look up indexes into that
  palette as *colorMapP.  Use only the first *colors_p entries of the
  map.

  If there are too many colors for a palette, return colorMapP->cht
  == NULL.

  Issue informational messages.
-----------------------------------------------------------------------------*/
    /* Figure out the colormap. */
    colorhist_vector chv;
    int colorCount;

    pm_message("analyzing colors...");
    chv = ppm_computecolorhist((pixel**)pixels, cols, rows, MAXCOLORS, 
                               &colorCount);
    colorMapP->count = colorCount;
    if (chv == NULL) {
        pm_message("More than %u colors found", MAXCOLORS);
        *minimum_bpp_p = 24;
        colorMapP->cht = NULL;
    } else {
        unsigned int const minbits = pm_maxvaltobits(colorMapP->count - 1);

        unsigned int i;

        pm_message("%u colors found", colorMapP->count);

        /* Only 1, 4, 8, and 24 are defined in the BMP spec we
           implement and other bpp's have in fact been seen to confuse
           viewers.  There is an extended BMP format that has 16 bpp
           too, but this program doesn't know how to generate that
           (see Bmptopnm.c, though).  
        */
        if (minbits == 1)
            *minimum_bpp_p = 1;
        else if (minbits <= 4)
            *minimum_bpp_p = 4;
        else if (minbits <= 8)
            *minimum_bpp_p = 8;
        else
            *minimum_bpp_p = 24;

        /*
         * Now scale the maxval to 255 as required by BMP format.
         */
        for (i = 0; i < colorMapP->count; ++i) {
            colorMapP->red[i] = (pixval) PPM_GETR(chv[i].color) * 255 / maxval;
            colorMapP->grn[i] = (pixval) PPM_GETG(chv[i].color) * 255 / maxval;
            colorMapP->blu[i] = (pixval) PPM_GETB(chv[i].color) * 255 / maxval;
        }
    
        /* And make a hash table for fast lookup. */
        colorMapP->cht = ppm_colorhisttocolorhash(chv, colorMapP->count);
        ppm_freecolorhist(chv);
    }
}



static void
choose_colortype_bpp(struct cmdline_info const cmdline,
                     unsigned int        const colors, 
                     unsigned int        const minimum_bpp,
                     enum colortype *    const colortype_p, 
                     unsigned int *      const bits_per_pixel_p) {

    if (!cmdline.bppSpec) {
        /* User has no preference as to bits per pixel.  Choose the
           smallest number possible for this image.
        */
        *bits_per_pixel_p = minimum_bpp;
    } else {
        if (cmdline.bpp < minimum_bpp)
            pm_error("There are too many colors in the image to "
                     "represent in the\n"
                     "number of bits per pixel you requested: %d.\n"
                     "You may use Ppmquant to reduce the number of "
                     "colors in the image.",
                     cmdline.bpp);
        else
            *bits_per_pixel_p = cmdline.bpp;
    }

    assert(*bits_per_pixel_p == 1 || 
           *bits_per_pixel_p == 4 || 
           *bits_per_pixel_p == 8 || 
           *bits_per_pixel_p == 24);

    if (*bits_per_pixel_p > 8) 
        *colortype_p = TRUECOLOR;
    else {
        *colortype_p = PALETTE;
    }
}



static void
doPbm(FILE *       const ifP,
      unsigned int const cols,
      unsigned int const rows,
      int          const format,
      int          const class,
      FILE *       const ofP) {
    
    /*  In the PBM case the raster is read directly from the input by 
        pbm_readpbmrow_packed.  The raster format is almost identical,
        except that BMP specifies rows to be zero-filled to 32 bit borders 
        and that in BMP the bottom row comes first in order.
    */

    int const CHARBITS = (sizeof(unsigned char)*8); 
    int const colChars = pbm_packed_bytes(cols);
    int const adjustedCols = (cols+31) /32 * 32;
    int const packedBytes  =  adjustedCols /8;

    unsigned char ** bitrow;    
    unsigned int row;

    bitrow = pbm_allocarray_packed(adjustedCols, rows);

    for (row = 0; row < rows; ++row) {
        unsigned char * const thisRow = bitrow[rows - row - 1];

        /* Clear end of each row */
        thisRow[packedBytes-1] = 0x00;
        thisRow[packedBytes-2] = 0x00;
        thisRow[packedBytes-3] = 0x00;
        thisRow[packedBytes-4] = 0x00;
        
        pbm_readpbmrow_packed(ifP, thisRow, cols, format);

        {
            unsigned int i;
            for (i = 0; i < colChars; ++i) 
                thisRow[i] = ~thisRow[i]; /* flip all pixels */
        }
        /* This may seem unnecessary, because the color palette 
           (RGB[] in BMPEncodePBM) can be inverted for the same effect.
           However we take this precaution, for there is indication that
           some BMP viewers may get confused with that.
        */

        if (cols % 8 >0) {
            /* adjust final partial byte */
            thisRow[colChars-1] >>= CHARBITS - cols % CHARBITS;
            thisRow[colChars-1] <<= CHARBITS - cols % CHARBITS;
        }
    }

    BMPEncodePBM(ofP, class, cols, rows, bitrow);
}            



static void
doPgmPpm(FILE * const ifP,
         unsigned int const cols,
         unsigned int const rows,
         pixval       const maxval,
         int          const ppmFormat,
         int          const class,
         FILE *       const ofP) {

    /* PGM and PPM.  The input image is read into a PPM array, scanned
       for color analysis and converted to a BMP raster.
       Logic works for PBM.
    */
    int minimumBpp;
    unsigned int bitsPerPixel;
    enum colortype colortype;
    unsigned int row;
    
    pixel ** pixels;
    colorMap colorMap;
    
    pixels = ppm_allocarray(cols, rows);
    
    for (row = 0; row < rows; ++row)
        ppm_readppmrow(ifP, pixels[row], cols, maxval, ppmFormat);
    
    analyze_colors((const pixel**)pixels, cols, rows, maxval, 
                   &minimumBpp, &colorMap);
    
    choose_colortype_bpp(cmdline, colorMap.count, minimumBpp, &colortype, 
                         &bitsPerPixel);
    
    BMPEncode(stdout, class, colortype, bitsPerPixel,
              cols, rows, (const pixel**)pixels, maxval, &colorMap);
    
    freeColorMap(&colorMap);
}



int
main(int argc, char **argv) {

    FILE * ifP;
    int rows;
    int cols;
    pixval maxval;
    int ppmFormat;

    ppm_init(&argc, argv);

    parse_command_line(argc, argv, &cmdline);

    ifP = pm_openr(cmdline.input_filename);
    
    ppm_readppminit(ifP, &cols, &rows, &maxval, &ppmFormat);
    
    if (PPM_FORMAT_TYPE(ppmFormat) == PBM_TYPE)
        doPbm(ifP, cols, rows, ppmFormat, cmdline.class, stdout);
    else
        doPgmPpm(ifP, cols, rows, maxval, ppmFormat, cmdline.class, stdout);

    pm_close(ifP);
    pm_close(stdout);

    return 0;
}
