/*=============================================================================
                             winicontopam
===============================================================================
  Convert from Windows icon format to PAM
=============================================================================*/

/*
  Here are some references for the Windows icon format:

  ICO (file format) - Wikipedia
  https://en.wikipedia.org/wiki/ICO_(file_format)

  ICO - Just Solve the File Format Problem
  http://fileformats.archiveteam.org/wiki/ICO
  (Has links to example icon file collections)

  GFF Format Summary: Microsoft Windows Cursor and Icon
  https://web.archive.org/web/20050421161512/http:/www.oreilly.com/www/centers/gff/formats/miccur/index.htm


*/

#include <assert.h>
#include <stdio.h>
#include <string.h>

#include "netpbm/pm_config.h"
#include "netpbm/pm_c_util.h"
#include "netpbm/mallocvar.h"
#include "netpbm/nstring.h"
#include "netpbm/shhopt.h"
#include "netpbm/pam.h"
#include "netpbm/pm_system.h"

#include "winicon.h"

#define RED   0
#define GRN   1
#define BLU   2
#define ALPHA 3
#define CHANNEL_CHARS "RGBA"



static bool verbose;



struct CmdlineInfo {

    const char * inputFileName;
    unsigned int allimages;
    unsigned int imageSpec;
    unsigned int image;
    unsigned int andmasks;
    unsigned int headerdump;
    unsigned int verbose;
};



static void
parseCommandLine(int argc, const char **argv,
                 struct CmdlineInfo * const cmdlineP) {

    optEntry *   option_def;
    unsigned int option_def_index;
    optStruct3   opt3;

    MALLOCARRAY_NOFAIL(option_def, 100);

    option_def_index = 0;

    OPTENT3(0, "allimages",   OPT_FLAG,   NULL,
            &cmdlineP->allimages,         0);
    OPTENT3(0, "image",     OPT_UINT,   &cmdlineP->image,
            &cmdlineP->imageSpec,         0);
    OPTENT3(0, "andmasks",  OPT_FLAG,   NULL,
            &cmdlineP->andmasks,          0);
    OPTENT3(0, "headerdump",   OPT_FLAG,   NULL,
            &cmdlineP->headerdump,        0);
    OPTENT3(0, "verbose",   OPT_FLAG,   NULL,
            &cmdlineP->verbose,           0);

    opt3.opt_table     = option_def;
    opt3.short_allowed = false;
    opt3.allowNegNum   = false;

    pm_optParseOptions4(&argc, argv, opt3, sizeof(opt3), 0);

    if (cmdlineP->allimages && cmdlineP->imageSpec)
        pm_error("You cannot specify both -allimages and -image");

    if (argc-1 < 1)
        cmdlineP->inputFileName = "-";
    else {
        cmdlineP->inputFileName = argv[1];

        if (argc-1 > 1)
            pm_error("Too many arguments.  The only possible "
                     "non-option argument is the input file name");
    }

    free(option_def);
}



static unsigned char const pngSignature[] = PNG_SIGNATURE;



struct File {

    FILE *       fileP;
    const char * name;
    pm_filepos   pos;

};



static uint32_t
u8_le(const unsigned char * const buf,
      size_t                const offset) {

    return buf[offset + 0];
}



static uint32_t
u16_le(const unsigned char * const buf,
       size_t                const offset) {

    return
        ((uint32_t)buf[offset + 0] << 0) +
        ((uint32_t)buf[offset + 1] << 8);
}



static uint32_t
u32_le(const unsigned char * const buf,
       size_t                const offset) {

    return
        ((uint32_t)buf[offset + 0] <<  0) +
        ((uint32_t)buf[offset + 1] <<  8) +
        ((uint32_t)buf[offset + 2] << 16) +
        ((uint32_t)buf[offset + 3] << 24);
}



static uint32_t
s32_le(const unsigned char * const buf,
       size_t                const offset) {

    return
        ((uint32_t)buf[offset + 0] <<  0) +
        ((uint32_t)buf[offset + 1] <<  8) +
        ((uint32_t)buf[offset + 2] << 16) +
        ((uint32_t)buf[offset + 3] << 24);
}



static uint32_t
u8_be(const unsigned char * const buf,
      size_t                const offset) {

    return buf[offset + 0];
}



static uint32_t
u32_be(const unsigned char * const buf,
       size_t                const offset) {

    return
        ((uint32_t)buf[offset + 0] << 24) +
        ((uint32_t)buf[offset + 1] << 16) +
        ((uint32_t)buf[offset + 2] <<  8) +
        ((uint32_t)buf[offset + 3] <<  0);
}



static uint32_t
u32_xx(const unsigned char * const buf,
       size_t                const offset) {

    uint32_t u32;

    ((uint8_t*) &u32)[0] = buf[offset + 0];
    ((uint8_t*) &u32)[1] = buf[offset + 1];
    ((uint8_t*) &u32)[2] = buf[offset + 2];
    ((uint8_t*) &u32)[3] = buf[offset + 3];

    return (u32);
}



#ifndef LITERAL_FN_DEF_MATCH
static qsort_comparison_fn cmpfn;
#endif

