/*=============================================================================
                                  pamtopng
===============================================================================
  Read a Netpbm image and produce a PNG (Portable Network Graphics) image.
=============================================================================*/

/*
  This Netpbm program pamtopng was derived in 2015 from the Netpbm program
  Pnmtopng. This was a nearly complete rewrite with the following goals:

  - Add ability to create a PNG alpha channel from the alpha channel in a
    PAM (format P7) file.

  - Simplify the 20 year old pnmtopng code. Because of the many, many features
    that program implements and its need for backward compatibility, the code
    had become rather complex.  This program is roughly 1/3 the size of
    pnmtopng.c that it replaces.

  - In 1995, bandwid was limited and therefore filesize had to be kept
    small. The original program tried to optimize for that by applying
    many "clever tricks". Today that isn't an issue anymore, so gone
    are filters, palettes, etc. Also, image conversions were removed,
    because those should be done with other NetPBM tools.

  - Add ability to create iTXt (international language) chunks.
*/

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <png.h>

#include "pm_c_util.h"
#include "mallocvar.h"
#include "nstring.h"
#include "shhopt.h"
#include "pam.h"
#include "pngx.h"
#include "pngtxt.h"


/* global variable */
static bool verbose;


struct CmdlineInfo {
    const char * inputFileName;
    unsigned int verbose;
    unsigned int interlace;
    unsigned int transparentSpec;
    const char * transparent;
    unsigned int chromaSpec;
    struct pngx_chroma chroma;
    unsigned int gammaSpec;
    float gamma;
    unsigned int srgbintentSpec;
    pngx_srgbIntent srgbintent;
    unsigned int textSpec;
    const char * text;
    unsigned int ztxtSpec;
    const char * ztxt;
    unsigned int itxtSpec;
    const char * itxt;
    unsigned int backgroundSpec;
    const char * background;
    unsigned int timeSpec;
    time_t time;
};



static void
parseChromaOpt(const char *         const chromaOpt,
               struct pngx_chroma * const chromaP) {

    int count;

    count = sscanf(chromaOpt, "%f %f %f %f %f %f %f %f",
                   &chromaP->wx, &chromaP->wy,
                   &chromaP->rx, &chromaP->ry,
                   &chromaP->gx, &chromaP->gy,
                   &chromaP->bx, &chromaP->by);

    if (count != 8)
        pm_error("Invalid syntax for the -chroma option value '%s'.  "
                 "Should be 8 floating point numbers: "
                 "x and y for each of white, red, green, and blue",
                 chromaOpt);
}



static void
parseSrgbintentOpt(const char *      const srgbintentOpt,
                   pngx_srgbIntent * const srgbintentP) {

    if (streq(srgbintentOpt, "perceptual"))
        *srgbintentP = PNGX_PERCEPTUAL;
    else if (streq(srgbintentOpt, "relativecolorimetric"))
        *srgbintentP = PNGX_RELATIVE_COLORIMETRIC;
    else if (streq(srgbintentOpt, "saturation"))
        *srgbintentP = PNGX_SATURATION;
    else if (streq(srgbintentOpt, "absolutecolorimetric"))
        *srgbintentP = PNGX_ABSOLUTE_COLORIMETRIC;
    else
        pm_error("Unrecognized sRGB intent value '%s'.  We understand "
                 "only 'perceptual', 'relativecolorimetric', "
                 "'saturation', and 'absolutecolorimetric'",
                 srgbintentOpt);
}



