/* winicontoppm.c - read a MS Windows .ico file and write portable pixmap(s)
**
** Copyright (C) 2000 by Lee Benfield - lee@recoil.org
**
** 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 <math.h>
#include <string.h>

#include "ppm.h"
#include "winico.h"

#define MAJVERSION 0
#define MINVERSION 3

static int      writeToFile = 0;
static int      verbose = 0;
static int      allicons = 0;
static int      writeands = 0;
static int      bestqual = 0;
static int      multippm = 0;
static int      file_offset = 0;    /* not actually used, but useful for debug */
static const char     er_read[] = "%s: read error";
static const char *   infname;
static const char *   outfname;
static FILE *   ifp;

static int 
GetByte(void) {
    int v;
   
    if ((v = getc(ifp)) == EOF)
    {
        pm_error(er_read, infname);
    }
   
    return v;
}
   
static short 
GetShort(void) {
    short v;
   
    if (pm_readlittleshort(ifp, &v) == -1)
    {
        pm_error(er_read, infname);
    }
   
    return v;
}
   
static long 
GetLong(void) {
    long v;
   
    if (pm_readlittlelong(ifp, &v) == -1)
    {
        pm_error(er_read, infname);
    }
   
    return v;
}
   


/*
 * These have no purpose but to wrapper the Byte, Short & Long 
 * functions.
 */
static u1 
readU1 (void) {
    file_offset++;
    return GetByte();
}

static u1 * 
readU1String (int length)
{
   
    u1 * string = malloc (sizeof (u1) * (length+1));
    fread(string,sizeof(u1),length,ifp);
    string[length] = 0;
    file_offset += length;
    return string;
}

static u2 
readU2 (void) {
    file_offset +=2;
    return GetShort();
}

static u4 
readU4 (void) {
    file_offset += 4;
    return GetLong();
}

static IC_Entry 
readICEntry (void) 
{
    IC_Entry entry = malloc ( sizeof (* entry) );
    entry->width         = readU1();
    entry->height        = readU1();
    entry->color_count   = readU1();
    entry->reserved      = readU1();
    entry->planes        = readU2();
    entry->bitcount      = readU2();
    entry->size_in_bytes = readU4();
    entry->file_offset   = readU4();

    /*
     * Spec says 0 color count is really 256.
     */
    if (entry->color_count == 0) entry->color_count = 256;
   
    return entry;
}



static IC_InfoHeader 
readInfoHeader (void) 
{
    IC_InfoHeader ih = malloc ( sizeof (* ih) );
    ih->size            = readU4();
    ih->width           = readU4();
    ih->height          = readU4();
    ih->planes          = readU2();
    ih->bitcount        = readU2();
    ih->compression     = readU4();
    ih->imagesize       = readU4();
    ih->x_pixels_per_m  = readU4();
    ih->y_pixels_per_m  = readU4();
    ih->colors_used     = readU4();
    ih->colors_important = readU4();
    return ih;
}

/*
 * I don't know why this isn't the same as the spec, it just <b>isn't</b>
 * The colors honestly seem to be stored BGR.  Bizarre.
 * 
 * I've checked this in the BMP code for bmptoppm and the gimp.  Guess the
 * spec I have is just plain wrong.
 */
static IC_Color 
readICColor (void) 
{
    IC_Color col = malloc ( sizeof (* col) );
    col->blue     = readU1();
    col->green    = readU1();
    col->red      = readU1();
    col->reserved = readU1();
    return col;
}
   


/*
 * Depending on if the image is stored as 1bpp, 4bpp or 8bpp, the 
 * encoding mechanism is different.
 * 
 * 8bpp => 1 byte/palette index.
 * 4bpp => High Nibble, Low Nibble
 * 1bpp => 1 palette value per bit, high bit 1st.
 */
