/*  

    This is dvipdfmx, an eXtended version of dvipdfm by Mark A. Wicks.

    Copyright (C) 2007-2012 by Jin-Hwan Cho and Shunsaku Hirata,
    the dvipdfmx project team.
    
    Copyright (C) 1998, 1999 by Mark A. Wicks <mwicks@kettering.edu>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
    
    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
*/

#if HAVE_CONFIG_H
#include "config.h"
#endif

/*
 * PNG SUPPORT
 *
 *  All bitdepth less than 16 is supported.
 *  Supported color types are: PALETTE, RGB, GRAY, RGB_ALPHA, GRAY_ALPHA.
 *  Supported ancillary chunks: tRNS, cHRM + gAMA, (sRGB), (iCCP)
 * 
 *  gAMA support is available only when cHRM exists. cHRM support is not
 *  tested well. CalRGB/CalGray colorspace is used for PNG images that
 *  have cHRM chunk (but not sRGB).
 *
 * LIMITATIONS
 *
 *   Recent version of PDF (>= 1.5) support 16 bpc, but 16 bit bitdepth PNG
 *   images are automatically converted to 8 bit bitpedth image.
 *
 * TODO
 *
 *  sBIT ? iTXT, tEXT and tIME as MetaData ?, pHYS (see below)
 *  16 bpc support for PDF-1.5. JBIG compression for monochrome image.
 *  Predictor for deflate ?
 */

#include "system.h"
#include "error.h"
#include "mem.h"

#include "dvipdfmx.h"

#include "pdfcolor.h"
#include "pdfobj.h"

#define PNG_DEBUG_STR "PNG"
#define PNG_DEBUG     3

#ifdef HAVE_LIBPNG

/*
 * Write, MNG, Progressive not required.
 */
#define PNG_NO_WRITE_SUPPORTED
#define PNG_NO_MNG_FEATURES
#define PNG_NO_PROGRESSIVE_READ
#if 0
/* 16_TO_8 required. */
#define PNG_NO_READ_TRANSFORMS
#endif

#include <png.h>
#include "pngimage.h"

#include "pdfximage.h"

#define PDF_TRANS_TYPE_NONE   0
#define PDF_TRANS_TYPE_BINARY 1
#define PDF_TRANS_TYPE_ALPHA  2

/* ColorSpace */
static pdf_obj *create_cspace_Indexed  (png_structp png_ptr, png_infop info_ptr);

/* CIE-Based: CalRGB/CalGray
 *
 * We ignore gAMA if cHRM is not found.
 */
static pdf_obj *create_cspace_CalRGB   (png_structp png_ptr, png_infop info_ptr);
static pdf_obj *create_cspace_CalGray  (png_structp png_ptr, png_infop info_ptr);
static pdf_obj *make_param_Cal         (png_byte color_type,
					double G,
					double xw, double yw,
					double xr, double yr,
					double xg, double yg,
					double xb, double yb);

/* sRGB:
 *
 * We (and PDF) do not have direct sRGB support. The sRGB color space can be
 * precisely represented by ICC profile, but we use approximate CalRGB color
 * space.
 */
static pdf_obj *create_cspace_sRGB    (png_structp png_ptr, png_infop info_ptr);
static pdf_obj *get_rendering_intent  (png_structp png_ptr, png_infop info_ptr);

/* ICCBased:
 *
 * Not supported yet.
 * Must check if ICC profile is valid and can be imported to PDF.
 * There are few restrictions (should be applied to PNG too?) in ICC profile
 * support in PDF. Some information should be obtained from profile.
 */
static pdf_obj *create_cspace_ICCBased (png_structp png_ptr, png_infop info_ptr);

/* Transparency */
static int      check_transparency (png_structp png_ptr, png_infop info_ptr);
/* Color-Key Mask */
static pdf_obj *create_ckey_mask   (png_structp png_ptr, png_infop info_ptr);
/* Soft Mask:
 *
 * create_soft_mask() is for PNG_COLOR_TYPE_PALLETE.
 * Images with alpha chunnel use strip_soft_mask().
 * An object representing mask itself is returned.
 */
static pdf_obj *create_soft_mask   (png_structp png_ptr, png_infop info_ptr,
				    png_bytep image_data_ptr,
				    png_uint_32 width, png_uint_32 height);
static pdf_obj *strip_soft_mask    (png_structp png_ptr, png_infop info_ptr,
				    png_bytep image_data_ptr,
				    png_uint_32p rowbytes_ptr,
				    png_uint_32 width, png_uint_32 height);

/* Read image body */
static void read_image_data (png_structp png_ptr, png_infop info_ptr,
			     png_bytep dest_ptr,
			     png_uint_32 height, png_uint_32 rowbytes);

int
check_for_png (FILE *png_file) 
{
  unsigned char sigbytes[4];

  rewind (png_file);
  if (fread (sigbytes, 1, sizeof(sigbytes), png_file) !=
      sizeof(sigbytes) ||
      (png_sig_cmp (sigbytes, 0, sizeof(sigbytes))))
    return 0;
  else
    return 1;
}

