/*****************************************************************************
                                    bmptopnm.c
******************************************************************************
 
 Bmptopnm - Converts from a Microsoft Windows or OS/2 .BMP file to a
 PBM, PGM, or PPM file.

 This program was formerly called Bmptoppm (and generated only PPM output).
 The name was changed in March 2002.

 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.

*****************************************************************************/
#include <string.h>

#include "pnm.h"
#include "shhopt.h"
#include "bitio.h"
#include "bmp.h"

/* MAXCOLORS is the maximum size of a color map in a BMP image */
#define MAXCOLORS       256

static xelval const bmpMaxval = 255;
    /* The maxval for intensity values in a BMP image -- either in a
       truecolor raster or in a colormap
    */

struct cmdline_info {
    /* All the information the user supplied in the command line,
       in a form easy for the program to use.
    */
    const char *input_filespec;  /* Filespecs of input files */
    int verbose;    /* -verbose option */
};

static const char *ifname;



static void
parse_command_line(int argc, char ** argv,
                   struct cmdline_info *cmdline_p) {
/*----------------------------------------------------------------------------
   Note that the file spec array we return is stored in the storage that
   was passed to us as the argv array.
-----------------------------------------------------------------------------*/
    optStruct *option_def = malloc(100*sizeof(optStruct));
        /* Instructions to OptParseOptions2 on how to parse our options.
         */
    optStruct2 opt;

    unsigned int option_def_index;

    option_def_index = 0;   /* incremented by OPTENTRY */
    OPTENTRY(0,   "verbose",     OPT_FLAG,   &cmdline_p->verbose,         0);

    /* Set the defaults */
    cmdline_p->verbose = FALSE;

    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 */

    optParseOptions2(&argc, argv, opt, 0);
        /* Uses and sets argc, argv, and some of *cmdline_p and others. */

    if (argc-1 == 0) 
        cmdline_p->input_filespec = "-";
    else if (argc-1 != 1)
        pm_error("Program takes zero or one argument (filename).  You "
                 "specified %d", argc-1);
    else
        cmdline_p->input_filespec = argv[1];

}



static char     er_read[] = "%s: read error";

static int
GetByte(FILE * const fp) {

    int             v;

    if ((v = getc(fp)) == EOF)
        pm_error(er_read, ifname);

    return v;
}



static short
GetShort(FILE * const fp) {

    short           v;

    if (pm_readlittleshort(fp, &v) == -1)
        pm_error(er_read, ifname);

    return v;
}



static long
GetLong(FILE * const fp) {

    long v;

    if (pm_readlittlelong(fp, &v) == -1)
        pm_error(er_read, ifname);

    return v;
}



static void
readOffBytes(FILE * const fp, unsigned int const nbytes) {
/*----------------------------------------------------------------------------
   Read 'nbytes' from file 'fp'.  Abort program if read error.
-----------------------------------------------------------------------------*/
    int i;
    
    for(i = 0; i < nbytes; ++i) {
        int rc;
        rc = getc(fp);
        if (rc == EOF)
            pm_error(er_read, ifname);
    }
}



static void
BMPreadfileheader(FILE * const ifP, 
                  unsigned int * const bytesReadP, 
                  unsigned int * const offBitsP) {

    unsigned long   cbSize;
    unsigned short  xHotSpot;
    unsigned short  yHotSpot;
    unsigned long   offBits;

    if (GetByte(ifP) != 'B')
        pm_error("%s is not a BMP file", ifname);
    if (GetByte(ifP) != 'M')
        pm_error("%s is not a BMP file", ifname);

    cbSize = GetLong(ifP);
    xHotSpot = GetShort(ifP);
    yHotSpot = GetShort(ifP);
    offBits = GetLong(ifP);

    *offBitsP = offBits;

    *bytesReadP = 14;
}


struct BMPheader {
    int cols;
    int rows;
    unsigned int cBitCount;
    enum bmpClass class;
    int cmapsize;
    unsigned short cPlanes;
};