static void
parseTimeOpt(const char * const timeOpt,
             time_t *     const timeP) {

    struct tm brokenTime;
    int year;
    int month;
    int count;

    count = sscanf(timeOpt, "%d-%d-%d %d:%d:%d",
                   &year,
                   &month,
                   &brokenTime.tm_mday,
                   &brokenTime.tm_hour,
                   &brokenTime.tm_min,
                   &brokenTime.tm_sec);

    if (count != 6)
        pm_error("Invalid value for -time '%s'.   It should have "
                 "the form [yy]yy-mm-dd hh:mm:ss.", timeOpt);

    if (year < 0)
        pm_error("Year is negative in -time value '%s'", timeOpt);
    if (year > 9999)
        pm_error("Year is more than 4 digits in -time value '%s'",
                 timeOpt);
    if (month < 0)
        pm_error("Month is negative in -time value '%s'", timeOpt);
    if (month > 12)
        pm_error("Month is >12 in -time value '%s'", timeOpt);
    if (brokenTime.tm_mday < 0)
        pm_error("Day of month is negative in -time value '%s'",
                 timeOpt);
    if (brokenTime.tm_mday > 31)
        pm_error("Day of month is >31 in -time value '%s'", timeOpt);
    if (brokenTime.tm_hour < 0)
        pm_error("Hour is negative in -time value '%s'", timeOpt);
    if (brokenTime.tm_hour > 23)
        pm_error("Hour is >23 in -time value '%s'", timeOpt);
    if (brokenTime.tm_min < 0)
        pm_error("Minute is negative in -time value '%s'", timeOpt);
    if (brokenTime.tm_min > 59)
        pm_error("Minute is >59 in -time value '%s'", timeOpt);
    if (brokenTime.tm_sec < 0)
        pm_error("Second is negative in -time value '%s'", timeOpt);
    if (brokenTime.tm_sec > 59)
        pm_error("Second is >59 in -time value '%s'", timeOpt);

    brokenTime.tm_mon = month - 1;
    if (year >= 1900)
        brokenTime.tm_year = year - 1900;
    else
        brokenTime.tm_year = year;

    /* Note that mktime() considers brokeTime to be in local time.
       This is what we want, since we got it from a user.  User should
       set his local time zone to UTC if he wants absolute time.
    */
    *timeP = mktime(&brokenTime);
}



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

    optEntry * option_def;
    optStruct3 opt;
    unsigned int option_def_index = 0;  /* incremented by OPTENT3 */

    const char * srgbintent;
    const char * chroma;
    const char * time;

    MALLOCARRAY(option_def, 100);

    OPTENT3(0,  "verbose",      OPT_FLAG,       NULL,
            &cmdlineP->verbose,        0);
    OPTENT3(0,  "interlace",    OPT_FLAG,       NULL,
            &cmdlineP->interlace,      0);
    OPTENT3(0,  "transparent", OPT_STRING,      &cmdlineP->transparent,
            &cmdlineP->transparentSpec, 0);
    OPTENT3(0,  "chroma",       OPT_STRING,     &chroma,
            &cmdlineP->chromaSpec,     0);
    OPTENT3(0,  "gamma",        OPT_FLOAT,      &cmdlineP->gamma,
            &cmdlineP->gammaSpec,      0);
    OPTENT3(0,  "srgbintent",   OPT_STRING,     &srgbintent,
            &cmdlineP->srgbintentSpec, 0);
    OPTENT3(0,  "text",         OPT_STRING,     &cmdlineP->text,
            &cmdlineP->textSpec,       0);
    OPTENT3(0,  "ztxt",         OPT_STRING,     &cmdlineP->ztxt,
            &cmdlineP->ztxtSpec,       0);
    OPTENT3(0,  "itxt",         OPT_STRING,     &cmdlineP->itxt,
            &cmdlineP->itxtSpec,       0);
    OPTENT3(0,  "background",   OPT_STRING,     &cmdlineP->background,
            &cmdlineP->backgroundSpec, 0);
    OPTENT3(0,  "time",         OPT_STRING,        &time,
            &cmdlineP->timeSpec,       0);

    opt.opt_table = option_def;
    opt.short_allowed = false;  /* we have no short (old-fashioned) options */
    opt.allowNegNum = false;  /* we have no parms that are negative numbers */

    /* uses and sets argc, argv, and some of *cmdlineP and others */
    pm_optParseOptions4(&argc, argv, opt, sizeof(opt), 0);

    if (cmdlineP->chromaSpec)
        parseChromaOpt(chroma, &cmdlineP->chroma);

    if (cmdlineP->srgbintentSpec)
        parseSrgbintentOpt(srgbintent, &cmdlineP->srgbintent);

    if (cmdlineP->timeSpec)
        parseTimeOpt(time, &cmdlineP->time);

    /* get the input-file or stdin pipe */
    if (argc-1 < 1)
        cmdlineP->inputFileName = "-";
    else if (argc-1 == 1)
        cmdlineP->inputFileName = argv[1];
    else
        pm_error("Program takes at most one argument: input file name.");

    free(option_def);
}



