/* ppmtowinicon.c - read portable pixmap file(s) and write a MS Windows .ico
**
** 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.
*/

/*
 * Changelog
 *
 * 2002/01/25 - Use pbm instead of ppm for alpha maps and fix alpha
 *		sense. (black = transparent, white = opaque).
 *		Option renamed to -andpbms; -andppms retained
 *		for compatibility.
 *		ppmtowinicon should now be the true reverse of winicontoppm.
 *		Filled some "ignored" fields in ICO output for maximum
 *		compatibility.  MVB.
 *
 * 2000/05/24 - using colorhash_table instead of a straight array search
 *              Though it's a little slower for files due to the low # of
 *              colors, its neater.  LAB.
 */

#include <math.h>
#include "winico.h"
#include "ppm.h"
#include "ppmcmap.h"

#define MAJVERSION 0
#define MINVERSION 3

#define MAXCOLORS 256


static int      verbose = 0;
static int      readAndMaps = 0;
static int      file_offset = 0;    /* not actually used, but useful for debug. */
static int      readFromStdin = 0;
static char     er_write[] = "%s: write error";
static char *   infname  = "";
static char *   outfname = "-";
static FILE *   ofp;
static FILE *   ifp;

static void 
PutByte(int const v) {
   
   if (putc(v, ofp) == EOF)
     {
	pm_error(er_write, outfname);
     }
}
   
static void 
PutShort(short const v) {
   
   if (pm_writelittleshort(ofp, v) == -1)
     {
	pm_error(er_write, outfname);
     }
   
}
   
static void 
PutLong(long const v) {
   
   if (pm_writelittlelong(ofp, v) == -1)
     {
	pm_error(er_write, outfname);
     }
   
}
   
/*
 * These have no purpose but to wrapper the Byte, Short & Long 
 * functions.
 */
static void 
writeU1 (u1 const v) {
   file_offset++;
   PutByte(v);
}

static void 
writeU1String (char * const string, int const length) {
   fwrite(string,sizeof(u1),length,ofp);
   file_offset += length;
}

static  void 
writeU2 (u2 const v) {
   file_offset +=2;
   PutShort(v);
}

static void 
writeU4 (u4 const v) {
   file_offset += 4;
   PutLong(v);
}

static MS_Ico 
createIconFile (void) {
   MS_Ico MSIconData = malloc( sizeof (* MSIconData) );
   MSIconData->reserved     = 0;
   MSIconData->type         = 1;
   MSIconData->count        = 0;
   MSIconData->entries      = NULL;
   return MSIconData;
}


/* create andBitmap from pgm */

static ICON_bmp 
createAndBitmap (gray ** const ba, int const cols, int const rows,
                 gray const maxval) {
   /*
    * How wide should the u1 string for each row be?
    * each byte is 8 pixels, but must be a multiple of 4 bytes.
    */
   ICON_bmp icBitmap = malloc ( sizeof (* icBitmap) );
   int xBytes,y,x;
   int wt = cols;
   u1 ** rowData;
   wt >>= 3;
   if (wt & 3) {
      wt = (wt & ~3) + 4;
   }
   xBytes = wt;
   rowData = malloc ( rows * sizeof (char *));
   icBitmap->xBytes = xBytes;
   icBitmap->data   = rowData;
   icBitmap->size   = xBytes * rows;
   for (y=0;y<rows;y++) {
      u1 * row = malloc ( xBytes * sizeof (u1));
      int byteOn = 0;
      int bitOn = 128;
      memset (row, 0, xBytes);
      rowData[rows-y-1] = row;
      /* 
       * Check there's a bit array, otherwise we're just faking this...
       */
      if (ba) {
	 for (x=0;x<cols;x++) {
            /* Black (bit clear) is transparent in PGM alpha maps,
             * in ICO bit *set* is transparent.
             */
            if (ba[y][x] <= maxval/2) row[byteOn] |= bitOn;

	    if (bitOn == 1) {
	       byteOn++;
	       bitOn = 128;
	    } else {
	       bitOn >>= 1;
	    }
	 }
      }
   }
   return icBitmap;
}


/*
 * Depending on if the image is stored as 1bpp, 4bpp or 8bpp, the 
 * encoding mechanism is different.
 * 
 * I didn't re-use the code from ppmtobmp since I need to keep the
 * bitmaps in memory till I've loaded all ppms.
 * 
 * 8bpp => 1 byte/palette index.
 * 4bpp => High Nibble, Low Nibble
 * 1bpp => 1 palette value per bit, high bit 1st.
 */