int
png_include_image (pdf_ximage *ximage, FILE *png_file)
{
  pdf_obj  *stream;
  pdf_obj  *stream_dict;
  pdf_obj  *colorspace, *mask, *intent;
  png_bytep stream_data_ptr;
  int       trans_type;
  ximage_info info;
  /* Libpng stuff */
  png_structp png_ptr;
  png_infop   png_info_ptr;
  png_byte    bpc, color_type;
  png_uint_32 width, height, rowbytes, xppm, yppm;

  pdf_ximage_init_image_info(&info);

  stream      = NULL;
  stream_dict = NULL;
  colorspace  = mask = intent = NULL;

  rewind (png_file);
  png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
  if (png_ptr == NULL || 
      (png_info_ptr = png_create_info_struct (png_ptr)) == NULL) {
    WARN("%s: Creating Libpng read/info struct failed.", PNG_DEBUG_STR);
    if (png_ptr)
      png_destroy_read_struct(&png_ptr, NULL, NULL);
    return -1;
  }

  /* Inititializing file IO. */
  png_init_io (png_ptr, png_file);

  /* Read PNG info-header and get some info. */
  png_read_info(png_ptr, png_info_ptr);
  color_type = png_get_color_type  (png_ptr, png_info_ptr);
  width      = png_get_image_width (png_ptr, png_info_ptr);
  height     = png_get_image_height(png_ptr, png_info_ptr);
  bpc        = png_get_bit_depth   (png_ptr, png_info_ptr);
  xppm       = png_get_x_pixels_per_meter(png_ptr, png_info_ptr);
  yppm       = png_get_y_pixels_per_meter(png_ptr, png_info_ptr);

  /* We do not need 16-bpc color. Ask libpng to convert down to 8-bpc. */
  if (bpc > 8) {
    png_set_strip_16(png_ptr);
    bpc = 8;
  }

  trans_type = check_transparency(png_ptr, png_info_ptr);
  /* check_transparency() does not do updata_info() */
  png_read_update_info(png_ptr, png_info_ptr);
  rowbytes = png_get_rowbytes(png_ptr, png_info_ptr);

  /* Values listed below will not be modified in the remaining process. */
  info.width  = width;
  info.height = height;
  info.bits_per_component = bpc;

  if (compat_mode)
    info.xdensity = info.ydensity = 72.0 / 100.0;
  else {
    if (xppm > 0)
      info.xdensity = 72.0 / 0.0254 / xppm;
    if (yppm > 0)
      info.ydensity = 72.0 / 0.0254 / yppm;
  }

  stream      = pdf_new_stream (STREAM_COMPRESS);
  stream_dict = pdf_stream_dict(stream);

  stream_data_ptr = (png_bytep) NEW(rowbytes*height, png_byte);
  read_image_data(png_ptr, png_info_ptr, stream_data_ptr, height, rowbytes);

  /* Non-NULL intent means there is valid sRGB chunk. */
  intent = get_rendering_intent(png_ptr, png_info_ptr);
  if (intent)
    pdf_add_dict(stream_dict, pdf_new_name("Intent"), intent);

  switch (color_type) {
  case PNG_COLOR_TYPE_PALETTE:

    colorspace = create_cspace_Indexed(png_ptr, png_info_ptr);

    switch (trans_type) {
    case PDF_TRANS_TYPE_BINARY:
      /* Color-key masking */
      mask = create_ckey_mask(png_ptr, png_info_ptr);
      break;
    case PDF_TRANS_TYPE_ALPHA:
      /* Soft mask */
      mask = create_soft_mask(png_ptr, png_info_ptr, stream_data_ptr, width, height);
      break;
    default:
      /* Nothing to be done here.
       * No tRNS chunk or image already composited with background color.
       */
      break;
    }
    break;
  case PNG_COLOR_TYPE_RGB:
  case PNG_COLOR_TYPE_RGB_ALPHA:

    if (png_get_valid(png_ptr, png_info_ptr, PNG_INFO_iCCP))
      colorspace = create_cspace_ICCBased(png_ptr, png_info_ptr);
    else if (intent) {
      colorspace = create_cspace_sRGB(png_ptr, png_info_ptr);
    } else {
      colorspace = create_cspace_CalRGB(png_ptr, png_info_ptr);
    }
    if (!colorspace)
      colorspace = pdf_new_name("DeviceRGB");

    switch (trans_type) {
    case PDF_TRANS_TYPE_BINARY:
      if (color_type != PNG_COLOR_TYPE_RGB)
	ERROR("Unexpected error in png_include_image().");
      mask = create_ckey_mask(png_ptr, png_info_ptr);
      break;
    /* rowbytes changes 4 to 3 at here */
    case PDF_TRANS_TYPE_ALPHA:
      if (color_type != PNG_COLOR_TYPE_RGB_ALPHA)
	ERROR("Unexpected error in png_include_image().");
      mask = strip_soft_mask(png_ptr, png_info_ptr,
			     stream_data_ptr, &rowbytes, width, height);
      break;
    default:
      mask = NULL;
    }
    info.num_components = 3;
    break;

  case PNG_COLOR_TYPE_GRAY:
  case PNG_COLOR_TYPE_GRAY_ALPHA:

    if (png_get_valid(png_ptr, png_info_ptr, PNG_INFO_iCCP))
      colorspace = create_cspace_ICCBased(png_ptr, png_info_ptr);
    else if (intent) {
      colorspace = create_cspace_sRGB(png_ptr, png_info_ptr);
    } else {
      colorspace = create_cspace_CalGray(png_ptr, png_info_ptr);
    }
    if (!colorspace)
      colorspace = pdf_new_name("DeviceGray");

    switch (trans_type) {
    case PDF_TRANS_TYPE_BINARY:
      if (color_type != PNG_COLOR_TYPE_GRAY)
	ERROR("Unexpected error in png_include_image().");
      mask = create_ckey_mask(png_ptr, png_info_ptr);
      break;
    case PDF_TRANS_TYPE_ALPHA:
      if (color_type != PNG_COLOR_TYPE_GRAY_ALPHA)
	ERROR("Unexpected error in png_include_image().");
      mask = strip_soft_mask(png_ptr, png_info_ptr,
			     stream_data_ptr, &rowbytes, width, height);
      break;
    default:
      mask = NULL;
    }
    info.num_components = 1;
    break;

  default:
    WARN("%s: Unknown PNG colortype %d.", PNG_DEBUG_STR, color_type);
  }
  pdf_add_dict(stream_dict, pdf_new_name("ColorSpace"), colorspace);

  pdf_add_stream(stream, stream_data_ptr, rowbytes*height);
  RELEASE(stream_data_ptr);

  if (mask) {
    if (trans_type == PDF_TRANS_TYPE_BINARY)
      pdf_add_dict(stream_dict, pdf_new_name("Mask"), mask);
    else if (trans_type == PDF_TRANS_TYPE_ALPHA) {
      pdf_add_dict(stream_dict, pdf_new_name("SMask"), pdf_ref_obj(mask));
      pdf_release_obj(mask);
    } else {
      WARN("%s: You found a bug in pngimage.c.", PNG_DEBUG_STR);
      pdf_release_obj(mask);
    }
  }

  png_read_end(png_ptr, NULL);

  /* Cleanup */
  if (png_info_ptr)
    png_destroy_info_struct(png_ptr, &png_info_ptr);
  if (png_ptr)
    png_destroy_read_struct(&png_ptr, NULL, NULL);

  pdf_ximage_set_image(ximage, &info, stream);

  return 0;
}