static int
cmpfn(const void * const aP,
      const void * const bP) {

    const struct IconDirEntry * const dirEntryAP = aP;
    const struct IconDirEntry * const dirEntryBP = bP;

    if (dirEntryAP->offset < dirEntryBP->offset)
        return -1;
    else if (dirEntryAP->offset > dirEntryBP->offset)
        return +1;
    else
        return 0;
}



static void
dumpIconDir(const struct IconDir * const dirP) {

    unsigned int i;

    pm_message("Type: %u", dirP->type);
    pm_message("Icon directory has %u images:", dirP->count);

    for (i = 0; i < dirP->count; ++i) {
        const struct IconDirEntry * const dirEntryP = &dirP->entries[i];

        pm_message("width: %u", dirEntryP->width);
        pm_message("height: %u", dirEntryP->height);
        pm_message("color count: %u", dirEntryP->color_count);
        pm_message("# color planes: %u", dirEntryP->color_planes);
        pm_message("bits per pixel: %u", dirEntryP->bits_per_pixel);
        pm_message("offset in file of image: %u", dirEntryP->offset);
        pm_message("size of image: %u", dirEntryP->size);
        pm_message("zero field: %u", dirEntryP->zero);
    }
}



static struct IconDir *
readIconDir(struct File * const fP,
            bool          const needHeaderDump) {

    struct IconDir head;
    struct IconDir * dirP;
    uint32_t  imageIndex; /* more bits than dir.count */

    pm_readlittleshortu(fP->fileP, &head.zero);
    pm_readlittleshortu(fP->fileP, &head.type);
    pm_readlittleshortu(fP->fileP, &head.count);
    fP->pos += 6;

    if (head.zero != 0 || head.type != ICONDIR_TYPE_ICO)
        pm_error("Not a valid windows icon file");

    MALLOCVAR(dirP);

    if (dirP == NULL)
        pm_error("Couldn't allocate memory for Icon directory");

    MALLOCARRAY(dirP->entries, head.count);

    if (dirP->entries == NULL)
        pm_error("Could not allocate memory for %u entries in icon directory",
                 head.count);

    dirP->zero           = head.zero;
    dirP->type           = head.type;
    dirP->count          = head.count;
    dirP->entriesAllocCt = head.count;

    for (imageIndex = 0; imageIndex < head.count; ++imageIndex) {
        struct IconDirEntry * const dirEntryP = &dirP->entries[imageIndex];

        unsigned char widthField, heightField;

        unsigned long ul;

        pm_readcharu(fP->fileP, &widthField);
        dirEntryP->width  = (widthField == 0 ? 256 : widthField);

        pm_readcharu(fP->fileP, &heightField);
        dirEntryP->height = (heightField == 0 ? 256 : heightField);

        pm_readcharu(fP->fileP, &dirEntryP->color_count);

        pm_readcharu(fP->fileP, &dirEntryP->zero);

        pm_readlittleshortu(fP->fileP, &dirEntryP->color_planes);

        pm_readlittleshortu(fP->fileP, &dirEntryP->bits_per_pixel);

        pm_readlittlelongu(fP->fileP, &ul); dirEntryP->size = ul;

        pm_readlittlelongu(fP->fileP, &ul); dirEntryP->offset = ul;

        fP->pos += 16;

        dirEntryP->index = imageIndex;
    }

    /* The following is paranoia code only:

       I've never seen a windows icon file in the wild with having the entries
       in the directory stored in a different order than the images
       themselves.  However, the file format allows for it ...
     */
    qsort(dirP->entries, dirP->count, sizeof(struct IconDirEntry), cmpfn);

    if (verbose) {
        pm_message("%s icon directory (%u image%s):",
                   fP->name,
                   dirP->count, dirP->count == 1 ? "" : "s");

        for (imageIndex = 0; imageIndex < dirP->count; ++imageIndex) {
            const struct IconDirEntry * const dirEntryP =
                &dirP->entries[imageIndex];

            uint32_t colorCt;

            if (dirEntryP->bits_per_pixel == 0)
                colorCt = 0;
            else if (dirEntryP->bits_per_pixel >= 32)
                colorCt = 1u << 24;
            else
                colorCt = 1u << dirEntryP->bits_per_pixel;

            if (dirEntryP->color_count != 0 &&
                colorCt > dirEntryP->color_count) {
                colorCt = dirEntryP->color_count;
            }
            pm_message ("%5u: %3u x %3u, %8u colors, %5u bytes",
                        dirEntryP->index,
                        dirEntryP->width,
                        dirEntryP->height,
                        colorCt,
                        dirEntryP->size);
        }
    }

    if (needHeaderDump)
        dumpIconDir(dirP);

    return dirP;
}



static void
freeIconDir(struct IconDir * const dirP) {

    free(dirP->entries);
    free(dirP);
}