static png_byte
colorTypeFromInputType(const struct pam * const pamP) {
/*----------------------------------------------------------------------------
  Analyze the Netpbm image for color-type and bit-depth
-----------------------------------------------------------------------------*/
    png_byte retval;

    if (pamP->depth < 1 || pamP->depth > 4)
        pm_error ("Number of color planes must be between 1 and 4 inclusive");

    if (pamP->maxval != 1 && pamP->maxval != 3 && pamP->maxval != 15 &&
        pamP->maxval != 255 && pamP->maxval != 65535)
        pm_error("The maxval of the input image is %u; "
                 "it must be 1, 3, 15, 255 or 65535", (unsigned)pamP->maxval);

    if (strneq(pamP->tuple_type, "RGB_ALPHA", 9)) {
        if (pamP->depth == 4)
            retval = PNG_COLOR_TYPE_RGB_ALPHA;
        else
            pm_error("Input tuple type is RGB_ALPHA, "
                     "but number of planes is %u instead of 4",
                pamP->depth);
    } else if (strneq(pamP->tuple_type, "RGB", 3)) {
        if (pamP->depth == 3)
            retval = PNG_COLOR_TYPE_RGB;
        else
            pm_error("Input tuple type is RGB, "
                     "but number of planes is %u instead of 3",
                     pamP->depth);
    } else if (strneq(pamP->tuple_type, "GRAYSCALE_ALPHA", 15)) {
        if (pamP->depth == 2)
            retval = PNG_COLOR_TYPE_GRAY_ALPHA;
        else
            pm_error("Input tuple type is GRAYSCALE_ALPHA, "
                     "but number of planes is %u instead of 2",
                     pamP->depth);
    } else if (strneq(pamP->tuple_type, "GRAYSCALE", 9)) {
        if (pamP->depth == 1)
            retval = PNG_COLOR_TYPE_GRAY;
        else
            pm_error("Input tuple type is GRAYSCALE, "
                     "but number of planes is %u instead of 1",
                     pamP->depth);
     } else if (strneq(pamP->tuple_type, "BLACKANDWHITE_ALPHA", 19)) {
        if (pamP->depth != 2)
            pm_error("Input tuple type is BLACKANDWHITE_ALPHA, "
                     "but number of planes is %u instead of 2",
                     pamP->depth);
        if (pamP->maxval != 1)
            pm_error("Input tuple type is BLACKANDWHITE_ALPHA, "
                     "but maxval is %u instead of 1", (unsigned)pamP->maxval);

        retval = PNG_COLOR_TYPE_GRAY_ALPHA;
    } else if (strneq(pamP->tuple_type, "BLACKANDWHITE", 13)) {
        if (pamP->depth != 1)
            pm_error("Input tuple type is BLACKANDWHITE, "
                     "but number of planes is %u instead of 1",
                     pamP->depth);
        if (pamP->maxval != 1)
            pm_error("Input tuple type is BLACKANDWHITE, "
                     "but maxval is %u instead of 1", (unsigned)pamP->maxval);

        retval = PNG_COLOR_TYPE_GRAY;
    } else
        pm_error("Unrecognized tuple type: '%s'", pamP->tuple_type);

    return retval;
}