/* 
 * The returned value trans_type is the type of transparency to be used for
 * this image. Possible values are:
 *
 *   PDF_TRANS_TYPE_NONE    No Masking will be used/required.
 *   PDF_TRANS_TYPE_BINARY  Pixels are either fully opaque/fully transparent.
 *   PDF_TRANS_TYPE_ALPHA   Uses alpha channel, requies SMask.(PDF-1.4)
 *
 * check_transparency() must check the current setting of output PDF version
 * and must choose appropriate trans_type value according to PDF version of
 * current output PDF document.
 *
 * If the PDF version is less than 1.3, no transparency is supported for this
 * version of PDF, hence PDF_TRANS_TYPE_NONE must be returned. And when the PDF
 * version is equal to 1.3, possible retrun values are PDF_TRANS_TYPE_BINARY or
 * PDF_TRANS_TYPE_NONE. The latter case arises when PNG file uses alpha channel
 * explicitly (color type PNG_COLOR_TYPE_XXX_ALPHA), or the tRNS chunk for the
 * PNG_COLOR_TYPE_PALETTE image contains intermediate values of opacity.
 *
 * Finally, in the case of PDF version 1.4, all kind of translucent pixels can
 * be represented with Soft-Mask.
 */

static int
check_transparency (png_structp png_ptr, png_infop info_ptr)
{
  int           trans_type;
  unsigned      pdf_version;
  png_byte      color_type;
  png_color_16p trans_values;
  png_bytep     trans;
  int           num_trans;

  pdf_version = pdf_get_version();
  color_type  = png_get_color_type(png_ptr, info_ptr);

  /*
   * First we set trans_type to appropriate value for PNG image.
   */
  if (color_type == PNG_COLOR_TYPE_RGB_ALPHA ||
      color_type == PNG_COLOR_TYPE_GRAY_ALPHA) {
    trans_type = PDF_TRANS_TYPE_ALPHA;
  } else if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) &&
	     png_get_tRNS(png_ptr, info_ptr, &trans, &num_trans, &trans_values)) {
    /* Have valid tRNS chunk. */
    switch (color_type) {
    case PNG_COLOR_TYPE_PALETTE:
      /* Use color-key mask if possible. */ 
      trans_type = PDF_TRANS_TYPE_BINARY;
      while (num_trans-- > 0) {
	if (trans[num_trans] != 0x00 && trans[num_trans] != 0xff) {
	  /* This seems not binary transparency */
	  trans_type = PDF_TRANS_TYPE_ALPHA;
	  break;
	}
      }
      break;
    case PNG_COLOR_TYPE_GRAY:
    case PNG_COLOR_TYPE_RGB:
      /* RGB or GRAY, single color specified by trans_values is transparent. */
      trans_type = PDF_TRANS_TYPE_BINARY;
      break;
    default:
      /* Else tRNS silently ignored. */
      trans_type = PDF_TRANS_TYPE_NONE;
    }
  } else { /* no transparency */
    trans_type = PDF_TRANS_TYPE_NONE;
  }

  /*
   * Now we check PDF version.
   * We can convert alpha cahnnels to explicit mask via user supplied alpha-
   * threshold value. But I will not do that.
   */
  if (( pdf_version < 3 && trans_type != PDF_TRANS_TYPE_NONE   ) ||
      ( pdf_version < 4 && trans_type == PDF_TRANS_TYPE_ALPHA )) {
    /*
     *   No transparency supported but PNG uses transparency, or Soft-Mask
     * required but no support for it is available in this version of PDF.
     * We must do pre-composition of image with the background image here. But,
     * we cannot do that in general since dvipdfmx is not a rasterizer. What we
     * can do here is to composite image with a rectangle filled with the
     * background color. However, images are stored as an Image XObject which
     * can be referenced anywhere in the PDF document content. Hence, we cannot
     * know the correct background color at this time. So we will choose white
     * as background color, which is most probable color in our cases.
     * We ignore bKGD chunk.
     */
    png_color_16 bg;
    bg.red = 255; bg.green = 255; bg.blue  = 255; bg.gray = 255; bg.index = 0;
    png_set_background(png_ptr, &bg, PNG_BACKGROUND_GAMMA_SCREEN, 0, 1.0);
    WARN("%s: Transparency will be ignored. (no support in PDF ver. < 1.3)", PNG_DEBUG_STR);
    if (pdf_version < 3)
      WARN("%s: Please use -V 3 option to enable binary transparency support.", PNG_DEBUG_STR);
    if (pdf_version < 4)
      WARN("%s: Please use -V 4 option to enable full alpha channel support.", PNG_DEBUG_STR);
    trans_type = PDF_TRANS_TYPE_NONE;
  }

  return trans_type;
}