static ICON_bmp 
create1Bitmap (pixel ** const pa, int const cols, int const rows, 
               colorhash_table const cht) {
   /*
    * How wide should the u1 string for each row be?
    * each byte is 8 pixels, but must be a multiple of 4 bytes.
    */
   ICON_bmp icBitmap = malloc ( sizeof (* icBitmap) );
   int xBytes,y,x;
   int wt = cols;
   u1 ** rowData;
   wt >>= 3;
   if (wt & 3) {
      wt = (wt & ~3) + 4;
   }
   xBytes = wt;
   rowData = malloc ( rows * sizeof (char *));
   icBitmap->xBytes = xBytes;
   icBitmap->data   = rowData;
   icBitmap->size   = xBytes * rows;
   for (y=0;y<rows;y++) {
      u1 * row = malloc ( xBytes * sizeof (u1));
      int byteOn = 0;
      int bitOn = 128;
      int value;
      memset (row, 0, xBytes);
      rowData[rows-y-1] = row;
      /* 
       * Check there's a pixel array, otherwise we're just faking this...
       */
      if (pa) {
	 for (x=0;x<cols;x++) {
	    /*
	     * So we've got a colorhash_table with two colors in it.
	     * Which is black?!
	     * 
	     * Unless the hashing function changes, 0's black.
	     */
	    value = ppm_lookupcolor(cht, &pa[y][x]);
	    if (!value) {
	       /* leave black. */
	    } else {
	       row[byteOn] |= bitOn;
	    }
	    if (bitOn == 1) {
	       byteOn++;
	       bitOn = 128;
	    } else {
	       bitOn >>= 1;
	    }
	 }
      }
   }
   return icBitmap;
}


static ICON_bmp 
create4Bitmap (pixel ** const pa, int const cols, int const rows,
               colorhash_table const cht) {
   /*
    * How wide should the u1 string for each row be?
    * each byte is 8 pixels, but must be a multiple of 4 bytes.
    */
   ICON_bmp icBitmap = malloc ( sizeof (* icBitmap) );
   int xBytes,y,x;
   int wt = cols;
   u1 ** rowData;
   wt >>= 1;
   if (wt & 3) {
      wt = (wt & ~3) + 4;
   }
   xBytes = wt;
   rowData = malloc ( rows * sizeof (char *));
   icBitmap->xBytes = xBytes;
   icBitmap->data   = rowData;
   icBitmap->size   = xBytes * rows;

   for (y=0;y<rows;y++) {
      u1 * row = malloc ( xBytes * sizeof (u1));
      int byteOn = 0;
      int nibble = 1;   /* high nibble = 1, low nibble = 0; */
      int value;
      memset (row, 0, xBytes);
      rowData[rows-y-1] = row;
      /* 
       * Check there's a pixel array, otherwise we're just faking this...
       */
      if (pa) {
	 for (x=0;x<cols;x++) {
	    value = ppm_lookupcolor(cht, &pa[y][x]);
	    /*
	     * Shift it, if we're putting it in the high nibble.
	     */
	    if (nibble) {
	       value <<= 4;
	    }
	    row[byteOn] |= value;
	    if (nibble) {
	       nibble = 0;
	    } else {
	       nibble = 1;
	       byteOn++;
	    }
	 }
      }
   }
   return icBitmap;
}



static ICON_bmp 
create8Bitmap (pixel ** const pa, int const cols, int const rows,
               colorhash_table const cht) {
   /*
    * How wide should the u1 string for each row be?
    * each byte is 8 pixels, but must be a multiple of 4 bytes.
    */
   ICON_bmp icBitmap = malloc ( sizeof (* icBitmap) );
   int xBytes,y,x;
   int wt = cols;
   u1 ** rowData;
   if (wt & 3) {
      wt = (wt & ~3) + 4;
   }
   xBytes = wt;
   rowData = malloc ( rows * sizeof (char *));
   icBitmap->xBytes = xBytes;
   icBitmap->data   = rowData;
   icBitmap->size   = xBytes * rows;

   for (y=0;y<rows;y++) {
      u1 * row = malloc ( xBytes * sizeof (u1));
      memset (row, 0, xBytes);
      rowData[rows-y-1] = row;
      /* 
       * Check there's a pixel array, otherwise we're just faking this...
       */
      if (pa) {
	 for (x=0;x<cols;x++) {
	    row[x] = ppm_lookupcolor(cht, &pa[y][x]);
	 }
      }
   }
   return icBitmap;
}