/*****************************************************************************
*  Subroutines that create all the (ancillary) chunks
*****************************************************************************/



static png_color_16
parseAndScaleColor(const char * const colorString,
                   xelval       const pngMaxval) {

    png_color_16 pngColor;

    if (colorString) {
        xel const inputColor = ppm_parsecolor(colorString, PNM_OVERALLMAXVAL);

        xel scaledColor;

        /* Scale the color down to the PNG bit depth */
        PPM_DEPTH(scaledColor, inputColor, PNM_OVERALLMAXVAL, pngMaxval);

        pngColor.red   = PPM_GETR(scaledColor);
        pngColor.green = PPM_GETG(scaledColor);
        pngColor.blue  = PPM_GETB(scaledColor);
        pngColor.gray  = PNM_GET1(scaledColor);
    }

    return pngColor;
}



static png_color_8
sigBitsFmImgType(unsigned int const pnmBitDepth,
                 int          const pngColorType) {
/*----------------------------------------------------------------------------
   A representation used in PNG of color resolutions in an original image.
-----------------------------------------------------------------------------*/
    png_color_8 retval;

    /* Initial values */
    if (pnmBitDepth < 8) {
        switch (pngColorType) {
        case PNG_COLOR_TYPE_RGB:
            retval.red   = pnmBitDepth;
            retval.green = pnmBitDepth;
            retval.blue  = pnmBitDepth;
            retval.gray  = 0;
            retval.alpha = 0;
            break;
        case PNG_COLOR_TYPE_RGB_ALPHA:
            retval.red   = pnmBitDepth;
            retval.green = pnmBitDepth;
            retval.blue  = pnmBitDepth;
            retval.gray  = 0;
            retval.alpha = pnmBitDepth;
            break;
        case PNG_COLOR_TYPE_GRAY:
            /* PNG can (so presumably will) use original bit depth */
            retval.red   = 0;
            retval.green = 0;
            retval.blue  = 0;
            retval.gray  = 0;
            retval.alpha = 0;
            break;
        case PNG_COLOR_TYPE_GRAY_ALPHA:
            retval.red   = 0;
            retval.green = 0;
            retval.blue  = 0;
            retval.gray  = pnmBitDepth;
            retval.alpha = pnmBitDepth;
            break;
        default:
            assert(false);
        }
    } else {
        /* PNG can (so presumably will) use original bit depth */
        retval.red   = 0;
        retval.green = 0;
        retval.blue  = 0;
        retval.gray  = 0;
        retval.alpha = 0;
    }
    return retval;
}



static void
doTrnsChunk(const struct pam * const pamP,
            struct pngx *      const pngxP,
            const char *       const trans) {

    if (pngx_colorType(pngxP) == PNG_COLOR_TYPE_GRAY_ALPHA ||
        pngx_colorType(pngxP) == PNG_COLOR_TYPE_RGB_ALPHA)
        pm_error("Both alpha channel and transparency chunk not allowed.");
    else {
        xelval const pngMaxval = pm_bitstomaxval(pngx_bitDepth(pngxP));
        png_color_16 const pngColor = parseAndScaleColor(trans, pngMaxval);
            /* Transparent color from text format scaled from 16-bit to
               maxval.
            */

        pngx_setTrnsValue(pngxP, pngColor);

        if (verbose) {
            if (pngx_colorType(pngxP) == PNG_COLOR_TYPE_GRAY) {
                pm_message("writing tRNS chunk with color {gray} = {%u}",
                           pngColor.gray );
            } else if (pngx_colorType(pngxP) == PNG_COLOR_TYPE_RGB) {
                pm_message("writing tRNS chunk with color "
                           "{red, green, blue} = {%u, %u, %u}",
                           pngColor.red, pngColor.green, pngColor.blue);
            }
        }
    }
}