/*
 * sRGB:
 *
 *   If sRGB chunk is present, cHRM and gAMA chunk must be ignored.
 *
 */
static pdf_obj *
get_rendering_intent (png_structp png_ptr, png_infop info_ptr)
{
  pdf_obj *intent;
  int      srgb_intent;

  if (png_get_valid(png_ptr, info_ptr, PNG_INFO_sRGB) &&
      png_get_sRGB (png_ptr, info_ptr, &srgb_intent)) {
    switch (srgb_intent) {
    case PNG_sRGB_INTENT_SATURATION:
      intent = pdf_new_name("Saturation");
      break;
    case PNG_sRGB_INTENT_PERCEPTUAL:
      intent = pdf_new_name("Perceptual");
      break;
    case PNG_sRGB_INTENT_ABSOLUTE:
      intent = pdf_new_name("AbsoluteColorimetric");
      break;
    case PNG_sRGB_INTENT_RELATIVE:
      intent = pdf_new_name("RelativeColorimetric");
      break;
    default:
      WARN("%s: Invalid value in PNG sRGB chunk: %d", PNG_DEBUG_STR, srgb_intent);
      intent = NULL;
    }
  } else
    intent = NULL;

  return intent;
}

/* Approximated sRGB */
static pdf_obj *
create_cspace_sRGB (png_structp png_ptr, png_infop info_ptr)
{
  pdf_obj  *colorspace;
  pdf_obj  *cal_param;
  png_byte  color_type;

  color_type = png_get_color_type(png_ptr, info_ptr);

  /* Parameters taken from PNG spec. section 4.2.2.3. */
  cal_param = make_param_Cal(color_type,
			     2.2,
			     0.3127, 0.329,
			     0.64, 0.33, 0.3, 0.6, 0.15, 0.06);
  if (!cal_param)
    return NULL;

  colorspace = pdf_new_array();

  switch (color_type) {
  case PNG_COLOR_TYPE_RGB:
  case PNG_COLOR_TYPE_RGB_ALPHA:
  case PNG_COLOR_TYPE_PALETTE:
    pdf_add_array(colorspace, pdf_new_name("CalRGB"));
    break;
  case PNG_COLOR_TYPE_GRAY:
  case PNG_COLOR_TYPE_GRAY_ALPHA:
    pdf_add_array(colorspace, pdf_new_name("CalGray"));
    break;
  }
  pdf_add_array(colorspace, cal_param);

  return colorspace;
}

static pdf_obj *
create_cspace_ICCBased (png_structp png_ptr, png_infop info_ptr)
{
  pdf_obj   *colorspace;
  int        csp_id, colortype;
  png_byte   color_type;
  png_charp  name;
  int        compression_type;  /* Manual page for libpng does not
				 * clarify whether profile data is inflated by libpng.
				 */
#if PNG_LIBPNG_VER_MINOR < 5
  png_charp   profile;
#else
  png_bytep   profile;
#endif
  png_uint_32 proflen;

  if (!png_get_valid(png_ptr, info_ptr, PNG_INFO_iCCP) ||
      !png_get_iCCP(png_ptr, info_ptr, &name, &compression_type, &profile, &proflen))
    return NULL;

  color_type = png_get_color_type(png_ptr, info_ptr);

  if (color_type & PNG_COLOR_MASK_COLOR) {
    colortype = PDF_COLORSPACE_TYPE_RGB;
#if 0
    alternate = create_cspace_CalRGB(png_ptr, info_ptr);
#endif
  } else {
    colortype = PDF_COLORSPACE_TYPE_GRAY;
#if 0
    alternate = create_cspace_CalGray(png_ptr, info_ptr);
#endif
  }

#if 0
  if (alternate)
    pdf_add_dict(dict, pdf_new_name("Alternate"), alternate);
#endif

  if (iccp_check_colorspace(colortype, profile, proflen) < 0)
    colorspace = NULL;
  else {
    csp_id = iccp_load_profile(name, profile, proflen);
    if (csp_id < 0) {
      colorspace = NULL;
    } else {
      colorspace = pdf_get_colorspace_reference(csp_id);
    }
  }

  /* Rendering intent ... */

  return colorspace;
}