static const unsigned char *
readImage(struct File *         const fP,
          struct IconDirEntry * const dirEntryP) {

    size_t rc;
    unsigned char * image;
    uint32_t skippedCt;

    /*  Don't try to read an image that is smaller than the
        BITMAPINFOHEADER of BMP images (40 bytes).

        PNG compressed images can't be smaller than that either, as the
        PNG header plus the mandantory IHDR and IEND chunks already take
        8 + 25 + 12 = 35 bytes, and there is to be a IDAT chunk too.
     */
    if (dirEntryP->size < 40) {
        pm_error("image %2u: format violation: too small as an image.",
                  dirEntryP->index);
    }
    if ((pm_filepos) dirEntryP->offset < fP->pos)
        pm_error("image %2u: format violation: invalid offset.",
                 dirEntryP->index);

    /* The following is paranoia code only:

       I've never seen a windows icon file in the wild with gaps between
       the images, but the file format allows for it, and Microsoft
       expects the user to fseek() to the start of each image.
     */
    skippedCt = 0;

    while ((pm_filepos) dirEntryP->offset > fP->pos) {
        if (getc(fP->fileP) == EOF) {
            pm_error("seeking to image %u: unexpected EOF", dirEntryP->index);
        }
        ++fP->pos;
        ++skippedCt;
    }

    /*  The additional four bytes are for purify and friends, as the
        routines reading BMP XOR and AND masks might read (but not
        evaluate) some bytes beyond the image data.
     */
    image = malloc(dirEntryP->size + sizeof(uint32_t));
    if (image == NULL)
        pm_error("out of memory.");

    rc = fread (image, 1, dirEntryP->size, fP->fileP);
    if (rc != dirEntryP->size) {
        pm_error("reading image %2u: unexpected EOF", dirEntryP->index);
    }
    fP->pos += dirEntryP->size;

    return image;
}



static uint8_t
getIdx1(const unsigned char * const bitmap,
        uint32_t              const offset,
        int16_t               const col) {

    return u8_le(bitmap, offset + (col >> 3)) >> (7 - (col & 0x07)) & 0x1;
}



static uint8_t
getIdx4(const unsigned char * const bitmap,
        uint32_t              const offset,
        int16_t               const col) {

    if ((col & 1) == 0x0000)
        return u8_le(bitmap, offset + (col >> 1)) >> 4 & 0x0F;
    else
        return u8_le(bitmap, offset + (col >> 1)) >> 0 & 0x0F;
}



static uint8_t
getIdx8(const unsigned char * const bitmap,
        uint32_t              const offset,
        int16_t               const col) {

    return u8_le(bitmap, offset + col);
}



typedef unsigned char PaletteEntry[4];



static void
dumpPalette(const PaletteEntry * const palette,
            unsigned int         const colorCt) {

    unsigned int i;

    for (i = 0; i < colorCt; ++i) {
        pm_message("Color %u: (%u, %u, %u)",
                   i, palette[i][2], palette[i][1], palette[i][0]);
    }
}



static void
readXorPalette(struct BitmapInfoHeader * const hdrP,
               const unsigned char *     const bitmap,
               uint32_t                  const maxSize,
               tuple **                  const tuples,
               uint16_t                  const index,
               bool                      const needHeaderDump,
               uint32_t *                const bytesConsumedP) {

    uint32_t paletteSize;

    int16_t     row;
    const PaletteEntry * palette;
    uint32_t    truncatedXorSize;
    uint32_t    bytesConsumed;
    uint32_t    bytesPerRow;
    const unsigned char * bitmapCursor;
    uint32_t sizeRemaining;

    uint8_t (*getIdx) (const unsigned char * bitmap,
                       uint32_t rowOffset,
                       int16_t col);

    if (hdrP->compression_method != BI_RGB)
        pm_error("image %2u: invalid compression method %u.",
                 index, hdrP->compression_method);

    assert(hdrP->bits_per_pixel < 16);

    switch (hdrP->bits_per_pixel) {
    case 1:
        if (hdrP->colors_in_palette == 0)
            hdrP->colors_in_palette = 2;
        getIdx = getIdx1;
        break;

    case 4:
        if (hdrP->colors_in_palette == 0)
            hdrP->colors_in_palette = 16;
        getIdx = getIdx4;
        break;

    case 8:
        if (hdrP->colors_in_palette == 0)
            hdrP->colors_in_palette = 256;
        getIdx = getIdx8;
        break;

    default:
        pm_error("image %2u: "
                 "bits per pixel is a value we don't understand: %u",
                  index, hdrP->bits_per_pixel);
        getIdx = NULL;
    }

    bitmapCursor = &bitmap[0];  /* initial value */
    sizeRemaining = maxSize;    /* initial value */
    bytesConsumed = 0;          /* initial value */

    paletteSize = hdrP->colors_in_palette * 4;

    if (sizeRemaining < paletteSize)
        pm_error("image %2u: "
                 "reading palette: image truncated.", index);

    palette = (const PaletteEntry *) bitmapCursor;

    if (needHeaderDump)
        dumpPalette(palette, hdrP->colors_in_palette);

    bitmapCursor  += paletteSize;
    sizeRemaining -= paletteSize;
    bytesConsumed += paletteSize;

    {
        uint32_t const xorSize = (uint32_t)
            (((hdrP->bits_per_pixel * hdrP->bm_width + 31) / 32)
             * 4 * hdrP->bm_height / 2);

        if (sizeRemaining < xorSize) {
            pm_message("image %2u: "
                       "reading XOR mask: image truncated.", index);
            truncatedXorSize = sizeRemaining;
        } else
            truncatedXorSize = xorSize;
    }

    bytesPerRow = ((hdrP->bits_per_pixel * hdrP->bm_width + 31) / 32) * 4;

    for (row = 0; hdrP->bm_height / 2 > row; ++row) {
        uint32_t rowOffset;

        if (hdrP->top_down)
            rowOffset = row * bytesPerRow;
        else
            rowOffset = (hdrP->bm_height / 2 - row - 1) * bytesPerRow;

        if (rowOffset + bytesPerRow <= truncatedXorSize) {
            int16_t col;
            for (col = 0; hdrP->bm_width > col; ++col) {
                uint8_t const idx = getIdx(bitmapCursor, rowOffset, col);

                if (idx > hdrP->colors_in_palette)
                    pm_error("invalid palette index in row %u, column %u.",
                             row, col);

                /*  The palette is an array of little-endian 32-bit values,
                    where the RGB value is encoded as follows:

                    red:   bits 2^16..2^23
                    green: bits 2^8 ..2^15
                    blue:  bits 2^0 ..2^7
                 */
                tuples[row][col][PAM_RED_PLANE] = palette[idx][2];
                tuples[row][col][PAM_GRN_PLANE] = palette[idx][1];
                tuples[row][col][PAM_BLU_PLANE] = palette[idx][0];
            }
        }
    }

    bitmapCursor  += truncatedXorSize;
    sizeRemaining -= truncatedXorSize;
    bytesConsumed += truncatedXorSize;

    *bytesConsumedP = bytesConsumed;
}