static u1 * 
read1Bitmap (int width, int height) 
{
    int tmp;
    int xBytes;
    u1 * bitmap = malloc ( (width * height ) * (sizeof (u1)) );
    int wt = width;
    wt >>= 3;
    if (wt & 3) {
        wt = (wt & ~3) + 4;
    }
    xBytes = wt;
    for (tmp = 0; tmp<height; tmp++ ) {
        int x;
        int rowByte = 0;
        int xOrVal = 128;
        u1 * row = readU1String(xBytes);
        for (x = 0; x< width; x++) {
            *(bitmap+((height-tmp-1)*width) + (x)) = (row[rowByte] & xOrVal) / xOrVal;
            if (xOrVal == 1) {
                xOrVal = 128;
                rowByte++;
            } else {
                xOrVal >>= 1;
            }
        }
    }
    return bitmap;
}


   
static u1 * 
read4Bitmap (int width, int height) 
{
    int tmp;
    u1 * bitmap = malloc ( (width * height ) * (sizeof (u1)) );
    int wt = width;
    int xBytes;
    wt >>= 1;
    if (wt & 3) {
        wt = (wt & ~3) + 4;
    }
    xBytes = wt;
    for (tmp = 0; tmp<height ; tmp++ ) {
        int rowByte = 0;
        int bottom = 1;
        int x;
        u1 * row = readU1String(xBytes);
        for (x = 0; x< width; x++) {
            /*
             * 2 nibbles, 2 values.
             */
            if (bottom) {
                *(bitmap+((height-tmp-1)*width) + (x)) = (row[rowByte] & 0xF0) >> 4;
            } else {
                *(bitmap+((height-tmp-1)*width) + (x)) = (row[rowByte] & 0xF);
                rowByte++;
            }
            bottom = !bottom;
        }
    }
    return bitmap;
}


   
static u1 * 
read8Bitmap (int width, int height) 
{
    int tmp;
    unsigned int xBytes;
    unsigned int wt = width;
    u1 * bitmap = malloc ( (width * height ) * (sizeof (u1)) );

   
    if (wt & 3) {
        wt = (wt & ~3) + 4;
    }
    xBytes = wt;
    for (tmp = 0; tmp<height ; tmp++ ) {
        int rowByte = 0;
        int x;
        u1 * row = readU1String(xBytes);
        for ( x = 0; x< width; x++) {
            *(bitmap+((height-tmp-1)*width) + (x)) = row[rowByte];
            rowByte++;
        }
    }
    return bitmap;
}
   