static IC_InfoHeader 
createInfoHeader(IC_Entry const entry, ICON_bmp const xbmp,
                 ICON_bmp const abmp) {
   IC_InfoHeader ih  = malloc ( sizeof (* ih) );
   ih->size          = 40;
   ih->width         = entry->width;
   ih->height        = entry->height * 2;  
   ih->planes        = 1;  
   ih->bitcount      = entry->bitcount;
   ih->compression   = 0;
   ih->imagesize     = entry->width * entry->height * 8 / entry->bitcount;
   ih->x_pixels_per_m= 0;
   ih->y_pixels_per_m= 0;
   ih->colors_used   = 1 << entry->bitcount;
   ih->colors_important = 0;
   return ih;
}



static IC_Palette 
createCleanPalette(void) {
   IC_Palette palette = malloc ( sizeof (* palette) );
   int x;
   palette->colors = malloc (MAXCOLORS * sizeof(IC_Color *));
   for (x=0;x<MAXCOLORS;x++ ){
      palette->colors[x] = NULL;
   }
   return palette;
}



static void 
addColorToPalette(IC_Palette const palette, int const i,
                  int const r, int const g, int const b) {
   palette->colors[i] = malloc ( sizeof (* palette->colors[i]) );
   palette->colors[i]->red      = r;
   palette->colors[i]->green    = g;
   palette->colors[i]->blue     = b;
   palette->colors[i]->reserved = 0;
}



static ICON_bmp 
createBitmap (int const bpp, pixel ** const pa, 
              int const cols, int const rows, 
              colorhash_table const cht) {
    
    ICON_bmp retval;
    const int assumed_bpp = (pa == NULL) ? 1 : bpp;

    switch (assumed_bpp) {
    case 1:
        retval = create1Bitmap (pa,cols,rows,cht);
        break;
    case 4:
        retval = create4Bitmap (pa,cols,rows,cht);
        break;
    case 8:
    default:
        retval = create8Bitmap (pa,cols,rows,cht);
        break;
    }
    return retval;
}