static void
readXorBitfields(struct BitmapInfoHeader * const hdrP,
                 const unsigned char *     const bitmap,
                 uint32_t                  const maxSize,
                 tuple **                  const tuples,
                 uint16_t                  const index,
                 bool *                    const haveAlphaP,
                 uint32_t *                const bytesConsumedP) {
/*----------------------------------------------------------------------------
   Return as *haveAlphaP whether the Xor mask indicates the pixels are
   anything but fully opaque.
-----------------------------------------------------------------------------*/
    uint32_t   bitfields[4];
    uint8_t    shift    [4];
    sample     maxval   [4];

    int16_t      row;
    uint32_t     bytesConsumed;
    uint32_t     bytesPerSample;
    uint32_t     bytesPerRow;
    const unsigned char * bitmapCursor;
    uint32_t     sizeRemaining;
    uint32_t     truncatedXorSize;

    static uint8_t alphas [256];
    bool         allOpaque;
    bool         allTransparent;

    bytesConsumed = 0;

    if (hdrP->compression_method != BI_RGB
        && hdrP->compression_method != BI_BITFIELDS)
        pm_error("image %2u: invalid compression method %u.",
                 index, hdrP->compression_method);

    assert(hdrP->bits_per_pixel >= 16);

    switch (hdrP->bits_per_pixel) {
    case 16:
        bytesPerSample = 2;
        bitfields[RED]   = 0x7C00;
        bitfields[GRN]   = 0x03E0;
        bitfields[BLU]   = 0x001F;
        bitfields[ALPHA] = 0x0000;
        break;

    case 24:
        bytesPerSample = 3;
        bitfields[RED]   = 0xFF0000;
        bitfields[GRN]   = 0x00FF00;
        bitfields[BLU]   = 0x0000FF;
        bitfields[ALPHA] = 0x000000;
        break;

    case 32:
        bytesPerSample = 4;
        bitfields[RED]   = 0x00FF0000;
        bitfields[GRN]   = 0x0000FF00;
        bitfields[BLU]   = 0x000000FF;
        bitfields[ALPHA] = 0xFF000000;
        break;

    default:
        pm_error("image %2u: bits per pixel is one we don't understand: %u.",
                 index, hdrP->bits_per_pixel);
        bytesPerSample = 0;
    }

    bitmapCursor = &bitmap[0]; /* initial value */
    sizeRemaining = maxSize;  /* initial value */

    /*  read bit fields from image data  */
    if (hdrP->compression_method == BI_BITFIELDS) {
        if (sizeRemaining < 12)
            pm_error("image %2u: "
                     "reading bit fields: image truncated.", index);

        bitfields[RED]   = u32_le(bitmapCursor, 0);
        bitfields[GRN]   = u32_le(bitmapCursor, 4);
        bitfields[BLU]   = u32_le(bitmapCursor, 8);
        bitfields[ALPHA] = 0;

        bitmapCursor  += 12;
        sizeRemaining -= 12;
        bytesConsumed += 12;
    }

    /*  determine shift and maxval from bit field for each channel */
    {
        unsigned int i;

        for (i = 0; 4 > i; ++i) {
            if (bitfields[i] == 0) {
                maxval[i] = 1;
                shift [i] = 0;
            } else {
                unsigned int j;

                maxval[i] = bitfields[i];

                for (j = 0; 32 > j; ++j) {
                    if ((maxval[i] & 0x1) != 0)
                        break;
                    maxval[i] >>= 1;
                }
                shift[i] = j;
            }

        }
    }

    /*  read the XOR mask */
    {
        uint32_t const xorSize = (uint32_t)
            (((hdrP->bits_per_pixel * hdrP->bm_width + 31) / 32)
             * 4 * hdrP->bm_height / 2);

        if (sizeRemaining < xorSize) {
            pm_message("image %2u: "
                       "reading XOR mask: image truncated.", index);
            truncatedXorSize = sizeRemaining;
        } else
            truncatedXorSize = xorSize;
    }

    bytesPerRow = ((hdrP->bits_per_pixel * hdrP->bm_width + 31) / 32) * 4;
    MEMSZERO(alphas);

    for (row = 0, allOpaque = true, allTransparent = true;
         hdrP->bm_height / 2 > row;
         ++row) {

        uint32_t offset;

        if (hdrP->top_down)
            offset = row * bytesPerRow;
        else
            offset = (hdrP->bm_height / 2 - row - 1) * bytesPerRow;

        if (offset + bytesPerRow <= truncatedXorSize) {
            unsigned int col;
            for (col = 0; col < hdrP->bm_width; ++col) {
                uint32_t const pixel = u32_le(bitmapCursor, offset);
                offset += bytesPerSample;

                tuples[row][col][PAM_RED_PLANE] =
                    pnm_scalesample((pixel & bitfields[RED]) >> shift[RED],
                                    maxval[RED], 255);
                tuples[row][col][PAM_GRN_PLANE] =
                    pnm_scalesample((pixel & bitfields[GRN]) >> shift[GRN],
                                    maxval[GRN], 255);
                tuples [row][col][PAM_BLU_PLANE]
                    = pnm_scalesample((pixel & bitfields[BLU]) >> shift[BLU],
                                      maxval[BLU], 255);

                if (bitfields [ALPHA] != 0) {
                    tuples[row][col][PAM_TRN_PLANE]
                        = pnm_scalesample(
                            (pixel & bitfields[ALPHA]) >> shift[ALPHA],
                            maxval[ALPHA], 255);

                    if (tuples[row][col][PAM_TRN_PLANE] != 0)
                        allTransparent = false;

                    if (tuples [row][col][PAM_TRN_PLANE] != 255)
                        allOpaque = false;

                    alphas[tuples[row][col][PAM_TRN_PLANE]] = !0;
                } else
                    allTransparent = false;
            }
        }
    }

    bitmapCursor  += truncatedXorSize;
    sizeRemaining -= truncatedXorSize;
    bytesConsumed += truncatedXorSize;

    /*  A fully transparent alpha channel (all zero) in XOR mask is
        defined to be void by Microsoft.
    */
    if (verbose) {
        if (allTransparent)
            pm_message("image %2u: All pixels are nominally coded in the "
                       "transparency map as fully transparent, "
                       "which is defined by the format to mean they "
                       "are all opaque", index);
        if (allOpaque)
            pm_message("image %2u: All pixels are fully opaque "
                       "in the transparency map", index);
    }
    *haveAlphaP = !allTransparent && !allOpaque;

    if (!allTransparent && ! allOpaque && verbose) {
        unsigned int i;
        unsigned int c;

        for (i = 0, c = 0; 256 > i; ++i) {
            if (alphas[i] != 0)
                ++c;
        }
        pm_message("image %2u: %u distinct transparency value%s",
                   index, c, (c == 1) ? "": "s");
    }
    *bytesConsumedP = bytesConsumed;
}