static void
BMPreadinfoheader(FILE *             const ifP, 
                  unsigned int *     const bytesReadP,
                  struct BMPheader * const headerP) {

    unsigned long   cbFix;
    unsigned short  cPlanes;

    unsigned long   cx;
    unsigned long   cy;
    unsigned short  cBitCount;
    unsigned long   compression;
    int             class;

    cbFix = GetLong(ifP);

    switch (cbFix) {
    case 12:
        class = C_OS2;

        cx = GetShort(ifP);
        cy = GetShort(ifP);
        cPlanes = GetShort(ifP);
        cBitCount = GetShort(ifP);
        /* I actually don't know if the OS/2 BMP format allows
           cBitCount > 8 or if it does, what it means, but ppmtobmp
           creates such BMPs, more or less as a byproduct of creating
           the same for Windows BMP, so we interpret cBitCount > 8 the
           same as for Windows.
        */
        if (cBitCount <= 8)
            headerP->cmapsize = 1 << cBitCount;
        else if (cBitCount == 24)
            headerP->cmapsize = 0;
        /* There is a 16 bit truecolor format, but we don't know how the
           bits are divided among red, green, and blue, so we can't handle it.
        */
        else
            pm_error("Unrecognized bits per pixel in BMP file header: %d",
                     cBitCount);

        break;
    case 40: {
        int colorsimportant;   /* ColorsImportant value from header */
        int colorsused;        /* ColorsUsed value from header */

        class = C_WIN;

        cx = GetLong(ifP);
        cy = GetLong(ifP);
        cPlanes = GetShort(ifP);
        cBitCount = GetShort(ifP);
 
        compression = GetLong(ifP);
        if (compression != 0) 
            pm_error("Input is compressed.  This program doesn't handle "
                     "compressed images.  Compression type code = %ld",
                     compression);

        /* And read the rest of the junk in the 40 byte header */
        GetLong(ifP);   /* ImageSize */
        GetLong(ifP);   /* XpixelsPerM */
        GetLong(ifP);   /* YpixelsPerM */
        colorsused = GetLong(ifP);   /* ColorsUsed */
        /* See comments in bmp.h for info about the definition of the following
           word and its relationship to the color map size (*pcmapsize).
           */
        colorsimportant = GetLong(ifP);  /* ColorsImportant */

        if (colorsused != 0)
            headerP->cmapsize = colorsused;
        else {
            if (cBitCount <= 8)
                headerP->cmapsize = 1 << cBitCount;
            else if (cBitCount == 24)
                headerP->cmapsize = 0;
            /* There is a 16 bit truecolor format, but we don't know
               how the bits are divided among red, green, and blue, so
               we can't handle it.  */
            else
                pm_error("Unrecognized bits per pixel in BMP file header: %d",
                         cBitCount);
        }
    }
        break;
    default:
        pm_error("%s: unknown cbFix: %ld", ifname, cbFix);
        break;
    }

    if (cPlanes != 1) {
        pm_error("%s: don't know how to handle cPlanes = %d"
             ,ifname
             ,cPlanes);
    }

    switch (class) {
    case C_WIN:
        pm_message("Windows BMP, %ldx%ldx%d"
               ,cx
               ,cy
               ,cBitCount);
        break;
    case C_OS2:
        pm_message("OS/2 BMP, %ldx%ldx%d"
               ,cx
               ,cy
               ,cBitCount);
        break;
    }

    headerP->cols = cx;
    headerP->rows = cy;
    headerP->cBitCount = cBitCount;
    headerP->class = class;
    headerP->cPlanes = cPlanes;

    *bytesReadP = cbFix;
}



static void
BMPreadcolormap(FILE *         const ifP, 
                unsigned int   const cBitCount, 
                int            const class, 
                xel                  colormap[], 
                int            const cmapsize,
                unsigned int * const bytesReadP) {

    int i;
    
    *bytesReadP = 0;  /* initial value */

    for (i = 0; i < cmapsize; ++i) {
        /* There is a document that says the bytes are ordered R,G,B,Z,
           but in practice it appears to be the following instead:
        */
        unsigned int r, g, b;
        
        b = GetByte(ifP);
        g = GetByte(ifP);
        r = GetByte(ifP);

        PNM_ASSIGN(colormap[i], r, g, b);

        *bytesReadP += 3;

        if (class == C_WIN) {
            GetByte(ifP);
            *bytesReadP += 1;
        }
    }
}



static void
convertRow(unsigned char  const bmprow[], 
           xel                  xelrow[],
           int            const cols, 
           unsigned int   const cBitCount, 
           xel            const colormap[]
           ) {
/*----------------------------------------------------------------------------
   Convert a row in raw BMP raster format bmprow[] to a row of xels xelrow[].

   The BMP image has 'cBitCount' bits per pixel.

   If the image is colormapped, colormap[] is the colormap
   (colormap[i] is the color with color index i).
-----------------------------------------------------------------------------*/
    if (cBitCount == 24) {
        /* It's truecolor */
        /* There is a document that gives a much different format for
           24 bit BMPs.  But this seems to be the de facto standard.
        */
        unsigned int col;
        unsigned int cursor;
        cursor = 0;
        for (col = 0; col < cols; ++col)
        {
            PNM_ASSIGN(xelrow[col], 
                       bmprow[cursor+2], bmprow[cursor+1], bmprow[cursor+0]);
            cursor += 3;
        }
    } else if (cBitCount == 8) {            
        /* It's a whole byte colormap index */
        unsigned int col;
        for (col = 0; col < cols; ++col)
            xelrow[col] = colormap[bmprow[col]];
    } else if (cBitCount < 8) {
        /* It's a bit field color index */
        unsigned char const mask = ( 1 << cBitCount ) - 1;

        unsigned int col;

        for (col = 0; col < cols; ++col) {
            unsigned int const cursor = (col*cBitCount)/8;
            unsigned int const shift = 8 - ((col*cBitCount) % 8) - cBitCount;
            unsigned int const index = 
                (bmprow[cursor] & (mask << shift)) >> shift;
            xelrow[col] = colormap[index];
        }
    } else
        pm_error("Internal error: invalid cBitCount in convertRow()");
}