/*
 * gAMA, cHRM:
 *
 *   If cHRM is present, we use CIE-Based color space. gAMA is also used here
 * if available.
 */

#define INVALID_CHRM_VALUE(xw,yw,xr,yr,xg,yg,xb,yb) (\
  (xw) <= 0.0 || (yw) < 1.0e-10 || \
  (xr) < 0.0  || (yr) < 0.0 || (xg) < 0.0 || (yg) < 0.0 || \
  (xb) < 0.0  || (yb) < 0.0)

static pdf_obj *
create_cspace_CalRGB (png_structp png_ptr, png_infop info_ptr)
{
  pdf_obj *colorspace;
  pdf_obj *cal_param;
  double   xw, yw, xr, yr, xg, yg, xb, yb;
  double   G;

  if (!png_get_valid(png_ptr, info_ptr, PNG_INFO_cHRM) ||
      !png_get_cHRM(png_ptr, info_ptr, &xw, &yw, &xr, &yr, &xg, &yg, &xb, &yb))
    return NULL;

  if (xw <= 0.0 || yw < 1.0e-10 ||
      xr < 0.0  || yr < 0.0 || xg < 0.0 || yg < 0.0 || xb < 0.0 || yb < 0.0) {
    WARN("%s: Invalid cHRM chunk parameters found.", PNG_DEBUG_STR);
    return NULL;
  }

  if (png_get_valid(png_ptr, info_ptr, PNG_INFO_gAMA) &&
      png_get_gAMA (png_ptr, info_ptr, &G)) {
    if (G < 1.0e-2) {
      WARN("%s: Unusual Gamma value: %g", PNG_DEBUG_STR, G);
      return NULL;
    }
    G = 1.0 / G; /* Gamma is inverted. */
  } else {
    G = 1.0;
  }

  cal_param = make_param_Cal(PNG_COLOR_TYPE_RGB, G, xw, yw, xr, yr, xg, yg, xb, yb);

  if (!cal_param)
    return NULL;

  colorspace = pdf_new_array();
  pdf_add_array(colorspace, pdf_new_name("CalRGB"));
  pdf_add_array(colorspace, cal_param);

  return colorspace;
}

static pdf_obj *
create_cspace_CalGray (png_structp png_ptr, png_infop info_ptr)
{
  pdf_obj *colorspace;
  pdf_obj *cal_param;
  double   xw, yw, xr, yr, xg, yg, xb, yb;
  double   G;

  if (!png_get_valid(png_ptr, info_ptr, PNG_INFO_cHRM) ||
      !png_get_cHRM(png_ptr, info_ptr, &xw, &yw, &xr, &yr, &xg, &yg, &xb, &yb))
    return NULL;

  if (xw <= 0.0 || yw < 1.0e-10 ||
      xr < 0.0  || yr < 0.0 || xg < 0.0 || yg < 0.0 || xb < 0.0 || yb < 0.0) {
    WARN("%s: Invalid cHRM chunk parameters found.", PNG_DEBUG_STR);
    return NULL;
  }

  if (png_get_valid(png_ptr, info_ptr, PNG_INFO_gAMA) &&
      png_get_gAMA (png_ptr, info_ptr, &G)) {
    if (G < 1.0e-2) {
      WARN("%s: Unusual Gamma value: %g", PNG_DEBUG_STR, G);
      return NULL;
    }
    G = 1.0 / G; /* Gamma is inverted. */
  } else {
    G = 1.0;
  }

  cal_param = make_param_Cal(PNG_COLOR_TYPE_GRAY, G, xw, yw, xr, yr, xg, yg, xb, yb);

  if (!cal_param)
    return NULL;

  colorspace = pdf_new_array();
  pdf_add_array(colorspace, pdf_new_name("CalGray"));
  pdf_add_array(colorspace, cal_param);

  return colorspace;
}