static void
readAnd(struct BitmapInfoHeader * const hdrP,
        const unsigned char *     const bitmap,
        uint32_t                  const maxSize,
        tuple **                  const tuples,
        uint16_t                  const index,
        unsigned int              const plane,
        sample                    const maxval) {
/*----------------------------------------------------------------------------
  Fill in plane 'plane' of the tuple array 'tuples' according to the and
  mask of a Windows icon.  Where the and mask for a pixel is 1, set the
  plane to 'maxval'; where it is zero, set it to zero.

  'bitmap' is the and mask, in a format describe dby *hdrP.

  'index' is the position of the icon in question in the Windows icon file --
  the first image in the file is 0, second is 1, etc.
-----------------------------------------------------------------------------*/
    int16_t  row;
    uint32_t bytesConsumed;
    uint32_t bytesPerRow;
    uint32_t sizeRemaining;
    uint32_t truncatedAndSize;

    sizeRemaining = maxSize;  /* initial value */
    bytesConsumed = 0;  /* initial value */

    {
        uint32_t const andSize = (uint32_t)
            (((1 * hdrP->bm_width + 31) / 32) * 4 * hdrP->bm_height / 2);

        if (sizeRemaining < andSize) {
            pm_message ("image %2u: "
                        "Input image ends %u bytes into the %u-byte "
                        "AND mask.  Implying remainder of mask",
                        index, sizeRemaining, andSize);
            truncatedAndSize = sizeRemaining;
        } else
            truncatedAndSize = andSize;
    }

    bytesPerRow = ((1 * hdrP->bm_width + 31) / 32) * 4;

    for (row = 0; row < hdrP->bm_height / 2; ++row) {
        uint32_t offset;

        if (hdrP->top_down)
            offset = row * bytesPerRow;
        else
            offset = (hdrP->bm_height / 2 - row - 1) * bytesPerRow;

        if (offset + bytesPerRow <= sizeRemaining) {
            unsigned int col;

            for (col = 0; col < hdrP->bm_width; ++col) {
                tuples[row][col][plane] =
                    ((u8_le(bitmap, offset + col/8)
                      & (1 << (7 - (col & 0x7)))) == 0x00) ?
                    maxval : 0
                ;
            }
        }
    }
    sizeRemaining -= truncatedAndSize;
    bytesConsumed += truncatedAndSize;
}