static unsigned char **
allocBMPraster(int const rows, unsigned int const bytesPerRow) {

    unsigned int const storageSize = 
        rows * sizeof(unsigned char *) + rows * bytesPerRow;        
    unsigned char ** BMPraster;
    unsigned int row;
    unsigned char * startOfRows;

    /* The raster array consists of an array of pointers to the rows
       followed by the rows of bytes, in a single allocated chunk of storage.
    */

    BMPraster = (unsigned char **) malloc(storageSize);

    if (BMPraster == NULL)
        pm_error("Unable to allocate %u bytes for the BMP raster\n",
                 storageSize);

    startOfRows = (unsigned char *)(BMPraster + rows);

    for (row = 0; row < rows; ++row) 
        BMPraster[row] = startOfRows + row * bytesPerRow;

    return BMPraster;
}


static void
BMPreadraster(FILE * const ifP, 
              int const cols, int const rows, 
              unsigned int const cBitCount, enum bmpClass const class, 
              xel colormap[], unsigned char *** const BMPrasterP, 
              unsigned int * const bytesReadP) {

    unsigned int const bytesPerRow = ((cols * cBitCount +31)/32)*4;
        /* A BMP raster row is a multiple of 4 bytes, padded on the right
           with don't cares.
        */
    int row;
    unsigned char ** BMPraster;

    BMPraster = allocBMPraster(rows, bytesPerRow);
    /*
     * The picture is stored bottom line first, top line last
     */

    *bytesReadP = 0;

    for (row = rows - 1; row >= 0; --row) {
        unsigned int bytesRead;
        
        bytesRead = 0;

        while (bytesRead < bytesPerRow) {
            int rc;
            rc = fread(BMPraster[row], 1, bytesPerRow, ifP);
            if (rc < 0) {
                if (feof(ifP))
                    pm_error("End of file reading row %u of BMP raster.", row);
                else 
                    pm_error("Error reading BMP raster.  Errno=%d (%s)",
                             errno, strerror(errno));
            }
            bytesRead += rc;
        }
        *bytesReadP += bytesRead;
    }
    *BMPrasterP = BMPraster;
}



static void
writeRaster(unsigned char ** const BMPraster,
            int const cols, int const rows, int format,
            unsigned int const cBitCount, enum bmpClass const class, 
            xel colormap[]) {

    xel * xelrow;
    int row;

    xelrow = pnm_allocrow(cols);

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

    for (row = 0; row < rows; ++row) {
        convertRow(BMPraster[row], xelrow, cols, cBitCount, colormap);
        pnm_writepnmrow(stdout, xelrow, cols, bmpMaxval, format, FALSE);
    }
    pnm_freerow(xelrow);
}



static void
reportHeader(struct BMPheader const header, unsigned int const offBits) {
    
    pm_message("BMP image header says:");
    pm_message("  Class of BMP: %s", 
               header.class == C_WIN ? "Windows" : 
               header.class == C_OS2 ? "OS/2" :
               "???");
    pm_message("  Width: %d pixels", header.cols);
    pm_message("  Height: %d pixels", header.rows);
    pm_message("  Depth: %d planes", header.cPlanes);
    pm_message("  Byte offset of raster within file: %u", offBits);
    pm_message("  Bits per pixel in raster: %u", header.cBitCount);
    pm_message("  Colors in color map: %d", header.cmapsize);
}        



static void
analyzeColors(xel colormap[], unsigned int const cmapsize, xelval const maxval,
              bool * const grayPresentP, bool * const colorPresentP) {
    
    if (cmapsize == 0) {
        /* No colormap, and we're not about to search the entire raster,
           so we just assume it's full color 
        */
        *colorPresentP = TRUE;
        *grayPresentP = TRUE;
    } else {
        unsigned int i;
        *colorPresentP = FALSE;  /* initial assumption */
        *grayPresentP = FALSE;   /* initial assumption */
        for (i = 0; i < cmapsize; ++i) {
            xelval const r = PPM_GETR(colormap[i]);
            xelval const g = PPM_GETG(colormap[i]);
            xelval const b = PPM_GETB(colormap[i]);
            
            if (r != g || g != b) 
                *colorPresentP = TRUE;
            else {
                /* All components are equal */
                if (r != 0 && r != maxval)
                    *grayPresentP = TRUE;
            }
        }
    }
}