static MS_Ico 
readIconFile (void) 
{
    int iter,iter2;

    MS_Ico MSIconData= malloc( sizeof (* MSIconData) );
   
    /*
     * reserved - should equal 0.
     */
    MSIconData->reserved = readU2();
    /*
     * Type - should equal 1
     */
    MSIconData->type     = readU2();
    /*
     * count - no of icons in file..
     */
    MSIconData->count    = readU2();
    /*
     * Allocate "count" array of entries.
     */
    if (verbose) 
        fprintf (stderr,"Icon file contains %d icons.\n",MSIconData->count);
    MSIconData->entries  = malloc (MSIconData->count * sizeof(IC_Entry *));
    /*
     * Read in each of the entries
     */
    for (iter = 0;iter < MSIconData->count ; iter++ ) {
        MSIconData->entries[iter] = readICEntry();
    }
    /* After that, we have to read in the infoheader, color map (if
     * any) and the actual bit/pix maps for the icons.  
     */
    if (verbose) fprintf (stderr,"#\tColors\tBPP\tWidth\tHeight\n");
    for (iter = 0;iter < MSIconData->count ; iter++ ) {
        int bpp;
        MSIconData->entries[iter]->ih = readInfoHeader ();
        MSIconData->entries[iter]->colors = 
            malloc (MSIconData->entries[iter]->color_count * 
                    sizeof(IC_Color *));
        for (iter2 = 0;
             iter2 < MSIconData->entries[iter]->color_count ; 
             iter2++ ) {
            MSIconData->entries[iter]->colors[iter2] = readICColor();
        }
        /* What's the bits per pixel?  Bit confusing, since there's a
         * field in entry, and in the infoheader.  I'll use both but
         * let the entry field take precedence.  
         */
        bpp = MSIconData->entries[iter]->bitcount ? 
            MSIconData->entries[iter]->bitcount : 
            MSIconData->entries[iter]->ih->bitcount;
        if (verbose) 
            fprintf (stderr,
                     "%d\t%d\t%d\t%d\t%d\n", iter,
                     MSIconData->entries[iter]->color_count, 
                     bpp, MSIconData->entries[iter]->width, 
                     MSIconData->entries[iter]->height);
        /* Pixels are stored bottom-up, left-to-right. Pixel lines are
         * padded with zeros to end on a 32bit (4byte) boundary. Every
         * line will have the same number of bytes. Color indices are
         * zero based, meaning a pixel color of 0 represents the first
         * color table entry, a pixel color of 255 (if there are that
         * many) represents the 256th entry.  
         */
        {
            /*
             * Read XOR Bitmap
             */
            switch (bpp) {
            case 1:
                MSIconData->entries[iter]->xorBitmap = 
                    read1Bitmap(MSIconData->entries[iter]->width,
                                MSIconData->entries[iter]->height);
                break;
            case 4:
                MSIconData->entries[iter]->xorBitmap = 
                    read4Bitmap(MSIconData->entries[iter]->width,
                                MSIconData->entries[iter]->height);
                break;
            case 8:
                MSIconData->entries[iter]->xorBitmap = 
                    read8Bitmap(MSIconData->entries[iter]->width,
                                MSIconData->entries[iter]->height);
                break;
            default:
                pm_error("Uncatered bit depth %d\n",bpp);
            }
            /*
             * Read AND Bitmap
             */
            MSIconData->entries[iter]->andBitmap = 
                read1Bitmap(MSIconData->entries[iter]->width,
                            MSIconData->entries[iter]->height);
        }
      
    }
    return MSIconData;
}



static char * 
trimOutputName (char * inputName)
{
    /*
     * Just trim off the final ".ppm", if there is one, else return as is.
     * oh, for =~ ... :)
     */
    char * outFile = malloc ( sizeof (char) * (strlen (inputName) + 1));
    strcpy(outFile, inputName);
    if (!strcmp (outFile + (strlen (outFile) - 4), ".ppm")) {
        *(outFile + (strlen (outFile) - 4)) = 0;
    }
    return outFile;

}



static int 
getBestQualityIcon(MS_Ico MSIconData)
{
    int x,best,best_size,best_bpp,bpp,size;
    IC_Entry entry;

    best_size = best_bpp = 0;
    for (x = 0; x < MSIconData->count; x++) {
        entry =  MSIconData->entries[x];
        size = entry->width * entry->height;
        bpp  = entry->bitcount ? entry->bitcount : entry->ih->bitcount;
        if (size > best_size) {
            best = x;
            best_size = size;
        } else if (size == best_size && bpp > best_bpp) {
            best = x;
            best_bpp = bpp;
        }
    }
    return best;
}