static void
dumpBmpHeader(struct BitmapInfoHeader const hdr,
              unsigned int            const imageIndex) {

    pm_message("BMP header for Image %u:", imageIndex);

    pm_message("header size: %u", hdr.header_size);
    pm_message("bitmap width: %u", hdr.bm_width);
    pm_message("bitmap height * 2: %u", hdr.bm_height);
    pm_message("row order: %s", hdr.top_down ? "top down" : "bottom up");
    pm_message("# color planes: %u", hdr.color_planes);
    pm_message("bits per pixel: %u", hdr.bits_per_pixel);
    pm_message("image size: %u", hdr.image_size);
    pm_message("horizontal resolution: %u", hdr.horizontal_resolution);
    pm_message("vertical resolution: %u", hdr.vertical_resolution);
    pm_message("# colors in palette: %u", hdr.colors_in_palette);
    pm_message("# important colors: %u", hdr.important_colors);
}



static void
readBmpHeader(const unsigned char *     const image,
              uint32_t                  const size,
              unsigned int              const imageIndex,
              bool                      const needHeaderDump,
              struct BitmapInfoHeader * const hdrP) {

    /*  BITMAPINFOHEADER structure */

    if (size < 40)
        pm_error("image %2u: reading BITMAPINFOHEADER: not enough data.",
                 imageIndex);

    hdrP->header_size           = u32_le(image,  0);
    hdrP->bm_width              = s32_le(image,  4);
    hdrP->bm_height             = s32_le(image,  8);
    hdrP->color_planes          = u16_le(image, 12);
    hdrP->bits_per_pixel        = u16_le(image, 14);
    hdrP->compression_method    = u32_le(image, 16);
    hdrP->image_size            = u32_le(image, 20);
    hdrP->horizontal_resolution = s32_le(image, 24);
    hdrP->vertical_resolution   = s32_le(image, 28);
    hdrP->colors_in_palette     = u32_le(image, 32);
    hdrP->important_colors      = u32_le(image, 36);

    if (hdrP->bm_height > 0) {
        hdrP->top_down = false;
    } else {
        hdrP->top_down   = true;
        hdrP->bm_height *= -1;
    }

    if (hdrP->header_size < 36
        || hdrP->bm_width == 0 || hdrP->bm_height == 0
        || (hdrP->bm_height & 1) != 0x0000) {
        pm_error("image %2u: format violation: invalid BMP header.",
                 imageIndex);
    }

    if (needHeaderDump)
        dumpBmpHeader(*hdrP, imageIndex);
}



static void
readXorMask(struct BitmapInfoHeader * const hdrP,
            const unsigned char *     const imageCursor,
            uint32_t                  const imageSize,
            tuple **                  const tuples,
            uint16_t                  const index,
            bool                      const needHeaderDump,
            bool *                    const haveAlphaP,
            uint32_t *                const bytesConsumedP) {
/*----------------------------------------------------------------------------
   Read the so-called XOR mask (for non-monochrome images, this is the
   color pixmap, which may include transparency).

   Return the pixels as 'tuples', an array whose width and height are
   given by *hdrP and depth is 4.

   Return as *haveAlphaP whether the Xor mask indicates the pixels are
   anything but fully opaque.
-----------------------------------------------------------------------------*/
    /*  preset the PAM with fully opaque black (just in case the image
        is truncated and not all pixels are filled in below).
    */
    {
        unsigned int row;

        for (row = 0; row < hdrP->bm_height / 2; ++row) {
            unsigned int col;
            for (col = 0; col < hdrP->bm_width; ++col) {
                tuples[row][col][PAM_RED_PLANE] = 0;
                tuples[row][col][PAM_GRN_PLANE] = 0;
                tuples[row][col][PAM_BLU_PLANE] = 0;
                tuples[row][col][PAM_TRN_PLANE] = 255;
            }
        }
    }

    if (hdrP->bits_per_pixel < 16) {
        readXorPalette(hdrP, imageCursor, imageSize, tuples, index,
                       needHeaderDump,
                       bytesConsumedP);
        *haveAlphaP = false;
    } else
        readXorBitfields(hdrP, imageCursor, imageSize, tuples, index,
                         haveAlphaP, bytesConsumedP);
}