static pdf_obj *
make_param_Cal (png_byte color_type,
		double G, /* Gamma */
		double xw, double yw,
		double xr, double yr, double xg, double yg, double xb, double yb)
{
  pdf_obj *cal_param;
  pdf_obj *white_point, *matrix, *dev_gamma;
  double Xw, Yw, Zw; /* Yw = 1.0 */
  double Xr, Xg, Xb, Yr, Yb, Yg, Zr, Zg, Zb;

#ifndef ABS
#define ABS(x) ((x) < 0 ? -(x) : (x))
#endif
  /*
   * TODO: Check validity
   *
   * Conversion found in
   *
   *  com.sixlegs.image.png - Java package to read and display PNG images
   *  Copyright (C) 1998, 1999, 2001 Chris Nokleberg
   *
   *  http://www.sixlegs.com/software/png/
   *
   */
  {
    double zw, zr, zg, zb;
    double fr, fg, fb;
    double det;

    /* WhitePoint */
    zw = 1 - (xw + yw);
    zr = 1 - (xr + yr); zg = 1 - (xg + yg); zb = 1 - (xb + yb);
    Xw = xw / yw; Yw = 1.0; Zw = zw / yw;

    /* Matrix */
    det = xr * (yg * zb - zg * yb) - xg * (yr * zb - zr * yb) + xb * (yr * zg - zr * yg);
    if (ABS(det) < 1.0e-10) {
      WARN("Non invertible matrix: Maybe invalid value(s) specified in cHRM chunk.");
      return NULL;
    }
    fr  = (Xw * (yg * zb - zg * yb) - xg * (zb - Zw * yb) + xb * (zg - Zw * yg)) / det;
    fg  = (xr * (zb - Zw * yb) - Xw * (yr * zb - zr * yb) + xb * (yr * Zw - zr)) / det;
    fb  = (xr * (yg * Zw - zg) - xg * (yr * Zw - zr) + Xw * (yr * zg - zr * yg)) / det;
    Xr = fr * xr; Yr = fr * yr; Zr = fr * zr;
    Xg = fg * xg; Yg = fg * yg; Zg = fg * zg;
    Xb = fb * xb; Yb = fb * yb; Zb = fb * zb;
  }

  if (G < 1.0e-2) {
    WARN("Unusual Gamma specified: %g", G);
    return NULL;
  }

  cal_param = pdf_new_dict();

  /* White point is always required. */
  white_point = pdf_new_array();
  pdf_add_array(white_point, pdf_new_number(ROUND(Xw, 0.00001)));
  pdf_add_array(white_point, pdf_new_number(ROUND(Yw, 0.00001)));
  pdf_add_array(white_point, pdf_new_number(ROUND(Zw, 0.00001)));
  pdf_add_dict(cal_param, pdf_new_name("WhitePoint"), white_point);

  /* Matrix - default: Identity */ 
  if (color_type & PNG_COLOR_MASK_COLOR) {
    if (G != 1.0) {
      dev_gamma = pdf_new_array();
      pdf_add_array(dev_gamma, pdf_new_number(ROUND(G, 0.00001)));
      pdf_add_array(dev_gamma, pdf_new_number(ROUND(G, 0.00001)));
      pdf_add_array(dev_gamma, pdf_new_number(ROUND(G, 0.00001)));
      pdf_add_dict(cal_param, pdf_new_name("Gamma"), dev_gamma);
    }

    matrix = pdf_new_array();
    pdf_add_array(matrix, pdf_new_number(ROUND(Xr, 0.00001)));
    pdf_add_array(matrix, pdf_new_number(ROUND(Yr, 0.00001)));
    pdf_add_array(matrix, pdf_new_number(ROUND(Zr, 0.00001)));
    pdf_add_array(matrix, pdf_new_number(ROUND(Xg, 0.00001)));
    pdf_add_array(matrix, pdf_new_number(ROUND(Yg, 0.00001)));
    pdf_add_array(matrix, pdf_new_number(ROUND(Zg, 0.00001)));
    pdf_add_array(matrix, pdf_new_number(ROUND(Xb, 0.00001)));
    pdf_add_array(matrix, pdf_new_number(ROUND(Yb, 0.00001)));
    pdf_add_array(matrix, pdf_new_number(ROUND(Zb, 0.00001)));
    pdf_add_dict (cal_param, pdf_new_name("Matrix"), matrix);
  } else { /* Gray */
    if (G != 1.0)
      pdf_add_dict(cal_param,
		   pdf_new_name("Gamma"),
		   pdf_new_number(ROUND(G, 0.00001)));
  }

  return cal_param;
}

/*
 * Set up Indexed ColorSpace for color-type PALETTE:
 *
 *  PNG allows only RGB color for base color space. If gAMA and/or cHRM
 *  chunk is available, we can use CalRGB color space instead of DeviceRGB
 *  for base color space.
 *
 */