static void
writeXors(bool const writeToFile, FILE * const multiOutF,
          char outputFileBase[], IC_Entry const entry,
          int const entryNum, bool const multiple, bool const xor) {
/*----------------------------------------------------------------------------
   Write an "xor" image (i.e. the main image) out.

   'multiple' means this is one of multiple images that are being written.
   'entryNum' is the sequence number within the winicon file of the image
   we are writing.

   'xor' means to include "xor" in the output file name.

   if 'multiOutF' is non-null, it is the stream descriptor of an open
   stream to which we are to write the image.  If it is null, 
   we are to open a file using outputFileBase[] and 'entryNum' and 'xor'
   to derive its name, and close it afterward.
-----------------------------------------------------------------------------*/
    FILE * outF;
    pixel ** ppm_array;
    int row;
    char *outputFile;

    outputFile = malloc(sizeof(char) * (strlen (outputFileBase) + 20));

    if (multiOutF)
        outF = multiOutF;
    else {
        if (writeToFile) {
            if (multiple) {
                sprintf(outputFile, "%s%s_%d.ppm",
                        outputFileBase,(xor ? "_xor" : ""), entryNum);
            } else { 
                sprintf(outputFile, "%s%s.ppm",
                        outputFileBase,(xor ? "_xor" : ""));
            }
        } else
            strcpy(outputFile, "-");
        
        outF = pm_openw(outputFile);
    }
    /* 
     * allocate an array to save the bmp data into.
     * note that entry->height will be 1/2 entry->ih->height,
     * as the latter adds "and" and "xor" height.
     */
    ppm_array = ppm_allocarray(entry->width, entry->height);
    for (row=0; row < entry->height; row++) {
        u1 * xorRow;
        int col;
        xorRow = entry->xorBitmap + row * entry->width;
        for (col=0; col < entry->width; col++) {
            int colorIndex;
            IC_Color color;
            colorIndex  = xorRow[col];
            color = entry->colors[colorIndex];
            PPM_ASSIGN(ppm_array[row][col],
                       color->red,color->green,color->blue);
        }
    }    
    ppm_writeppm(outF,ppm_array,entry->width, entry->height, 
                 (pixval) 255, 0);
    ppm_freearray(ppm_array,entry->height);

    if (!multiOutF) 
        pm_close(outF);
}
            


static void
writeAnds(FILE * const multiOutF, 
          char outputFileBase[], IC_Entry const entry, int const entryNum, 
          bool multiple) {
/*----------------------------------------------------------------------------
   Write an "and" image (i.e. the alpha mask) out.

   'multiple' means this is one of multiple images that are being written.
   'entryNum' is the sequence number within the winicon file of the image
   we are writing.

   if 'multiOutF' is non-null, it is the stream descriptor of an open
   stream to which we are to write the image.  If it is null, 
   we are to open a file using outputFileBase[] and 'entryNum' and 'xor'
   to derive its name, and close it afterward.
-----------------------------------------------------------------------------*/
    FILE * outF;
    bit ** pbm_array;
    u1 * andRow;
    int row;

    if (multiOutF)
        outF = multiOutF;
    else {
        char *outputFile;

        outputFile =  malloc ( sizeof (char) * (strlen (outputFileBase) + 20));

        if (allicons) 
            sprintf(outputFile, "%s_and_%d.pbm", outputFileBase, entryNum);
        else 
            sprintf(outputFile, "%s_and.pbm", outputFileBase);
        outF = pm_openw(outputFile);
        free(outputFile);
    }
    pbm_array = pbm_allocarray(entry->width, entry->height);
    for (row=0; row < entry->height; row++) {
        int col;
        andRow = entry->andBitmap + row * entry->width;
        for (col=0; col < entry->width; col++) {
            /* Note: black is transparent in a Netpbm alpha mask */
            pbm_array[row][col] = andRow[col] ? PBM_BLACK: PBM_WHITE;
        }
    }

    pbm_writepbm(outF, pbm_array, entry->width, entry->height, 0);

    pbm_freearray(pbm_array, entry->height);
    if (!multiOutF)
        pm_close (outF);	 
}



static void
openMultiXor(char outputFileBase[], FILE ** const multiOutFP) {

    char *outputFile;

    outputFile =  malloc ( sizeof (char) * (strlen (outputFileBase) + 20));
    /*
     * Open the output file now, it'll stay open the whole time.
     */
    if (writeToFile) {
        sprintf(outputFile, "%s%s.ppm",
                outputFileBase, (writeands ? "_xor" : ""));
    } else {
        sprintf(outputFile,"-");
    }
    *multiOutFP = pm_openw(outputFile);
    free(outputFile);
}