static void
doChrmChunk(struct pngx *      const pngxP,
            struct pngx_chroma const chroma) {

    pngx_setChrm(pngxP, chroma);

    if (verbose) {
        pm_message("writing cHRM chunk { wx, wy, rx, ry, gx, gy, bx, by } = "
                   "{ %4.2f, %4.2f, %4.2f, %4.2f, "
                   "%4.2f, %4.2f, %4.2f, %4.2f }",
                   chroma.wx, chroma.wy,
                   chroma.rx, chroma.ry,
                   chroma.gx, chroma.gy,
                   chroma.bx, chroma.by);
    }
}



static void
doGamaChunk(struct pngx *  const pngxP,
            float          const gamma) {

    pngx_setGama(pngxP, gamma);

    if (verbose) {
        pm_message("writing gAMA chunk with image gamma value %4.2f", gamma);
    }
}



static void
doSbitChunk(const struct pam * const pamP,
            struct pngx *      const pngxP,
            png_color_8        const sigBits) {

    if (sigBits.red + sigBits.green + sigBits.blue +
        sigBits.gray + sigBits.alpha > 0) {
        pngx_setSbit(pngxP, sigBits);
    }
}



static void
doSrgbChunk(struct pngx *   const pngxP,
            pngx_srgbIntent const srgbIntent) {

    pngx_setSrgb(pngxP, srgbIntent);

    if (verbose) {
        pm_message("writing sRGB chunk with intent value %s",
                   pngx_srgbIntentDesc(srgbIntent));
    }
}



static void
doTextChunkSet(struct pngx * const pngxP,
               const char *  const textFileName) {

    bool const ztxt = true;
    bool const itxt = false;

    FILE * tfP;

    tfP = pm_openr(textFileName);

    pngtxt_addChunk(pngxP, tfP, ztxt, itxt, verbose);

    pm_close(tfP);
}



static void
doZtxtChunkSet(struct pngx * const pngxP,
               const char *  const textFileName) {

    bool const ztxt = true;
    bool const itxt = false;

    FILE * tfP;

    tfP = pm_openr(textFileName);

    pngtxt_addChunk(pngxP, tfP, ztxt, itxt, verbose);

    pm_close(tfP);
}



static void
doItxtChunkSet(struct pngx * const pngxP,
               const char *  const textFileName) {

    bool const ztxt = true;
    bool const itxt = true;

    FILE * tfP;

    tfP = pm_openr(textFileName);

    pngtxt_addChunk(pngxP, tfP, ztxt, itxt, verbose);
}



static void
doBkgdChunk (const struct pam * const pamP,
             struct pngx *      const pngxP,
             const char *       const colorName)
{
    xelval const pngMaxval = pm_bitstomaxval(pngx_bitDepth(pngxP));

    png_color_16 const pngColor = parseAndScaleColor(colorName, pngMaxval);
        /* Background color from text format, scaled from 16-bit to maxval */

    pngx_setBkgdRgb(pngxP, pngColor);

    if (verbose) {
        if (pngx_colorType(pngxP) == PNG_COLOR_TYPE_GRAY ||
            pngx_colorType(pngxP) == PNG_COLOR_TYPE_GRAY_ALPHA) {
            pm_message("writing bKGD chunk with gray level = %u",
                       pngColor.gray);
        } else if (pngx_colorType(pngxP) == PNG_COLOR_TYPE_RGB ||
                   pngx_colorType(pngxP) == PNG_COLOR_TYPE_RGB_ALPHA) {
            pm_message("writing bKGD chunk with color {red, green, blue} = "
                       "{%u, %u, %u}",
                       pngColor.red, pngColor.green, pngColor.blue);
        }
    }
}