static void 
addEntryToIcon (MS_Ico const MSIconData, char * const xorPPM,
                char * const andPGM) {

   IC_Entry entry = malloc ( sizeof (* entry) );
   FILE * xorfile;
   FILE * andfile;
   pixel ** xorPPMarray;
   gray ** andPGMarray;
   ICON_bmp xorBitmap;
   ICON_bmp andBitmap;
   int xorRows, xorCols, andRows, andCols;
   int bpp, colors, i;
   int entry_cols;
   IC_Palette palette = createCleanPalette();
   colorhist_vector xorChv;
   colorhash_table  xorCht;
   colorhash_table  andCht; 
   
   pixval xorMaxval;
   gray andMaxval;
   /*
    * Read the xor PPM.
    */
   xorfile = pm_openr(xorPPM);
   xorPPMarray = ppm_readppm( xorfile, &xorCols, &xorRows, &xorMaxval );
   pm_close(xorfile);
   /*
    * Since the entry uses 1 byte to hold the width and height of the icon, the
    * image can't be more than 256 x 256.
    */
   if (xorRows > 255 || xorCols > 255) {
      pm_error("Max size for a icon is 255 x 255 (1 byte fields)\n%s is "
               "%d x %d", xorPPM, xorCols, xorRows);
   }
   
   if (verbose) pm_message("read PPM: maxval = %d", xorMaxval);

   /*
    * Figure out the colormap and turn it into the appropriate GIF
    * colormap - this code's pretty much straight from ppmtobpm
    */
   if (verbose) pm_message("computing colormap...");
   xorChv = ppm_computecolorhist(xorPPMarray, xorCols, xorRows, MAXCOLORS, 
                                 &colors);
   if (xorChv == (colorhist_vector) 0)
     pm_error("%s has too many colors - try doing a 'ppmquant %d'"
	      , xorPPM, MAXCOLORS);
   if (verbose) pm_message("%d colors found", colors);

   if (verbose && (xorMaxval > 255))
       pm_message("maxval is not 255 - automatically rescaling colors");
   for (i = 0; i < colors; ++i)
     {
	if (xorMaxval == 255)
	  {
	     addColorToPalette(palette,i,
			       PPM_GETR(xorChv[i].color),
			       PPM_GETG(xorChv[i].color),
			       PPM_GETB(xorChv[i].color));
	  }
	else
	  {
	     addColorToPalette(palette,i,
			       PPM_GETR(xorChv[i].color) * 255 / xorMaxval,
			       PPM_GETG(xorChv[i].color) * 255 / xorMaxval,
			       PPM_GETB(xorChv[i].color) * 255 / xorMaxval);
	  }
     }
   
   /* And make a hash table for fast lookup. */
   xorCht = ppm_colorhisttocolorhash(xorChv, colors);
   ppm_freecolorhist(xorChv);
   
   /*
    * All the icons I found seemed to pad the palette to the max entries
    * for that bitdepth.
    * 
    * The spec indicates this isn't neccessary, but I'll follow this behaviour
    * just in case.
    */
   if (colors < 3) {
      bpp = 1;
      entry_cols = 2;
   } else if (colors < 17) {
      bpp = 4;
      entry_cols = 16;
   } else {
      bpp = 8;
      entry_cols = 256;
   }
   if (!strlen (andPGM)) {
      /* He's not supplying a bitmap for 'and'.  Fake the bitmap. */
      andPGMarray = NULL;
      andCols = xorCols;
      andRows = xorRows;
      andMaxval = 1;
      andCht  = NULL;

   } else {
      andfile = pm_openr(andPGM);
      andPGMarray = pgm_readpgm( andfile, &andCols, &andRows, &andMaxval );
      pm_close(andfile);

      if ((andCols != xorCols) || (andRows != xorRows)) {
          pm_error("%s and %s have different dimensions.\nAborting.\n",
                   xorPPM, andPGM);
      }
   }
   if (verbose) pm_message ("Dimensions of ppms %d, %d\n",xorCols,xorRows);
   xorBitmap = createBitmap(bpp,xorPPMarray,xorCols,xorRows,xorCht);
   andBitmap = createAndBitmap(andPGMarray, andCols, andRows, andMaxval);
   /*
    * Fill in the entry data fields.
    */
   entry->width         = xorCols;
   entry->height        = xorRows;
   entry->color_count   = entry_cols;
   entry->reserved      = 0;
   entry->planes        = 1;
   /* 
    * all the icons I looked at ignored this value...
    */
   entry->bitcount      = bpp;
   entry->ih            = createInfoHeader(entry,xorBitmap,andBitmap);
   entry->colors        = palette->colors;
   entry->size_in_bytes = 
       xorBitmap->size + andBitmap->size + 40 + (4 * entry->color_count);
   if (verbose) 
       pm_message ("entry->size_in_bytes = %d + %d + %d = %d\n",
                   xorBitmap->size ,andBitmap->size, 
                   40, entry->size_in_bytes );
   /*
    * We don't know the offset ATM, set to 0 for now.
    * Have to calculate this at the end.
    */
   entry->file_offset   = 0;
   entry->xorBitmapOut  = xorBitmap->data;
   entry->andBitmapOut  = andBitmap->data;
   entry->xBytesXor     = xorBitmap->xBytes;
   entry->xBytesAnd     = andBitmap->xBytes;  
   /*
    * Add the entry to the entries array.
    */
   MSIconData->count++;
   /* 
    * Perhaps I should use something that allocs a decent amount at start...
    */
   MSIconData->entries = 
       realloc (MSIconData->entries, MSIconData->count * sizeof(IC_Entry *));
   MSIconData->entries[MSIconData->count-1] = entry;
}

static void 
writeIC_Entry (IC_Entry const entry) {
   writeU1(entry->width);
   writeU1(entry->height);
   writeU1(entry->color_count); /* chops 256->0 on its own.. */
   writeU1(entry->reserved);
   writeU2(entry->planes);
   writeU2(entry->bitcount);
   writeU4(entry->size_in_bytes);
   writeU4(entry->file_offset);
}



static void 
writeIC_InfoHeader (IC_InfoHeader const ih) {
   writeU4(ih->size);
   writeU4(ih->width);
   writeU4(ih->height);
   writeU2(ih->planes);
   writeU2(ih->bitcount);
   writeU4(ih->compression);
   writeU4(ih->imagesize);
   writeU4(ih->x_pixels_per_m);
   writeU4(ih->y_pixels_per_m);
   writeU4(ih->colors_used);
   writeU4(ih->colors_important);
}