static void
openMultiAnd(char outputFileBase[], FILE ** const multiAndOutFP) {

    char *outputFile;

    outputFile =  malloc ( sizeof (char) * (strlen (outputFileBase) + 20));
    
    sprintf(outputFile, "%s_and.pbm", outputFileBase);
    
    *multiAndOutFP = pm_openw(outputFile);
    free(outputFile);
}




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

    int argn;
    int startEntry, endEntry;
    const char * const usage = "[-writeands] [-allicons|-bestqual] [-multippm] "
        "[-verbose] [iconfile] [ppmdestfile]";
    MS_Ico MSIconData;
    char * outputFileBase;
    char * outputFile;
    FILE * multiOutF;
    FILE * multiAndOutF;
   
    ppm_init (&argc, argv);
    /*
     * Parse command line arguments.
     */
    argn = 1;
    while (argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0') {
        if (pm_keymatch(argv[argn], "-verbose", 2))
            verbose++;
        else if (pm_keymatch(argv[argn], "-allicons", 2))
            allicons++;
        else if (pm_keymatch(argv[argn], "-bestqual", 2))
            bestqual++;
        else if (pm_keymatch(argv[argn], "-writeands", 2))
            writeands++;
        else if (pm_keymatch(argv[argn], "-multippm", 2))
            multippm++;
        else pm_usage(usage);
        ++argn;
    }
   
    if (bestqual && allicons) {
        pm_message ("bestqual flag ignored.");
        bestqual--;
    }
   
    if (argn < argc) {
        ifp = pm_openr(argv[argn]);
        infname = argv[argn];
        ++argn;
    } else {
        ifp = stdin;
        infname = "noname";
    }
   
    if (argn < argc && strcmp(argv[argn],"-")) {
        outputFileBase = trimOutputName(argv[argn]);
        outfname = argv[argn];
        outputFile =  malloc ( sizeof (char) * (strlen (outputFileBase) + 20));
        writeToFile++;
        ++argn;
    } else {
        outfname = "noname";
        if (allicons || writeands) {
            pm_error("When using -allicons or -writeands, "
                     "please supply an output filename.\n");
        }
        if (argn < argc) argn++;
    }
   
    if (argn != argc)
        pm_usage(usage);
   
    MSIconData = readIconFile ();
    /*
     * Now we've read the icon file in (Hopefully! :)
     * Go through each of the entries, and write out files of the
     * form
     * 
     * fname_0_xor.ppm
     * fname_0_and.ppm
     * 
     * (or to stdout, depending on args parsing above).
     */
    /*
     * If allicons is set, we want everything, if not, just go through once.
     */
    startEntry = 0;
    if (allicons) {
        endEntry = MSIconData->count;
    } else {
        endEntry = 1;
    }
    /*
     * If bestqual is set, find the icon with highest size & bpp.
     */
    if (bestqual) {
        startEntry = getBestQualityIcon(MSIconData);
        endEntry = startEntry+1;
    }
   
    if (multippm) 
        openMultiXor(outputFileBase, &multiOutF);
    else
        multiOutF = NULL;

    if (writeands && multippm) 
        openMultiAnd(outputFileBase, &multiAndOutF);
    else
        multiAndOutF = NULL;

    {
        int entryNum;

        for (entryNum = startEntry ; entryNum < endEntry ; entryNum++ ) {
            IC_Entry const entry = MSIconData->entries[entryNum];

            writeXors(writeToFile, multiOutF, outputFileBase, entry, entryNum, 
                      allicons, writeands);
            if (writeands)
                writeAnds(multiAndOutF, outputFileBase, 
                          entry, entryNum, allicons);
        }
    }
    if (multiOutF)
        pm_close (multiOutF);	 
    if (multiAndOutF)
        pm_close(multiAndOutF);
    return 0;
}

