/*\
 * $Id: ppmtobmp.c,v 1.9 1992/11/24 19:39:33 dws Exp dws $
 *
 * 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.
 *
 * $Log: ppmtobmp.c,v $
 * Revision 1.9  1992/11/24  19:39:33  dws
 * Added copyright.
 *
 * Revision 1.8  1992/11/17  02:16:52  dws
 * Moved length functions to bmp.h.
 *
 * Revision 1.7  1992/11/11  23:18:16  dws
 * Modified to adjust the bits per pixel to 1, 4, or 8.
 *
 * Revision 1.6  1992/11/11  22:43:39  dws
 * Commented out a superfluous message.
 *
 * Revision 1.5  1992/11/11  05:58:06  dws
 * First version that works.
 *
 * Revision 1.4  1992/11/11  03:40:32  dws
 * Moved calculation of bits per pixel to BMPEncode.
 *
 * Revision 1.3  1992/11/11  03:02:34  dws
 * Added BMPEncode function.
 *
 * Revision 1.2  1992/11/08  01:44:35  dws
 * Added option processing and reading of PPM file.
 *
 * Revision 1.1  1992/11/08  00:46:07  dws
 * Initial revision
\*/

#define _BSD_SOURCE 1    /* Make sure strdup() is in string.h */
#include <string.h>
#include "bmp.h"
#include "ppm.h"
#include "shhopt.h"
#include "bitio.h"

#define MAXCOLORS 256

enum colortype {TRUECOLOR, PALETTE};
/*
 * Utilities
 */

static char     er_write[] = "stdout: write error";

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 *fp, unsigned char v)
{
        if (putc(v, fp) == EOF)
        {
                pm_error(er_write);
        }
}

static void
PutShort(FILE *fp, short v)
{
        if (pm_writelittleshort(fp, v) == -1)
        {
                pm_error(er_write);
        }
}

static void
PutLong(fp, v)
        FILE           *fp;
        long            v;
{
        if (pm_writelittlelong(fp, v) == -1)
        {
                pm_error(er_write);
        }
}

/*
 * BMP writing
 */

/*
 * returns the number of bytes written, or -1 on error.
 */
static int
BMPwritefileheader(fp, class, bitcount, x, y)
        FILE           *fp;
        int             class;
        unsigned long   bitcount;
        unsigned long   x;
        unsigned long   y;
{
        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;
}

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

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

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

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

                PutLong(fp, 0);
                PutLong(fp, 0);
                PutLong(fp, 0);
                PutLong(fp, 0);
                PutLong(fp, 0);
                PutLong(fp, 0);


                break;
        case C_OS2:
                cbFix = 12;
                PutLong(fp, cbFix);

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

                break;
        default:
                pm_error(er_internal, "BMPwriteinfoheader");
        }

        return cbFix;
}

/*
 * returns the number of bytes written, or -1 on error.
 */
static int
BMPwritergb(FILE *fp, int class, pixval R, pixval G, pixval B)
{
        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;
}

/*
 * returns the number of bytes written, or -1 on error.
 */
static int
BMPwritecolormap(FILE *fp, const int class, const int bpp,
                 const int colors, 
                 const unsigned char R[], const unsigned char G[], 
                 const unsigned char B[])
{
        int             nbyte = 0;
        int             i;
        long            ncolors;

        for (i = 0; i < colors; i++)
        {
                nbyte += BMPwritergb(fp,class,R[i],G[i],B[i]);
        }

        ncolors = (1 << bpp);

        for (; i < ncolors; i++)
        {
                nbyte += BMPwritergb(fp,class,0,0,0);
        }

        return nbyte;
}

/*
 * returns the number of bytes written, or -1 on error.
 */
static int
BMPwriterow_palette(FILE *fp, const pixel * const row, 
                    const unsigned long cx, 
                    const unsigned short bpp, const colorhash_table cht)
{
        BITSTREAM       b;
        unsigned        nbyte = 0;
        int             rc;
        unsigned        x;

        if ((b = pm_bitinit(fp, "w")) == (BITSTREAM) 0)
        {
                return -1;
        }

        for (x = 0; x < cx; x++)
        {
            if ((rc = pm_bitwrite(b, bpp, 
                                  ppm_lookupcolor(cht, &row[x]))) == -1)
            {
                return -1;
            }
            nbyte += rc;
        }

        if ((rc = pm_bitfini(b)) == -1)
        {
                return -1;
        }
        nbyte += rc;

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

        return nbyte;
}