static void
readBmp(FILE * const ifP, unsigned char *** const BMPrasterP, 
        int * const colsP, int * const rowsP,
        bool * const grayPresentP, bool * const colorPresentP,
        unsigned int * const cBitCountP, unsigned int * const classP, 
        xel * const colormap,
        bool const verbose) {

    unsigned int pos;

    /* The following are all information from the BMP headers */
    unsigned int offBits;
        /* Byte offset into file of raster */
    struct BMPheader BMPheader;

    pos = 0;
    { 
        unsigned int bytesRead;
        BMPreadfileheader(ifP, &bytesRead, &offBits);
        pos += bytesRead;
    }
    {
        unsigned int bytesRead;
        BMPreadinfoheader(ifP, &bytesRead, &BMPheader);
        if (verbose)
            pm_message("Read %u bytes of header", bytesRead);
        pos += bytesRead;
    }

    if (verbose) 
        reportHeader(BMPheader, offBits);

    if(offBits != BMPoffbits(BMPheader.class, BMPheader.cBitCount, 
                             BMPheader.cmapsize)) {
        pm_message("warning: offBits is %u, expected %u"
                   , offBits
                   , BMPoffbits(BMPheader.class, BMPheader.cBitCount, 
                                BMPheader.cmapsize));
    }
    {
        unsigned int bytesRead;
        BMPreadcolormap(ifP, BMPheader.cBitCount, BMPheader.class, 
                        colormap, BMPheader.cmapsize, &bytesRead);
        pos += bytesRead;
        if (bytesRead != BMPlencolormap(BMPheader.class, BMPheader.cBitCount, 
                                        BMPheader.cmapsize)) {
            pm_message("warning: %u-byte RGB table, expected %u bytes"
                       , bytesRead
                   , BMPlencolormap(BMPheader.class, BMPheader.cBitCount, 
                                    BMPheader.cmapsize));
        }
    }

    analyzeColors(colormap, BMPheader.cmapsize, bmpMaxval, 
                  grayPresentP, colorPresentP);

    readOffBytes(ifP, offBits-pos);

    pos = offBits;

    {
        unsigned int bytesRead;
        BMPreadraster(ifP, BMPheader.cols, BMPheader.rows, 
                      BMPheader.cBitCount, BMPheader.class, colormap, 
                      BMPrasterP, &bytesRead);
        pos += bytesRead;
    }
    if(pos != BMPlenfile(BMPheader.class, BMPheader.cBitCount, 
                         BMPheader.cmapsize, BMPheader.cols, BMPheader.rows)) {
        pm_message("warning: read %u bytes, expected to read %u bytes"
                   , pos
                   , BMPlenfile(BMPheader.class, BMPheader.cBitCount, 
                                BMPheader.cmapsize, 
                                BMPheader.cols, BMPheader.rows));
    }
    *colsP = BMPheader.cols;
    *rowsP = BMPheader.rows;
    *cBitCountP = BMPheader.cBitCount;
    *classP = BMPheader.class;
}



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

    struct cmdline_info cmdline;
    FILE* ifP;
    int outputType;

    bool grayPresent, colorPresent;
        /* These tell whether the image contains shades of gray other than
           black and white and whether it has colors other than black, white,
           and gray.
        */
    int cols, rows;
    unsigned char **BMPraster;
        /* The raster part of the BMP image, as a row x column array, with
           each element being a raw byte from the BMP raster.  Note that
           BMPraster[0] is really Row 0 -- the top row of the image, even
           though the bottom row comes first in the BMP format.
        */
    unsigned int cBitCount;
    enum bmpClass class;
    xel colormap[MAXCOLORS];

    pnm_init(&argc, argv);

    parse_command_line(argc, argv, &cmdline);

    ifP = pm_openr(cmdline.input_filespec);
    if (strcmp(cmdline.input_filespec, "-"))
        ifname = "Standard Input";
    else 
        ifname = cmdline.input_filespec;

    readBmp(ifP, &BMPraster, &cols, &rows, &grayPresent, &colorPresent, 
            &cBitCount, &class, colormap,
            cmdline.verbose);
    pm_close(ifP);

    if (colorPresent) {
        outputType = PPM_TYPE;
        pm_message("WRITING PPM IMAGE");
    } else if (grayPresent) {
        outputType = PGM_TYPE;
        pm_message("WRITING PGM IMAGE");
    } else {
        outputType = PBM_TYPE;
        pm_message("WRITING PBM IMAGE");
    }
    pnm_writepnminit(stdout, cols, rows, bmpMaxval, outputType, FALSE);

    writeRaster(BMPraster, cols, rows, outputType, cBitCount, class,
                colormap);

    free(BMPraster);

    exit(0);
}