static pdf_obj *
create_cspace_Indexed (png_structp png_ptr, png_infop info_ptr)
{
  pdf_obj   *colorspace;
  pdf_obj   *base, *lookup;
  png_byte  *data_ptr;
  png_colorp plte;
  int        num_plte, i;

  if (!png_get_valid(png_ptr, info_ptr, PNG_INFO_PLTE) ||
      !png_get_PLTE(png_ptr, info_ptr, &plte, &num_plte)) {
    WARN("%s: PNG does not have valid PLTE chunk.", PNG_DEBUG_STR);
    return NULL;
  }

  /* Order is important. */
  colorspace = pdf_new_array ();
  pdf_add_array(colorspace, pdf_new_name("Indexed"));

  if (png_get_valid(png_ptr, info_ptr, PNG_INFO_iCCP))
    base = create_cspace_ICCBased(png_ptr, info_ptr);
  else {
    if (png_get_valid(png_ptr, info_ptr, PNG_INFO_sRGB))
      base = create_cspace_sRGB(png_ptr, info_ptr);
    else
      base = create_cspace_CalRGB(png_ptr, info_ptr);
  }

  if (!base)
    base = pdf_new_name("DeviceRGB");

  pdf_add_array(colorspace, base);
  pdf_add_array(colorspace, pdf_new_number(num_plte-1));
  data_ptr = NEW(num_plte*3, png_byte);
  for (i = 0; i < num_plte; i++) {
    data_ptr[3*i]   = plte[i].red;
    data_ptr[3*i+1] = plte[i].green;
    data_ptr[3*i+2] = plte[i].blue;
  }
  lookup = pdf_new_string(data_ptr, num_plte*3);
  RELEASE(data_ptr);
  pdf_add_array(colorspace, lookup);

  return colorspace;
}

/*
 * pHYs: no support
 *
 *  pngimage.c is not responsible for adjusting image size.
 *  Higher layer must do something for this.
 */

/*
 * Colorkey Mask: array
 *
 *  [component_0_min component_0_max ... component_n_min component_n_max]
 *
 */

static pdf_obj *
create_ckey_mask (png_structp png_ptr, png_infop info_ptr)
{
  pdf_obj  *colorkeys;
  png_byte  color_type;
  png_bytep trans;
  int       num_trans, i;
  png_color_16p colors;

  if (!png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) ||
      !png_get_tRNS(png_ptr, info_ptr, &trans, &num_trans, &colors)) {
    WARN("%s: PNG does not have valid tRNS chunk!", PNG_DEBUG_STR);
    return NULL;
  }

  colorkeys  = pdf_new_array();
  color_type = png_get_color_type(png_ptr, info_ptr);

  switch (color_type) {
  case PNG_COLOR_TYPE_PALETTE:
    for (i = 0; i < num_trans; i++) {
      if (trans[i] == 0x00) {
	pdf_add_array(colorkeys, pdf_new_number(i));
	pdf_add_array(colorkeys, pdf_new_number(i));
      } else if (trans[i] != 0xff) {
	WARN("%s: You found a bug in pngimage.c.", PNG_DEBUG_STR);
      }
    }
    break;
  case PNG_COLOR_TYPE_RGB:
    pdf_add_array(colorkeys, pdf_new_number(colors->red));
    pdf_add_array(colorkeys, pdf_new_number(colors->red));
    pdf_add_array(colorkeys, pdf_new_number(colors->green));
    pdf_add_array(colorkeys, pdf_new_number(colors->green));
    pdf_add_array(colorkeys, pdf_new_number(colors->blue));
    pdf_add_array(colorkeys, pdf_new_number(colors->blue));
    break;
  case PNG_COLOR_TYPE_GRAY:
    pdf_add_array(colorkeys, pdf_new_number(colors->gray));
    pdf_add_array(colorkeys, pdf_new_number(colors->gray));
    break;
  default:
    WARN("%s: You found a bug in pngimage.c.", PNG_DEBUG_STR);
    pdf_release_obj(colorkeys);
    colorkeys = NULL;
  }

  return colorkeys;
}

/*
 * Soft-Mask: stream
 *
 *   <<
 *      /Type             /XObject
 *      /Subtype          /Image
 *      /Width            -int-
 *      /Height           -int-
 *      /BitsPerComponent bpc
 *   >>
 *   stream .... endstream
 *
 *   ColorSpace, Mask, SMask must be absent. ImageMask must be false or absent.
 */

static pdf_obj *
create_soft_mask (png_structp png_ptr, png_infop info_ptr,
		  png_bytep image_data_ptr, png_uint_32 width, png_uint_32 height)
{
  pdf_obj    *smask, *dict;
  png_bytep   smask_data_ptr;
  png_bytep   trans;
  int         num_trans;
  png_uint_32 i;

  if (!png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) ||
      !png_get_tRNS(png_ptr, info_ptr, &trans, &num_trans, NULL)) {
    WARN("%s: PNG does not have valid tRNS chunk but tRNS is requested.", PNG_DEBUG_STR);
    return NULL;
  }

  smask = pdf_new_stream(STREAM_COMPRESS);
  dict  = pdf_stream_dict(smask);
  smask_data_ptr = (png_bytep) NEW(width*height, png_byte);
  pdf_add_dict(dict, pdf_new_name("Type"),    pdf_new_name("XObjcect"));
  pdf_add_dict(dict, pdf_new_name("Subtype"), pdf_new_name("Image"));
  pdf_add_dict(dict, pdf_new_name("Width"),      pdf_new_number(width));
  pdf_add_dict(dict, pdf_new_name("Height"),     pdf_new_number(height));
  pdf_add_dict(dict, pdf_new_name("ColorSpace"), pdf_new_name("DeviceGray"));
  pdf_add_dict(dict, pdf_new_name("BitsPerComponent"), pdf_new_number(8));
  for (i = 0; i < width*height; i++) {
    png_byte idx = image_data_ptr[i];
    smask_data_ptr[i] = (idx < num_trans) ? trans[idx] : 0xff;
  }
  pdf_add_stream(smask, (char *)smask_data_ptr, width*height);
  RELEASE(smask_data_ptr);

  return smask;
}