/*
 * returns the number of bytes written, or -1 on error.
 */
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++) {
        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;
}

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

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

        /*
         * 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)
                {
                        pm_error("row had bad number of bytes: %d"
                                 ,rc);
                }
                nbyte += rc;
        }

        return nbyte;
}

/*
 * Write a BMP file of the given class.
 *
 * Note that we must have 'colors' in order to know exactly how many
 * colors are in the R, G, B, arrays.  Entries beyond those in the
 * arrays are undefined.
 */
static void
BMPEncode(FILE *fp, const int class, const enum colortype colortype,
          const int bpp,
          const int x, const int y, 
          const pixel ** const pixels, const pixval maxval, const int colors, 
          const colorhash_table cht, 
          const unsigned char R[], const unsigned char G[], 
          const unsigned char B[])
{
        unsigned long   nbyte = 0;

        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 += BMPwritefileheader(fp, class, bpp, x, y);
        nbyte += BMPwriteinfoheader(fp, class, bpp, x, y);
        if (colortype == PALETTE)
            nbyte += BMPwritecolormap(fp, class, bpp, colors, R, G, B);

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

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



static void
analyze_colors(const pixel ** const pixels, 
               const int cols, const int rows, const pixval maxval, 
               int * const colors_p, 
               int * const minimum_bpp_p,
               colorhash_table *cht_p, 
               unsigned char Red[], 
               unsigned char Green[], 
               unsigned char Blue[]) {
/*----------------------------------------------------------------------------
  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 Red[], Green[], Blue[], and *cht_p.  Use only the first
  *colors_p entries of the Red[], etc. array.

  If there are too many colors for a palette, return *cht_p == NULL.

  Issue informational messages.

-----------------------------------------------------------------------------*/

    /* Figure out the colormap. */
    colorhist_vector chv;
    int i;

    pm_message("analyzing colors...");
    chv = ppm_computecolorhist((pixel**)pixels, cols, rows, MAXCOLORS, 
                               colors_p);
    if (chv == NULL) {
        pm_message("More than %d colors found", MAXCOLORS);
        *minimum_bpp_p = 24;
        *cht_p = NULL;
    } else {
        *minimum_bpp_p = pm_maxvaltobits(*colors_p-1);
        pm_message("%d colors found", *colors_p);
        /*
         * Now scale the maxval to 255 as required by BMP format.
         */
        for (i = 0; i < *colors_p; ++i)
        {
            Red[i] = (pixval) PPM_GETR(chv[i].color) * 255 / maxval;
            Green[i] = (pixval) PPM_GETG(chv[i].color) * 255 / maxval;
            Blue[i] = (pixval) PPM_GETB(chv[i].color) * 255 / maxval;
        }
    
        /* And make a hash table for fast lookup. */
        *cht_p = ppm_colorhisttocolorhash(chv, *colors_p);
        ppm_freecolorhist(chv);
    }
}



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

    if (!cmdline.bppSpec) {
        /* User has no preference as to bits per pixel.  Note that 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 (minimum_bpp == 1)
            *bits_per_pixel_p = 1;
        else if (minimum_bpp <= 4)
            *bits_per_pixel_p = 4;
        else if (minimum_bpp <= 8)
            *bits_per_pixel_p = 8;
        else
            *bits_per_pixel_p = 24;
    } 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;
    }
    if (*bits_per_pixel_p > 8) 
        *colortype_p = TRUECOLOR;
    else
        *colortype_p = PALETTE;
}



int
main(int argc, char **argv)
{
        FILE *ifp;

        int rows;
        int cols;
        int colors;
        pixval maxval;
        int minimum_bpp;
        int bits_per_pixel;
        enum colortype colortype;

        pixel** pixels;
        colorhash_table cht;
        /* 'Red', 'Green', and 'Blue' are the color map for the BMP file,
           with indices the same as in 'cht', above.
        */
        unsigned char Red[MAXCOLORS], Green[MAXCOLORS], Blue[MAXCOLORS];

        ppm_init(&argc, argv);

        parse_command_line(argc, argv, &cmdline);

        ifp = pm_openr(cmdline.input_filename);

        pixels = ppm_readppm(ifp, &cols, &rows, &maxval);

        pm_close(ifp);


        analyze_colors((const pixel**)pixels, cols, rows, maxval, 
                       &colors, &minimum_bpp, &cht, Red, Green, Blue);

        choose_colortype_bpp(cmdline, colors, minimum_bpp, &colortype, 
                             &bits_per_pixel);

        BMPEncode(stdout, cmdline.class, colortype, bits_per_pixel,
                  cols, rows, (const pixel**)pixels, maxval, colors, cht,
                  Red, Green, Blue);

        if (cht) 
            ppm_freecolorhash(cht);

        pm_close(stdout);

        exit(0);
}