static void
reportImage(unsigned int            const imageIndex,
            struct BitmapInfoHeader const hdr,
            bool                    const haveAlpha) {

    const char * const style =
        haveAlpha ? "RGB +alpha" :
        hdr.bits_per_pixel < 16 ? "RGB/palette" :
        "RGB"
        ;

    pm_message("image %2u: "
               "BMP %3u x %3u x %2u (%s)",
               imageIndex,
               hdr.bm_width, hdr.bm_height / 2, hdr.bits_per_pixel,
               style);
}



static void
convertBmp(const unsigned char * const image,
           FILE *                const ofP,
           struct IconDirEntry * const dirEntryP,
           bool                  const needHeaderDump,
           bool                  const wantAndMaskPlane) {

    struct BitmapInfoHeader hdr;
    uint32_t                offset;
    bool                    haveAlpha;
    uint32_t                xorByteCt;

    struct pam outpam;
    tuple **   tuples;

    readBmpHeader(image, dirEntryP->size, dirEntryP->index, needHeaderDump,
                  &hdr);

    offset = hdr.header_size;  /* Start after header */

    if ((dirEntryP->width != hdr.bm_width)
        || (dirEntryP->height != hdr.bm_height / 2)) {
        pm_message("image %2u: "
                   "mismatch in header and image dimensions "
                   "(%u x %u vs. %u x %u)",
                   dirEntryP->index,
                   dirEntryP->width,
                   dirEntryP->height,
                   hdr.bm_width,
                   hdr.bm_height / 2);
    }

    if ((dirEntryP->bits_per_pixel != 0)
        && (dirEntryP->bits_per_pixel != hdr.bits_per_pixel)) {
        pm_message("image %2u "
                   "mismatch in header and image bpp value"
                   "(%u vs. %u)",
                   dirEntryP->index,
                   dirEntryP->bits_per_pixel,
                   hdr.bits_per_pixel);
    }

    outpam.size   = sizeof(struct pam);
    outpam.len    = PAM_STRUCT_SIZE(allocation_depth);
    outpam.file   = ofP;
    outpam.format = PAM_FORMAT;
    outpam.width  = hdr.bm_width;
    outpam.height = hdr.bm_height / 2;
    outpam.maxval = 255;
    outpam.allocation_depth = 5;
    outpam.depth  = 0;
        /* Just for tuple array allocation; we set the value for the actual
           output image below.
        */

    tuples = pnm_allocpamarray(&outpam);

    readXorMask(&hdr, &image[offset],
                dirEntryP->size - offset,
                tuples, dirEntryP->index, needHeaderDump,
                &haveAlpha, &xorByteCt);

    offset += xorByteCt;

    {
        /* If there is no alpha channel in the XOR mask, use the AND mask as
           the transparency plane.  Else, there are two transparency
           maps. If requested, return the AND mask as a fifth PAM plane.
        */
        bool haveAnd;
        unsigned int andPlane;

        if (!haveAlpha) {
            haveAnd = true;
            andPlane = PAM_TRN_PLANE;
            strcpy (outpam.tuple_type, "RGB_ALPHA");
            outpam.depth  = 4;
        } else if (wantAndMaskPlane) {
            haveAnd = true;
            andPlane = PAM_TRN_PLANE + 1;
            outpam.depth  = 5;
            strcpy(outpam.tuple_type, "RGB_ALPHA_ANDMASK");
        } else {
            haveAnd = false;
            strcpy (outpam.tuple_type, "RGB_ALPHA");
            outpam.depth  = 4;
        }
        if (haveAnd) {
            readAnd(&hdr, &image[offset], dirEntryP->size - offset,
                    tuples, dirEntryP->index, andPlane, outpam.maxval);
        }
    }
    pnm_writepam(&outpam, tuples);
    pnm_freepamarray(tuples, &outpam);

    reportImage(dirEntryP->index, hdr, haveAlpha);
}



static void
reportPngInfo(const unsigned char * const image,
              struct IconDirEntry * const dirEntryP) {

    struct PngIhdr ihdr;

    ihdr.length      = u32_be (image, sizeof(pngSignature)  +0);
    ihdr.signature   = u32_xx (image, sizeof(pngSignature)  +4);
    ihdr.width       = u32_be (image, sizeof(pngSignature)  +8);
    ihdr.height      = u32_be (image, sizeof(pngSignature) +12);
    ihdr.bit_depth   = u8_be  (image, sizeof(pngSignature) +16);
    ihdr.color_type  = u8_be  (image, sizeof(pngSignature) +17);
    ihdr.compression = u8_be  (image, sizeof(pngSignature) +18);
    ihdr.filter      = u8_be  (image, sizeof(pngSignature) +19);
    ihdr.interlace   = u8_be  (image, sizeof(pngSignature) +20);

    if ((ihdr.length != 13)
        || ihdr.signature != *(uint32_t*)"IHDR") {
        pm_message("image %2u: PNG (uncommonly formatted)",
                   dirEntryP->index);
    } else {
        uint32_t depth;
        const char * colorType;

        switch (ihdr.color_type) {
        case 0:
            colorType = "grayscale";
            depth     = ihdr.bit_depth;
            break;

        case 2:
            colorType = "RGB";
            depth     = ihdr.bit_depth * 3;
            break;

        case 3:
            colorType = "RGB/palette";
            depth     = 8;
            break;

        case 4:
            colorType = "grayscale + alpha";
            depth     = ihdr.bit_depth * 2;
            break;

        case 6:
            colorType = "RGB + alpha";
            depth     = ihdr.bit_depth * 4;
            break;

        default:
            colorType = "unknown color system";
            depth     = 0;
            break;
        }
        pm_message("image %2u: PNG %3u x %3u x %2u (%s)",
                   dirEntryP->index,
                   ihdr.width, ihdr.height, depth, colorType);

        if ((dirEntryP->width != ihdr.width)
            || (dirEntryP->height != ihdr.height)) {
            pm_message("image %2u:"
                       " mismatch in header and image dimensions"
                       " (%u x %u vs %u x %u)",
                       dirEntryP->index, dirEntryP->width, dirEntryP->height,
                       ihdr.width, ihdr.height);
        }
        /* Mismatch between dirEntryP->bits_per_pixel and 'depth' is
           normal, because the creator of the winicon file doesn't necessarily
           know the true color resolution.
        */
    }
}