static void 
writeIC_Color (IC_Color const col) {
   /* Since the ppm might not have as many colors in it as we'd like,
    * (2, 16, 256), stick 0 in the gaps.
    * 
    * This means that we lose palette information, but that can't be
    * helped.  
    */
   if (col == NULL) {
      writeU1(0);
      writeU1(0);
      writeU1(0);
      writeU1(0);
   } else {
      writeU1(col->blue);
      writeU1(col->green);
      writeU1(col->red);
      writeU1(col->reserved);
   }
}



static void
writeBitmap(u1 ** const bitmap, int const xBytes, int const height) {
   int y;
   for (y = 0;y<height;y++) {
      fwrite (bitmap[y],1,xBytes,ofp);
      file_offset += xBytes;
   }
}



static void 
writeMS_Ico (MS_Ico const MSIconData, char * const outFname) {
   int x,y;
   if (!strcmp(outfname,"-")) {
      ofp = stdout;
   } else if (!(ofp = fopen (outfname,"wb"))) {
      pm_error ("Failed to open file %s for writing.\n",outfname);
   }

   writeU2(MSIconData->reserved);
   writeU2(MSIconData->type);
   writeU2(MSIconData->count);
   for (x=0;x<MSIconData->count;x++) writeIC_Entry(MSIconData->entries[x]);
   for (x=0;x<MSIconData->count;x++) {
      writeIC_InfoHeader(MSIconData->entries[x]->ih);
      for (y=0;y<(MSIconData->entries[x]->color_count);y++) {
	 writeIC_Color(MSIconData->entries[x]->colors[y]);
      }
      if (verbose) pm_message("writing xor bitmap\n");
      writeBitmap(MSIconData->entries[x]->xorBitmapOut,
                  MSIconData->entries[x]->xBytesXor,
                  MSIconData->entries[x]->height);
      if (verbose) pm_message("writing and bitmap\n");
      writeBitmap(MSIconData->entries[x]->andBitmapOut,
                  MSIconData->entries[x]->xBytesAnd,
                  MSIconData->entries[x]->height);
   }
   fclose (ofp);
}



int 
main (int argc, char ** argv) {
   MS_Ico MSIconData = createIconFile();
   int iconOn = 1;
   int argn;
   int offset;
   char * usage = "[-andppms] [-output output.ico] [icon1.ppm icon2.ppm ... ]";
   
   /*
    * usage:
    * 
    * ppmtowinicon [-andppms] icon1.ppm [icon2.ppm icon3.ppm ... ] output.ico
    * 
    * Use -a flag if you supply your own and bitmaps.
    */
   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], "-andpgms", 2) 
               || pm_keymatch(argv[argn], "-andppms", 2)) {
	readAndMaps++;
      }
      else if (pm_keymatch(argv[argn], "-output", 2)) {
	 if (argc - argn > 1) {
	    outfname = argv[argn+1];
	    argn++;
	 } else {
	    pm_error ("-output must be supplied a filename");
	 }
      }
      else pm_usage(usage);
      ++argn;
   }
   
   if (argn < argc) {
      /*
       * If there's cmd line args left over, fine. 
       * Use them later.
       */
   } else {
      ifp = stdin;
      infname = "noname";
      readFromStdin++;
   }

   
   if (readFromStdin) {
      addEntryToIcon(MSIconData, "-", "");
      if (verbose) pm_message ("Added entry from stdin.\n");
   } else {
      /*
       * If we're not using fake and maps, then we skip 1 each time.
       */
      for ( iconOn = argn; iconOn < argc ; iconOn += (readAndMaps ? 2 : 1) ) {
	 char * xorPPM;
	 char * andPPM;
	 xorPPM = argv[iconOn];
	 andPPM = (readAndMaps ? argv[iconOn+1] : "");
	 addEntryToIcon(MSIconData, xorPPM, andPPM);
      }
   }
   /*
    * Now we have to go through and calculate the offsets.
    * The first infoheader starts at 6 + count*16 bytes.
    */
   offset = (MSIconData->count * 16) + 6;
   for ( iconOn = 0; iconOn < MSIconData->count; iconOn++ ) {
      IC_Entry entry = MSIconData->entries[iconOn];
      entry->file_offset = offset;
      /* 
       * Increase the offset by the size of this offset & data.
       * this includes the size of the color data.
       */
      offset += entry->size_in_bytes;
   }
   /*
    * And now, we have to actually SAVE the .ico!
    */
   writeMS_Ico(MSIconData,argv[argc-1]);
   return 0;
}