static void
doTimeChunk(struct pngx * const pngxP,
            time_t        const time) {

    pngx_setTime(pngxP, time);

    if (verbose) {
        struct tm * const brokenTimeP = gmtime(&time);

        char buffer[100];

        strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", brokenTimeP);

        pm_message("Writing tIME chunk specifying datetime %s", buffer);
    }
}



static void
convertRow(const struct pam * const pamP,
           const tuple *      const tuplerow,
           png_byte *         const pngRow,
           unsigned int       const bitDepth) {

    unsigned int col;

    /* An image row consists of columns x planes like gray or rgb(a) x 8 or 16
       bits.
    */
    for (col = 0; col < pamP->width; ++col) {
        unsigned int plane;
        for (plane = 0; plane < pamP->depth; ++plane) {
            if (bitDepth > 8) {
                /* Copy 2 bytes = 16 bits for one pixel */
                pngRow[2 * (pamP->depth * col + plane)] =
                    (tuplerow[col][plane] >> 8) & 0xff ;
                pngRow[2 * (pamP->depth * col + plane) + 1] =
                    tuplerow[col][plane] & 0xff ;
            } else {
                /* Copy 1 byte for one pixel. Later, a packing of 2, 4 or 8
                   pixels into a single byte can still happen.
                */
                pngRow[pamP->depth * col + plane] = tuplerow[col][plane];
            }
        }
    }
}



static void
writeRasterRowByRow(const struct pam * const pamP,
                    struct pngx *      const pngxP,
                    int                const bitDepth) {

    unsigned int const rowSz =
        pamP->width * pamP->depth * (MAX(1, bitDepth/8));

    tuple * tupleRow;
    png_byte * pngRow;
    unsigned int row;

    /* We process row-by-row and do not read the complete image into memory */

    tupleRow = pnm_allocpamrow(pamP);

    MALLOCARRAY(pngRow, rowSz);

    if (pngRow == NULL)
        pm_error("Unable to allocate space for PNG pixel row for "
                 "%u columns", pamP->width);
    else {
        for (row = 0; row < pamP->height; ++row) {
            pnm_readpamrow(pamP, tupleRow);

            convertRow(pamP, tupleRow, pngRow, bitDepth);

            pngx_writeRow(pngxP, pngRow);
        }
        free(pngRow);
    }
    pnm_freepamrow(tupleRow);
}



static png_bytep
mallocPngImage(unsigned int const rowSize,
               unsigned int const height) {

    png_bytep pngImage;

    if (UINT_MAX / rowSize < height)
        pm_error("Image is uncomputably large at %u rows of %u bytes",
                 height, rowSize);

    MALLOCARRAY(pngImage, height * rowSize);

    if (!pngImage)
        pm_error("could not allocate %u bytes for a PNG image buffer",
                 height * rowSize);

    return pngImage;
}



static unsigned int
pngLineSize(struct pngx * const pngxP) {

    unsigned int const bytesPerSample = pngx_bitDepth(pngxP) == 16 ? 2 : 1;

    unsigned int samplesPerPixel;

    switch (pngx_colorType(pngxP)) {
        case PNG_COLOR_TYPE_GRAY:
            samplesPerPixel = 1;
            break;
        case PNG_COLOR_TYPE_GRAY_ALPHA:
            samplesPerPixel = 2;
            break;
        case PNG_COLOR_TYPE_RGB:
            samplesPerPixel = 3;
            break;
        case PNG_COLOR_TYPE_RGB_ALPHA:
            samplesPerPixel = 4;
            break;
        default:
            assert(false);
    }

    if (UINT_MAX / bytesPerSample / samplesPerPixel < pngx_imageWidth(pngxP)) {
        pm_error("pngcopy: width %u of PNG is uncomputably large\n",
                  pngx_imageWidth(pngxP));
    }

    return pngx_imageWidth(pngxP) * bytesPerSample * samplesPerPixel;
}