static void
convertPng(const unsigned char * const image,
           FILE *                const ofP,
           struct IconDirEntry * const dirEntryP) {

    pm_bufferDesc imageBuffer;

    reportPngInfo(image, dirEntryP);

    imageBuffer.size              = dirEntryP->size;
    imageBuffer.buffer            = (unsigned char *)image;
    imageBuffer.bytesTransferredP = NULL;

    fflush(stdout);

    pm_system_lp("pngtopam", pm_feed_from_memory, &imageBuffer,
                 NULL /* stdout accepter */, NULL,
                 "pngtopam", "-alphapam", NULL);
}



static uint32_t
bestImage(struct IconDir * const dirP) {

    uint32_t imageIndex;
    uint32_t bestPixelCt;
    uint32_t bestColorCt;
    uint16_t best;

    bestPixelCt = 0;  /* initial value */
    bestColorCt = 0;  /* initial value */
    best        = 0;  /* initial value */

    for (imageIndex = 0; dirP->count > imageIndex; ++imageIndex) {
        struct IconDirEntry * const dirEntryP = &dirP->entries[imageIndex];

        uint32_t const pixelCt = dirEntryP->width * dirEntryP->height;

        uint32_t colorCt;

        /*  32-bit icons have 24 bit color information only.

            Since NT 5.1 (aka WinXP), it is allowed to place 8-bit
            transparency information in the remaining bits (to check,
            you have to read all these bits in the image!), so I prefer
            32-bit images over 24-bit images (which violate the
            spec. anyway).
        */
        if (dirEntryP->bits_per_pixel > 24)
            colorCt = 1u << 25;
        else
            colorCt = 1u << dirEntryP->bits_per_pixel;

        if (dirEntryP->color_count != 0 && colorCt > dirEntryP->color_count)
            colorCt = dirEntryP->color_count;

        if ((pixelCt > bestPixelCt)
            || ((pixelCt == bestPixelCt) && (colorCt > bestColorCt))) {
            /* This is a new best */
            bestPixelCt = pixelCt;
            bestColorCt = colorCt;
            best        = imageIndex;
        }
    }
    return best;
}



static void
convertImage(struct File *         const icoP,
             struct IconDirEntry * const dirEntryP,
             FILE *                const ofP,
             bool                  const needHeaderDump,
             bool                  const wantAndMaskPlane) {

    const unsigned char * image;  /* malloced */

    image = readImage(icoP, dirEntryP);

    if (memeq(image, pngSignature, sizeof (pngSignature)))
        convertPng(image, ofP, dirEntryP);
    else
        convertBmp(image, ofP, dirEntryP, needHeaderDump, wantAndMaskPlane);

    free((void *)image);
}



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

    struct File ico;
    struct IconDir * dirP;
    struct CmdlineInfo cmdline;

    pm_proginit (&argc, argv);

    parseCommandLine(argc, argv, &cmdline);

    verbose = cmdline.verbose;

    ico.name =
        streq(cmdline.inputFileName, "-") ?  "<stdin>" : cmdline.inputFileName;
    ico.pos   = 0;
    ico.fileP = pm_openr(cmdline.inputFileName);

    dirP = readIconDir(&ico, cmdline.headerdump);

    if (cmdline.allimages) {
        unsigned int i;
        for (i = 0; i < dirP->count; ++i)
            convertImage(&ico, &dirP->entries[i], stdout,
                         cmdline.headerdump, cmdline.andmasks);
    } else if (cmdline.imageSpec) {
        unsigned int i;
        bool found;
        for (i = 0, found = false; i < dirP->count; ++i) {
            if (dirP->entries[i].index == cmdline.image) {
                found = true;
                convertImage(&ico, &dirP->entries[i], stdout,
                             cmdline.headerdump, cmdline.andmasks);
            }
        }
        if (!found)
            pm_error("no image index %u in.input", cmdline.image);
    } else {
        convertImage(&ico, &dirP->entries[bestImage(dirP)], stdout,
                     cmdline.headerdump, cmdline.andmasks);
    }

    freeIconDir(dirP);

    if (ico.fileP != stdin)
        pm_close(ico.fileP);

    return 0;
}