/* bitdepth is always 8 (16 is not supported) */
static pdf_obj *
strip_soft_mask (png_structp png_ptr, png_infop info_ptr,
		 /* next two values will be modified. */
		 png_bytep image_data_ptr, png_uint_32p rowbytes_ptr,
		 png_uint_32 width, png_uint_32 height)
{
  pdf_obj    *smask, *dict;
  png_byte    color_type;
  png_bytep   smask_data_ptr;
  png_uint_32 i;

  color_type = png_get_color_type(png_ptr, info_ptr);

  if (color_type & PNG_COLOR_MASK_COLOR) {
    if (*rowbytes_ptr != 4*width*sizeof(png_byte)) { /* Something wrong */
      WARN("%s: Inconsistent rowbytes value.", PNG_DEBUG_STR);
      return NULL;
    }
  } else {
    if (*rowbytes_ptr != 2*width*sizeof(png_byte)) { /* Something wrong */
      WARN("%s: Inconsistent rowbytes value.", PNG_DEBUG_STR);
      return NULL;
    }
  }

  smask = pdf_new_stream(STREAM_COMPRESS);
  dict  = pdf_stream_dict(smask);
  pdf_add_dict(dict, pdf_new_name("Type"),    pdf_new_name("XObjcect"));
  pdf_add_dict(dict, pdf_new_name("Subtype"), pdf_new_name("Image"));
  pdf_add_dict(dict, pdf_new_name("Width"),      pdf_new_number(width));
  pdf_add_dict(dict, pdf_new_name("Height"),     pdf_new_number(height));
  pdf_add_dict(dict, pdf_new_name("ColorSpace"), pdf_new_name("DeviceGray"));
  pdf_add_dict(dict, pdf_new_name("BitsPerComponent"), pdf_new_number(8));

  smask_data_ptr = (png_bytep) NEW(width*height, png_byte);

  switch (color_type) {
  case PNG_COLOR_TYPE_RGB_ALPHA:
    for (i = 0; i < width*height; i++) {
      memmove(image_data_ptr+(3*i), image_data_ptr+(4*i), 3);
      smask_data_ptr[i] = image_data_ptr[4*i+3];
    }
    *rowbytes_ptr = 3*width*sizeof(png_byte);
    break;
  case PNG_COLOR_TYPE_GRAY_ALPHA:
    for (i = 0; i < width*height; i++) {
      image_data_ptr[i] = image_data_ptr[2*i];
      smask_data_ptr[i] = image_data_ptr[2*i+1];
    }
    *rowbytes_ptr = width*sizeof(png_byte);
    break;
  default:
    WARN("You found a bug in pngimage.c!");
    pdf_release_obj(smask);
    RELEASE(smask_data_ptr);
    return NULL;
  }

  pdf_add_stream(smask, smask_data_ptr, width*height);
  RELEASE(smask_data_ptr);

  return smask;
}

static void
read_image_data (png_structp png_ptr, png_infop info_ptr, /* info_ptr unused */
		 png_bytep dest_ptr, png_uint_32 height, png_uint_32 rowbytes)
{
  png_bytepp  rows_p;
  png_uint_32 i;

  rows_p = (png_bytepp) NEW (height, png_bytep);
  for (i=0; i< height; i++)
    rows_p[i] = dest_ptr + (rowbytes * i);
  png_read_image(png_ptr, rows_p);
  RELEASE(rows_p);
}

int
png_get_bbox (FILE *png_file, long *width, long *height,
	       double *xdensity, double *ydensity)
{
  png_structp png_ptr;
  png_infop   png_info_ptr;
  png_uint_32 xppm, yppm;

  rewind (png_file);
  png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
  if (png_ptr == NULL || 
      (png_info_ptr = png_create_info_struct (png_ptr)) == NULL) {
    WARN("%s: Creating Libpng read/info struct failed.", PNG_DEBUG_STR);
    if (png_ptr)
      png_destroy_read_struct(&png_ptr, NULL, NULL);
    return -1;
  }

  /* Inititializing file IO. */
  png_init_io (png_ptr, png_file);

  /* Read PNG info-header and get some info. */
  png_read_info(png_ptr, png_info_ptr);
  *width      = png_get_image_width (png_ptr, png_info_ptr);
  *height     = png_get_image_height(png_ptr, png_info_ptr);
  xppm       = png_get_x_pixels_per_meter(png_ptr, png_info_ptr);
  yppm       = png_get_y_pixels_per_meter(png_ptr, png_info_ptr);

  /* Cleanup */
  if (png_info_ptr)
    png_destroy_info_struct(png_ptr, &png_info_ptr);
  if (png_ptr)
    png_destroy_read_struct(&png_ptr, NULL, NULL);

  if (compat_mode)
    *xdensity = *ydensity = 72.0 / 100.0;
  else {
    *xdensity = xppm ? 72.0 / 0.0254 / xppm : 1.0;
    *ydensity = yppm ? 72.0 / 0.0254 / yppm : 1.0;
  }

  return 0;
}

#endif /* HAVE_LIBPNG */