static void
writeRasterWholeImg(struct pam *  const pamP,
                    struct pngx * const pngxP,
                    unsigned int  const bitDepth) {

    unsigned int const pngRowSize = pngLineSize(pngxP);

    tuple * tupleRow;
    png_bytep pngImage;
        /* A one-dimensional malloc'ed array of all pixels in image */
    png_bytep * pngRowP;
        /* A malloc'ed array of row pointers into pngImage[] */
    unsigned int row;

    tupleRow = pnm_allocpamrow(pamP);

    pngImage = mallocPngImage(pngRowSize, pamP->height);

    MALLOCARRAY(pngRowP, pamP->height);

    if (!pngRowP)
        pm_error("Failed to allocate an array for %u PNG row pointers",
                 pamP->height);

    for (row = 0; row < pamP->height; ++row) {
        png_bytep const thisPngRowP = &pngImage[row * pngRowSize];

        pnm_readpamrow(pamP, tupleRow);

        convertRow(pamP, tupleRow, thisPngRowP, bitDepth);

        pngRowP[row] = thisPngRowP;
    }

    pngx_writeImage(pngxP, pngRowP);

    free(pngRowP);
    free(pngImage);
    pnm_freepamrow(tupleRow);
}



static void
reportInputFormat(const struct pam * const pamP) {

    const char * formatDesc;

    if (pamP->format == PBM_FORMAT || pamP->format == RPBM_FORMAT)
        formatDesc = "PBM";
    else if (pamP->format == PGM_FORMAT || pamP->format == RPGM_FORMAT)
        formatDesc = "PGM";
    else if (pamP->format == PPM_FORMAT || pamP->format == RPPM_FORMAT)
        formatDesc = "PPM";
    else if (pamP->format == PAM_FORMAT)
        formatDesc = "PAM";
    else
        formatDesc = NULL;

    if (formatDesc)
        pm_message("Input format = %s", formatDesc);
    else
        pm_message("Unrecognized input format, format code = 0x%x",
                   pamP->format);

    pm_message("Input tuple type = '%s'", pamP->tuple_type);
    pm_message("Input depth = %u", pamP->depth);
    pm_message("Input maxval = %u", (unsigned int) pamP->maxval);
}



static unsigned int
pngBitDepth(unsigned int const pnmBitDepth,
            int          const pngColorType) {

    unsigned int retval;

    if ((pngColorType == PNG_COLOR_TYPE_RGB ||
         pngColorType == PNG_COLOR_TYPE_RGB_ALPHA ||
         pngColorType == PNG_COLOR_TYPE_GRAY_ALPHA) &&
        pnmBitDepth < 8) {

        retval = 8;
    } else
        retval = pnmBitDepth;

    return retval;
}



static void
addAncillaryChunks(struct pam *       const pamP,
                   struct pngx *      const pngxP,
                   struct CmdlineInfo const cmdline,
                   png_color_8        const sigBits) {
/*----------------------------------------------------------------------------
  Where requested, add ancillary chunks.
-----------------------------------------------------------------------------*/
    if (cmdline.transparentSpec)
        doTrnsChunk(pamP, pngxP,cmdline.transparent);

    if (cmdline.chromaSpec)
        doChrmChunk(pngxP, cmdline.chroma);

    if (cmdline.gammaSpec)
        doGamaChunk(pngxP, cmdline.gamma);

    /* no iccp */

    doSbitChunk(pamP, pngxP, sigBits);

    if (cmdline.srgbintentSpec)
        doSrgbChunk(pngxP, cmdline.srgbintent);

    if (cmdline.textSpec)
        doTextChunkSet(pngxP, cmdline.text);

    if (cmdline.ztxtSpec)
        doZtxtChunkSet(pngxP, cmdline.ztxt);

    if (cmdline.itxtSpec)
        doItxtChunkSet(pngxP, cmdline.itxt);

    if (cmdline.backgroundSpec)
        doBkgdChunk(pamP, pngxP, cmdline.background);

    /* no hist */

    /* no phys */

    /* no splt */

    if (cmdline.timeSpec)
        doTimeChunk(pngxP, cmdline.time);

    /* Write the ancillary chunks to PNG image */
    pngx_writeInfo(pngxP);
}



static void
setShift(struct pngx * const pngxP,
         png_color_8   const sigBits) {

    if (sigBits.red + sigBits.green + sigBits.blue +
        sigBits.gray + sigBits.alpha > 0) {

        /* Move the 1, 2, 4 bits to most significant bits */
        pngx_setShift(pngxP, sigBits);
    }
}



static void
doIhdrChunk(struct pngx * const pngxP,
            unsigned int  const width,
            unsigned int  const height,
            unsigned int  const pnmBitDepth,
            int           const pngColorType,
            bool          const interlace) {

    int const interlaceMethod =
        interlace ? PNG_INTERLACE_ADAM7 : PNG_INTERLACE_NONE;

    pngx_setIhdr(pngxP, width, height,
                 pngBitDepth(pnmBitDepth, pngColorType), pngColorType,
                 interlaceMethod, PNG_COMPRESSION_TYPE_BASE,
                 PNG_FILTER_TYPE_BASE);
}



static void
pamtopng(FILE *             const ifP,
         FILE *             const ofP,
         struct CmdlineInfo const cmdline) {

    unsigned int  pnmBitDepth;
    int           pngColorType;
    struct pngx * pngxP;
    png_color_8   sigBits;
    struct pam    pam;

    pnm_readpaminit(ifP, &pam, PAM_STRUCT_SIZE(tuple_type));

    if (verbose)
        reportInputFormat(&pam);

    pnmBitDepth = pm_maxvaltobits(pam.maxval);

    pngColorType = colorTypeFromInputType(&pam);

    pngx_create(&pngxP, PNGX_WRITE, NULL);

    png_init_io(pngxP->png_ptr, ofP);

    doIhdrChunk(pngxP, pam.width, pam.height,
                pnmBitDepth, pngColorType, cmdline.interlace > 0);

    sigBits = sigBitsFmImgType(pnmBitDepth, pngColorType);

    addAncillaryChunks(&pam, pngxP, cmdline, sigBits);

    setShift(pngxP, sigBits);

    if ((pngColorType == PNG_COLOR_TYPE_GRAY) && (pnmBitDepth < 8)) {
        /* Pack multiple pixels in a byte */
        pngx_setPacking(pngxP);
    }

    if (cmdline.interlace) {
        /* Libpng will expect us to provide pixels in interlaced sequence
           if we write row-by-row, and that is much too difficult, so we
           do whole-image-at-once and let Libpng do the work.
        */
        writeRasterWholeImg(&pam, pngxP, pnmBitDepth);
    } else {
        /* We save memory by going row-by-row */
        writeRasterRowByRow(&pam, pngxP, pnmBitDepth);
    }

    pngx_writeEnd(pngxP);
    pngx_destroy(pngxP);
}



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

    FILE * ifP;
    struct CmdlineInfo cmdline;

    pm_proginit(&argc, argv);

    parseCommandLine(argc, argv, &cmdline);

    verbose = cmdline.verbose;

    ifP = pm_openr(cmdline.inputFileName);

    pamtopng(ifP, stdout, cmdline);

    pm_close(ifP);

    return 0;
}



/* Derived from pnmtorast.c by Jef Poskanzer and pamrgbatopng.c by Bryan
** Henderson <bryanh@giraffe-data.com> and probably some other sources
**
** Copyright (C) 1995-1998 by Alexander Lehmann <alex@hal.rhein-main.de>
**                        and Willem van Schaik <willem@schaik.com>
** Copyright (C) 1999,2001 by Greg Roelofs <newt@pobox.com>
** Copyright (C) 2015 by Willem van Schaik <willem@schaik.com>
**
** 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.
*/
