]> git.imager.perl.org - imager.git/commitdiff
Merge branch 'png'
authorTony Cook <tony@develop-help.com>
Sun, 29 Apr 2012 04:54:18 +0000 (14:54 +1000)
committerTony Cook <tony@develop-help.com>
Sun, 29 Apr 2012 04:54:18 +0000 (14:54 +1000)
29 files changed:
Changes
MANIFEST
MANIFEST.SKIP
PNG/Changes
PNG/MANIFEST
PNG/MANIFEST.SKIP
PNG/PNG.xs
PNG/impng.c
PNG/impng.h
PNG/t/10png.t
PNG/testimg/badcrc.png [new file with mode: 0644]
PNG/testimg/bilevel.png [new file with mode: 0644]
PNG/testimg/comment.png [new file with mode: 0644]
PNG/testimg/cover.png [new file with mode: 0644]
PNG/testimg/cover16.png [new file with mode: 0644]
PNG/testimg/cover16i.png [new file with mode: 0644]
PNG/testimg/coveri.png [new file with mode: 0644]
PNG/testimg/coverpal.png [new file with mode: 0644]
PNG/testimg/coverpali.png [new file with mode: 0644]
PNG/testimg/gray.png [new file with mode: 0644]
PNG/testimg/graya.png [new file with mode: 0644]
PNG/testimg/pal.png [new file with mode: 0644]
PNG/testimg/paltrans.png [new file with mode: 0644]
PNG/testimg/rgb16.png [new file with mode: 0644]
PNG/testimg/rgb8.png [new file with mode: 0644]
PNG/testimg/rgb8i.png [new file with mode: 0644]
fileformatdocs/pngdump.pl [new file with mode: 0644]
lib/Imager/Files.pod
t/x20spell.t

diff --git a/Changes b/Changes
index 1fe7376596a513207eb7210490747cff25441ad3..48470985d22ed97f129332a32997cf0fbfbc3d07 100644 (file)
--- a/Changes
+++ b/Changes
@@ -69,6 +69,20 @@ Other changes:
  - the i_get_file_background() and i_get_file_backgroundf() APIs now
    return int to indicate whether the i_background tag was found.
 
+ - PNG rework
+   - improve error reporting
+   - add png_interlace, png_bits tags
+   - read paletted images as paletted images, including transparency
+   - read 1 bit greyscale images as a type suitable for other file
+     handlers to write as bilevel
+   - read 16 bit/sample PNG as 16-bit/sample Imager images
+   - write "bilevel" paletted images as 1 bit grayscale images
+   - write paletted images as paletted images
+   - write 16-bit (or higher)/sample images as 16-bit/sample PNG
+     images
+   - improved metadata support
+   https://rt.cpan.org/Ticket/Display.html?id=29268
+
 Imager 0.89 - 18 Mar 2012
 ===========
 
index b33c65b6fd8b0af08727777b8a8de1bdbc0828ee..83b363703b9444ad23be9c16da9b418c06e44a5a 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -225,8 +225,24 @@ PNG/PNG.xs
 PNG/README
 PNG/t/00load.t
 PNG/t/10png.t                  Test png support
+PNG/testimg/badcrc.png
+PNG/testimg/bilevel.png
+PNG/testimg/comment.png
+PNG/testimg/cover.png
+PNG/testimg/cover16.png
+PNG/testimg/cover16i.png
+PNG/testimg/coveri.png
+PNG/testimg/coverpal.png
+PNG/testimg/coverpali.png
+PNG/testimg/gray.png
+PNG/testimg/graya.png
+PNG/testimg/pal.png
 PNG/testimg/palette.png
 PNG/testimg/palette_out.png
+PNG/testimg/paltrans.png
+PNG/testimg/rgb16.png
+PNG/testimg/rgb8.png
+PNG/testimg/rgb8i.png
 pnm.c
 polygon.c
 ppport.h
index 30c026a5e532674b8ea6a08952e00d3a339f28fd..ef04e5dd728e6ac45853fd372f593056e1dc0788 100644 (file)
@@ -96,6 +96,9 @@ Makefile\.old$
 /meta\.tmp$
 ^imconfig\.h$
 
+# generated if we build them in their own directory
+^(PNG|TIFF|FT2|W32|GIF)/blib/
+
 # generated from .im files
 ^combine\.c$
 ^compose\.c$
index eb9f02abe870fafc6438c085ce89ec0cf6547063..52bc4017ba8bbb6618accf1378cbc226b9345df0 100644 (file)
@@ -1,3 +1,7 @@
+
+ - improve error reporting to actually report the error text from
+   libpng.
+
 Imager-File-PNG 0.84
 ====================
 
@@ -39,6 +43,10 @@ Imager-File-PNG 0.79
    plan to continue receiving mail at that address.
    https://rt.cpan.org/Ticket/Display.html?id=68591
 
+ - Makefile.PL updates to report library detection info back to the
+   main Imager Makefile.PL.
+   https://rt.cpan.org/Ticket/Display.html?id=9675
+
 Imager-File-PNG 0.78
 ====================
 
index be3f4e809f6d1cd8814d8193dcf4da6eae6bce21..08579200c90dd443085d4031206946f88fc71391 100644 (file)
@@ -10,5 +10,21 @@ PNG.xs
 README
 t/00load.t
 t/10png.t
+testimg/badcrc.png
+testimg/bilevel.png
+testimg/comment.png
+testimg/cover.png
+testimg/cover16.png
+testimg/cover16i.png
+testimg/coveri.png
+testimg/coverpal.png
+testimg/coverpali.png
+testimg/gray.png
+testimg/graya.png
+testimg/pal.png
 testimg/palette.png
 testimg/palette_out.png
+testimg/paltrans.png
+testimg/rgb16.png
+testimg/rgb8.png
+testimg/rgb8i.png
index c47251f94177a65002b17f9b71d2d2ea2783684a..3051d8c2c7a605dbe6fd54f2dbc669608eba3a44 100644 (file)
@@ -5,10 +5,15 @@
 ^blib/
 ^pm_to_blib$
 ^Makefile$
+^Makefile\.old$
 ^MANIFEST\.bak$
 
 # test products
 ^testout
+\.gcov$
+\.gcda$
+\.gcno$
+
 
 # system
 ^\.svn/
index af235869d1adfdaedeb1d2f8f4ccd110871ee2c2..cc90923abdcb5ed2c412627130a114db21b37b0b 100644 (file)
@@ -22,5 +22,8 @@ i_writepng_wiol(im, ig)
     Imager::ImgRaw     im
         Imager::IO     ig
 
+unsigned
+i_png_lib_version()
+
 BOOT:
        PERL_INITIALIZE_IMAGER_CALLBACKS;
index 5fd41512616014621b2cdec4a1cfc46346254ec2..1df25ca15e1bdea05741fac0e582f9deee8b1abb 100644 (file)
@@ -1,26 +1,6 @@
 #include "impng.h"
 #include "png.h"
-
-/* Check to see if a file is a PNG file using png_sig_cmp().  png_sig_cmp()
- * returns zero if the image is a PNG and nonzero if it isn't a PNG.
- *
- * The function check_if_png() shown here, but not used, returns nonzero (true)
- * if the file can be opened and is a PNG, 0 (false) otherwise.
- *
- * If this call is successful, and you are going to keep the file open,
- * you should call png_set_sig_bytes(png_ptr, PNG_BYTES_TO_CHECK); once
- * you have created the png_ptr, so that libpng knows your application
- * has read that many bytes from the start of the file.  Make sure you
- * don't call png_set_sig_bytes() with more than 8 bytes read or give it
- * an incorrect number of bytes read, or you will either have read too
- * many bytes (your fault), or you are telling libpng to read the wrong
- * number of magic bytes (also your fault).
- *
- * Many applications already read the first 2 or 4 bytes from the start
- * of the image to determine the file type, so it would be easiest just
- * to pass the bytes to png_sig_cmp() or even skip that if you know
- * you have a PNG file, and call png_set_sig_bytes().
- */
+#include <stdlib.h>
 
 /* this is a way to get number of channels from color space 
  * Color code to channel number */
 static int CC2C[PNG_COLOR_MASK_PALETTE|PNG_COLOR_MASK_COLOR|PNG_COLOR_MASK_ALPHA];
 
 #define PNG_BYTES_TO_CHECK 4
 
+static i_img *
+read_direct8(png_structp png_ptr, png_infop info_ptr, int channels, i_img_dim width, i_img_dim height);
+
+static i_img *
+read_direct16(png_structp png_ptr, png_infop info_ptr, int channels, i_img_dim width, i_img_dim height);
+
+static i_img *
+read_paletted(png_structp png_ptr, png_infop info_ptr, int channels, i_img_dim width, i_img_dim height);
+
+static i_img *
+read_bilevel(png_structp png_ptr, png_infop info_ptr, i_img_dim width, i_img_dim height);
+
+static int
+write_direct8(png_structp png_ptr, png_infop info_ptr, i_img *im);
+
+static int
+write_direct16(png_structp png_ptr, png_infop info_ptr, i_img *im);
+
+static int
+write_paletted(png_structp png_ptr, png_infop info_ptr, i_img *im, int bits);
+
+static int
+write_bilevel(png_structp png_ptr, png_infop info_ptr, i_img *im);
+
+static void 
+get_png_tags(i_img *im, png_structp png_ptr, png_infop info_ptr, int bit_depth, int color_type);
+
+static int
+set_png_tags(i_img *im, png_structp png_ptr, png_infop info_ptr);
+
+static const char *
+get_string2(i_img_tags *tags, const char *name, char *buf, size_t *size);
+
+unsigned
+i_png_lib_version(void) {
+  return png_access_version_number();
+}
 
 static void
 wiol_read_data(png_structp png_ptr, png_bytep data, png_size_t length) {
   io_glue *ig = png_get_io_ptr(png_ptr);
-  int rc = i_io_read(ig, data, length);
+  ssize_t rc = i_io_read(ig, data, length);
   if (rc != length) png_error(png_ptr, "Read overflow error on an iolayer source.");
 }
 
 static void
 wiol_write_data(png_structp png_ptr, png_bytep data, png_size_t length) {
-  int rc;
+  ssize_t rc;
   io_glue *ig = png_get_io_ptr(png_ptr);
   rc = i_io_write(ig, data, length);
   if (rc != length) png_error(png_ptr, "Write error on an iolayer source.");
@@ -48,29 +64,44 @@ wiol_write_data(png_structp png_ptr, png_bytep data, png_size_t length) {
 
 static void
 wiol_flush_data(png_structp png_ptr) {
-  /* XXX : This needs to be added to the io layer */
+  io_glue *ig = png_get_io_ptr(png_ptr);
+  if (!i_io_flush(ig))
+    png_error(png_ptr, "Error flushing output");
 }
 
+static void
+error_handler(png_structp png_ptr, png_const_charp msg) {
+  mm_log((1, "PNG error: '%s'\n", msg));
 
-/* Check function demo 
-
-int
-check_if_png(char *file_name, FILE **fp) {
-  char buf[PNG_BYTES_TO_CHECK];
-  if ((*fp = fopen(file_name, "rb")) != NULL) return 0;
-  if (fread(buf, 1, PNG_BYTES_TO_CHECK, *fp) != PNG_BYTES_TO_CHECK) return 0;
-  return(!png_sig_cmp((png_bytep)buf, (png_size_t)0, PNG_BYTES_TO_CHECK));
+  i_push_error(0, msg);
+  longjmp(png_jmpbuf(png_ptr), 1);
 }
+
+/*
+
+  For writing a warning might have information about an error, so send
+  it to the error stack.
+
 */
+static void
+write_warn_handler(png_structp png_ptr, png_const_charp msg) {
+  mm_log((1, "PNG write warning '%s'\n", msg));
+
+  i_push_error(0, msg);
+}
+
+#define PNG_DIM_MAX 0x7fffffffL
 
 undef_int
 i_writepng_wiol(i_img *im, io_glue *ig) {
   png_structp png_ptr;
   png_infop info_ptr = NULL;
-  int width,height,y;
+  i_img_dim width,height,y;
   volatile int cspace,channels;
-  double xres, yres;
-  int aspect_only, have_res;
+  unsigned char *data;
+  unsigned char * volatile vdata = NULL;
+  int bits;
+  int is_bilevel = 0, zero_is_white;
 
   mm_log((1,"i_writepng(im %p ,ig %p)\n", im, ig));
 
@@ -84,15 +115,62 @@ i_writepng_wiol(i_img *im, io_glue *ig) {
   height = im->ysize;
   width  = im->xsize;
 
+  /* if we ever have 64-bit i_img_dim
+   * the libpng docs state that png_set_user_limits() can be used to
+   * override the PNG_USER_*_MAX limits, but as implemented they
+   * don't.  We check against the theoretical limit of PNG here, and
+   * try to override the limits below, in case the libpng
+   * implementation ever matches the documentation.
+   *
+   * https://sourceforge.net/tracker/?func=detail&atid=105624&aid=3314943&group_id=5624
+   * fixed in libpng 1.5.3
+   */
+  if (width > PNG_DIM_MAX || height > PNG_DIM_MAX) {
+    i_push_error(0, "Image too large for PNG");
+    return 0;
+  }
+
   channels=im->channels;
 
-  if (channels > 2) { cspace = PNG_COLOR_TYPE_RGB; channels-=3; }
-  else { cspace=PNG_COLOR_TYPE_GRAY; channels--; }
-  
-  if (channels) cspace|=PNG_COLOR_MASK_ALPHA;
-  mm_log((1,"cspace=%d\n",cspace));
+  if (i_img_is_monochrome(im, &zero_is_white)) {
+    is_bilevel = 1;
+    bits = 1;
+    cspace = PNG_COLOR_TYPE_GRAY;
+    mm_log((1, "i_writepng: bilevel output\n"));
+  }
+  else if (im->type == i_palette_type) {
+    int colors = i_colorcount(im);
+
+    cspace = PNG_COLOR_TYPE_PALETTE;
+    bits = 1;
+    while ((1 << bits) < colors) {
+      bits += bits;
+    }
+    mm_log((1, "i_writepng: paletted output\n"));
+  }
+  else {
+    switch (channels) {
+    case 1:
+      cspace = PNG_COLOR_TYPE_GRAY;
+      break;
+    case 2:
+      cspace = PNG_COLOR_TYPE_GRAY_ALPHA;
+      break;
+    case 3:
+      cspace = PNG_COLOR_TYPE_RGB;
+      break;
+    case 4:
+      cspace = PNG_COLOR_TYPE_RGB_ALPHA;
+      break;
+    default:
+      fprintf(stderr, "Internal error, channels = %d\n", channels);
+      abort();
+    }
+    bits = im->bits > 8 ? 16 : 8;
+    mm_log((1, "i_writepng: direct output\n"));
+  }
 
-  channels = im->channels;
+  mm_log((1,"i_writepng: cspace=%d, bits=%d\n",cspace, bits));
 
   /* Create and initialize the png_struct with the desired error handler
    * functions.  If you want to use the default stderr and longjump method,
@@ -101,7 +179,8 @@ i_writepng_wiol(i_img *im, io_glue *ig) {
    * in case we are using dynamically linked libraries.  REQUIRED.
    */
   
-  png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING,NULL,NULL,NULL);
+  png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, 
+                                   error_handler, write_warn_handler);
   
   if (png_ptr == NULL) return 0;
 
@@ -119,6 +198,8 @@ i_writepng_wiol(i_img *im, io_glue *ig) {
    */
   if (setjmp(png_jmpbuf(png_ptr))) {
     png_destroy_write_struct(&png_ptr, &info_ptr);
+    if (vdata)
+      myfree(vdata);
     return(0);
   }
   
@@ -133,44 +214,43 @@ i_writepng_wiol(i_img *im, io_glue *ig) {
    * currently be PNG_COMPRESSION_TYPE_BASE and PNG_FILTER_TYPE_BASE. REQUIRED
    */
 
-  png_set_IHDR(png_ptr, info_ptr, width, height, 8, cspace,
+  /* by default, libpng (not PNG) limits the image size to a maximum
+   * 1000000 pixels in each direction, but Imager doesn't.
+   * Configure libpng to avoid that limit.
+   */
+  png_set_user_limits(png_ptr, width, height);
+
+  png_set_IHDR(png_ptr, info_ptr, width, height, bits, cspace,
               PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
 
-  have_res = 1;
-  if (i_tags_get_float(&im->tags, "i_xres", 0, &xres)) {
-    if (i_tags_get_float(&im->tags, "i_yres", 0, &yres))
-      ; /* nothing to do */
-    else
-      yres = xres;
+  if (!set_png_tags(im, png_ptr, info_ptr)) {
+    png_destroy_write_struct(&png_ptr, &info_ptr);
+    return 0;
   }
-  else {
-    if (i_tags_get_float(&im->tags, "i_yres", 0, &yres))
-      xres = yres;
-    else
-      have_res = 0;
+
+  if (is_bilevel) {
+    if (!write_bilevel(png_ptr, info_ptr, im)) {
+      png_destroy_write_struct(&png_ptr, &info_ptr);
+      return 0;
+    }
   }
-  if (have_res) {
-    aspect_only = 0;
-    i_tags_get_int(&im->tags, "i_aspect_only", 0, &aspect_only);
-    xres /= 0.0254;
-    yres /= 0.0254;
-    png_set_pHYs(png_ptr, info_ptr, xres + 0.5, yres + 0.5, 
-                 aspect_only ? PNG_RESOLUTION_UNKNOWN : PNG_RESOLUTION_METER);
+  else if (im->type == i_palette_type) {
+    if (!write_paletted(png_ptr, info_ptr, im, bits)) {
+      png_destroy_write_struct(&png_ptr, &info_ptr);
+      return 0;
+    }
   }
-
-  png_write_info(png_ptr, info_ptr);
-
-  if (!im->virtual && im->type == i_direct_type && im->bits == i_8_bits) {
-    for (y = 0; y < height; y++) 
-      png_write_row(png_ptr, (png_bytep) &(im->idata[channels*width*y]));
+  else if (bits == 16) {
+    if (!write_direct16(png_ptr, info_ptr, im)) {
+      png_destroy_write_struct(&png_ptr, &info_ptr);
+      return 0;
+    }
   }
   else {
-    unsigned char *data = mymalloc(im->xsize * im->channels);
-    for (y = 0; y < height; y++) {
-      i_gsamp(im, 0, im->xsize, y, data, NULL, im->channels);
-      png_write_row(png_ptr, (png_bytep)data);
+    if (!write_direct8(png_ptr, info_ptr, im)) {
+      png_destroy_write_struct(&png_ptr, &info_ptr);
+      return 0;
     }
-    myfree(data);
   }
 
   png_write_end(png_ptr, info_ptr);
@@ -183,9 +263,15 @@ i_writepng_wiol(i_img *im, io_glue *ig) {
   return(1);
 }
 
+typedef struct {
+  char *warnings;
+} i_png_read_state, *i_png_read_statep;
 
+static void
+read_warn_handler(png_structp, png_const_charp);
 
-static void get_png_tags(i_img *im, png_structp png_ptr, png_infop info_ptr);
+static void
+cleanup_read_state(i_png_read_statep);
 
 i_img*
 i_readpng_wiol(io_glue *ig) {
@@ -194,20 +280,28 @@ i_readpng_wiol(io_glue *ig) {
   png_infop info_ptr;
   png_uint_32 width, height;
   int bit_depth, color_type, interlace_type;
-  int number_passes,y;
-  int channels,pass;
+  int channels;
   unsigned int sig_read;
+  i_png_read_state rs;
 
+  rs.warnings = NULL;
   sig_read  = 0;
 
   mm_log((1,"i_readpng_wiol(ig %p)\n", ig));
+  i_clear_error();
 
-  png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,NULL,NULL,NULL);
+  png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, &rs, 
+                                  error_handler, read_warn_handler);
+  if (!png_ptr) {
+    i_push_error(0, "Cannot create PNG read structure");
+    return NULL;
+  }
   png_set_read_fn(png_ptr, (png_voidp) (ig), wiol_read_data);
   
   info_ptr = png_create_info_struct(png_ptr);
   if (info_ptr == NULL) {
     png_destroy_read_struct(&png_ptr, (png_infopp)NULL, (png_infopp)NULL);
+    i_push_error(0, "Cannot create PNG info structure");
     return NULL;
   }
   
@@ -215,8 +309,12 @@ i_readpng_wiol(io_glue *ig) {
     if (im) i_img_destroy(im);
     mm_log((1,"i_readpng_wiol: error.\n"));
     png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
+    cleanup_read_state(&rs);
     return NULL;
   }
+  
+  /* we do our own limit checks */
+  png_set_user_limits(png_ptr, PNG_DIM_MAX, PNG_DIM_MAX);
 
   png_set_sig_bytes(png_ptr, sig_read);
   png_read_info(png_ptr, info_ptr);
@@ -241,42 +339,353 @@ i_readpng_wiol(io_glue *ig) {
     return NULL;
   }
 
+  if (color_type == PNG_COLOR_TYPE_PALETTE) {
+    im = read_paletted(png_ptr, info_ptr, channels, width, height);
+  }
+  else if (color_type == PNG_COLOR_TYPE_GRAY
+          && bit_depth == 1
+          && !png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
+    im = read_bilevel(png_ptr, info_ptr, width, height);
+  }
+  else if (bit_depth == 16) {
+    im = read_direct16(png_ptr, info_ptr, channels, width, height);
+  }
+  else {
+    im = read_direct8(png_ptr, info_ptr, channels, width, height);
+  }
+
+  if (im)
+    get_png_tags(im, png_ptr, info_ptr, bit_depth, color_type);
+
+  png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
+
+  if (im) {
+    if (rs.warnings) {
+      i_tags_set(&im->tags, "png_warnings", rs.warnings, -1);
+    }
+  }
+  cleanup_read_state(&rs);
+  
+  mm_log((1,"(%p) <- i_readpng_wiol\n", im));  
+  
+  return im;
+}
+
+static i_img *
+read_direct8(png_structp png_ptr, png_infop info_ptr, int channels,
+            i_img_dim width, i_img_dim height) {
+  i_img * volatile vim = NULL;
+  int color_type = png_get_color_type(png_ptr, info_ptr);
+  int bit_depth = png_get_bit_depth(png_ptr, info_ptr);
+  i_img_dim y;
+  int number_passes, pass;
+  i_img *im;
+  unsigned char *line;
+  unsigned char * volatile vline = NULL;
+
+  if (setjmp(png_jmpbuf(png_ptr))) {
+    if (vim) i_img_destroy(vim);
+    if (vline) myfree(vline);
+
+    return NULL;
+  }
+
+  number_passes = png_set_interlace_handling(png_ptr);
+  mm_log((1,"number of passes=%d\n",number_passes));
+
   png_set_strip_16(png_ptr);
   png_set_packing(png_ptr);
-  if (color_type == PNG_COLOR_TYPE_PALETTE) png_set_expand(png_ptr);
-  if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) png_set_expand(png_ptr);
 
+  if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
+    png_set_expand(png_ptr);
+    
   if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
     channels++;
     mm_log((1, "image has transparency, adding alpha: channels = %d\n", channels));
     png_set_expand(png_ptr);
   }
   
+  png_read_update_info(png_ptr, info_ptr);
+  
+  im = vim = i_img_8_new(width,height,channels);
+  if (!im) {
+    png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
+    return NULL;
+  }
+  
+  line = vline = mymalloc(channels * width);
+  for (pass = 0; pass < number_passes; pass++) {
+    for (y = 0; y < height; y++) {
+      if (pass > 0)
+       i_gsamp(im, 0, width, y, line, NULL, channels);
+      png_read_row(png_ptr,(png_bytep)line, NULL);
+      i_psamp(im, 0, width, y, line, NULL, channels);
+    }
+  }
+  myfree(line);
+  vline = NULL;
+  
+  png_read_end(png_ptr, info_ptr); 
+
+  return im;
+}
+
+static i_img *
+read_direct16(png_structp png_ptr, png_infop info_ptr, int channels,
+            i_img_dim width, i_img_dim height) {
+  i_img * volatile vim = NULL;
+  i_img_dim x, y;
+  int number_passes, pass;
+  i_img *im;
+  unsigned char *line;
+  unsigned char * volatile vline = NULL;
+  unsigned *bits_line;
+  unsigned * volatile vbits_line = NULL;
+  size_t row_bytes;
+
+  if (setjmp(png_jmpbuf(png_ptr))) {
+    if (vim) i_img_destroy(vim);
+    if (vline) myfree(vline);
+    if (vbits_line) myfree(vbits_line);
+
+    return NULL;
+  }
+
   number_passes = png_set_interlace_handling(png_ptr);
   mm_log((1,"number of passes=%d\n",number_passes));
+
+  if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
+    channels++;
+    mm_log((1, "image has transparency, adding alpha: channels = %d\n", channels));
+    png_set_expand(png_ptr);
+  }
+  
   png_read_update_info(png_ptr, info_ptr);
   
-  im = i_img_8_new(width,height,channels);
+  im = vim = i_img_16_new(width,height,channels);
   if (!im) {
     png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
     return NULL;
   }
-
-  for (pass = 0; pass < number_passes; pass++)
-    for (y = 0; y < height; y++) { png_read_row(png_ptr,(png_bytep) &(im->idata[channels*width*y]), NULL); }
+  
+  row_bytes = png_get_rowbytes(png_ptr, info_ptr);
+  line = vline = mymalloc(row_bytes);
+  memset(line, 0, row_bytes);
+  bits_line = vbits_line = mymalloc(sizeof(unsigned) * width * channels);
+  for (pass = 0; pass < number_passes; pass++) {
+    for (y = 0; y < height; y++) {
+      if (pass > 0) {
+       i_gsamp_bits(im, 0, width, y, bits_line, NULL, channels, 16);
+       for (x = 0; x < width * channels; ++x) {
+         line[x*2] = bits_line[x] >> 8;
+         line[x*2+1] = bits_line[x] & 0xff;
+       }
+      }
+      png_read_row(png_ptr,(png_bytep)line, NULL);
+      for (x = 0; x < width * channels; ++x)
+       bits_line[x] = (line[x*2] << 8) + line[x*2+1];
+      i_psamp_bits(im, 0, width, y, bits_line, NULL, channels, 16);
+    }
+  }
+  myfree(line);
+  myfree(bits_line);
+  vline = NULL;
+  vbits_line = NULL;
   
   png_read_end(png_ptr, info_ptr); 
+
+  return im;
+}
+
+static i_img *
+read_bilevel(png_structp png_ptr, png_infop info_ptr,
+            i_img_dim width, i_img_dim height) {
+  i_img * volatile vim = NULL;
+  i_img_dim x, y;
+  int number_passes, pass;
+  i_img *im;
+  unsigned char *line;
+  unsigned char * volatile vline = NULL;
+  i_color palette[2];
+
+  if (setjmp(png_jmpbuf(png_ptr))) {
+    if (vim) i_img_destroy(vim);
+    if (vline) myfree(vline);
+
+    return NULL;
+  }
+
+  number_passes = png_set_interlace_handling(png_ptr);
+  mm_log((1,"number of passes=%d\n",number_passes));
+
+  png_set_packing(png_ptr);
+
+  png_set_expand(png_ptr);  
+  
+  png_read_update_info(png_ptr, info_ptr);
   
-  get_png_tags(im, png_ptr, info_ptr);
+  im = vim = i_img_pal_new(width, height, 1, 256);
+  if (!im) {
+    png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
+    return NULL;
+  }
 
-  png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
+  palette[0].channel[0] = palette[0].channel[1] = palette[0].channel[2] = 
+    palette[0].channel[3] = 0;
+  palette[1].channel[0] = palette[1].channel[1] = palette[1].channel[2] = 
+    palette[1].channel[3] = 255;
+  i_addcolors(im, palette, 2);
   
-  mm_log((1,"(0x%08X) <- i_readpng_scalar\n", im));  
+  line = vline = mymalloc(width);
+  memset(line, 0, width);
+  for (pass = 0; pass < number_passes; pass++) {
+    for (y = 0; y < height; y++) {
+      if (pass > 0) {
+       i_gpal(im, 0, width, y, line);
+       /* expand indexes back to 0/255 */
+       for (x = 0; x < width; ++x)
+         line[x] = line[x] ? 255 : 0;
+      }
+      png_read_row(png_ptr,(png_bytep)line, NULL);
+
+      /* back to palette indexes */
+      for (x = 0; x < width; ++x)
+       line[x] = line[x] ? 1 : 0;
+      i_ppal(im, 0, width, y, line);
+    }
+  }
+  myfree(line);
+  vline = NULL;
   
+  png_read_end(png_ptr, info_ptr); 
+
   return im;
 }
 
-static void get_png_tags(i_img *im, png_structp png_ptr, png_infop info_ptr) {
+/* FIXME: do we need to unscale palette color values from the 
+   supplied alphas? */
+static i_img *
+read_paletted(png_structp png_ptr, png_infop info_ptr, int channels,
+             i_img_dim width, i_img_dim height) {
+  i_img * volatile vim = NULL;
+  int color_type = png_get_color_type(png_ptr, info_ptr);
+  int bit_depth = png_get_bit_depth(png_ptr, info_ptr);
+  i_img_dim y;
+  int number_passes, pass;
+  i_img *im;
+  unsigned char *line;
+  unsigned char * volatile vline = NULL;
+  int num_palette, i;
+  png_colorp png_palette;
+  png_bytep png_pal_trans;
+  png_color_16p png_color_trans;
+  int num_pal_trans;
+
+  if (setjmp(png_jmpbuf(png_ptr))) {
+    if (vim) i_img_destroy(vim);
+    if (vline) myfree(vline);
+
+    return NULL;
+  }
+
+  number_passes = png_set_interlace_handling(png_ptr);
+  mm_log((1,"number of passes=%d\n",number_passes));
+
+  png_set_strip_16(png_ptr);
+  png_set_packing(png_ptr);
+
+  if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
+    png_set_expand(png_ptr);
+    
+  if (!png_get_PLTE(png_ptr, info_ptr, &png_palette, &num_palette)) {
+    i_push_error(0, "Paletted image with no PLTE chunk");
+    return NULL;
+  }
+
+  if (png_get_tRNS(png_ptr, info_ptr, &png_pal_trans, &num_pal_trans,
+                  &png_color_trans)) {
+    channels++;
+  }
+  else {
+    num_pal_trans = 0;
+  }
+  
+  png_read_update_info(png_ptr, info_ptr);
+  
+  im = vim = i_img_pal_new(width, height, channels, 256);
+  if (!im) {
+    png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
+    return NULL;
+  }
+
+  for (i = 0; i < num_palette; ++i) {
+    i_color c;
+
+    c.rgba.r = png_palette[i].red;
+    c.rgba.g = png_palette[i].green;
+    c.rgba.b = png_palette[i].blue;
+    if (i < num_pal_trans)
+      c.rgba.a = png_pal_trans[i];
+    else
+      c.rgba.a = 255;
+    i_addcolors(im, &c, 1);
+  }
+
+  line = vline = mymalloc(width);
+  for (pass = 0; pass < number_passes; pass++) {
+    for (y = 0; y < height; y++) {
+      if (pass > 0)
+       i_gpal(im, 0, width, y, line);
+      png_read_row(png_ptr,(png_bytep)line, NULL);
+      i_ppal(im, 0, width, y, line);
+    }
+  }
+  myfree(line);
+  vline = NULL;
+  
+  png_read_end(png_ptr, info_ptr); 
+
+  return im;
+}
+
+struct png_text_name {
+  const char *keyword;
+  const char *tagname;
+};
+
+static const struct png_text_name
+text_tags[] = {
+  { "Author", "png_author" },
+  { "Comment", "i_comment" },
+  { "Copyright", "png_copyright" },
+  { "Creation Time", "png_creation_time" },
+  { "Description", "png_description" },
+  { "Disclaimer", "png_disclaimer" },
+  { "Software", "png_software" },
+  { "Source", "png_source" },
+  { "Title", "png_title" },
+  { "Warning", "png_warning" }
+};
+
+static const int text_tags_count = sizeof(text_tags) / sizeof(*text_tags);
+
+static const char * const
+chroma_tags[] = {
+  "png_chroma_white_x",
+  "png_chroma_white_y",
+  "png_chroma_red_x",
+  "png_chroma_red_y",
+  "png_chroma_green_x",
+  "png_chroma_green_y",
+  "png_chroma_blue_x",
+  "png_chroma_blue_y"
+};
+
+static const int chroma_tag_count = sizeof(chroma_tags) / sizeof(*chroma_tags);
+
+static void
+get_png_tags(i_img *im, png_structp png_ptr, png_infop info_ptr,
+            int bit_depth, int color_type) {
   png_uint_32 xres, yres;
   int unit_type;
 
@@ -293,4 +702,633 @@ static void get_png_tags(i_img *im, png_structp png_ptr, png_infop info_ptr) {
       i_tags_setn(&im->tags, "i_aspect_only", 1);
     }
   }
+  {
+    int interlace = png_get_interlace_type(png_ptr, info_ptr);
+
+    i_tags_setn(&im->tags, "png_interlace", interlace != PNG_INTERLACE_NONE);
+    switch (interlace) {
+    case PNG_INTERLACE_NONE:
+      i_tags_set(&im->tags, "png_interlace_name", "none", -1);
+      break;
+      
+    case PNG_INTERLACE_ADAM7:
+      i_tags_set(&im->tags, "png_interlace_name", "adam7", -1);
+      break;
+      
+    default:
+      i_tags_set(&im->tags, "png_interlace_name", "unknown", -1);
+      break;
+    }
+  }
+
+  /* the various readers can call png_set_expand(), libpng will make
+     it's internal record of bit_depth at least 8 in that case */
+  i_tags_setn(&im->tags, "png_bits", bit_depth);
+  
+  if (png_get_valid(png_ptr, info_ptr, PNG_INFO_sRGB)) {
+    int intent;
+    if (png_get_sRGB(png_ptr, info_ptr, &intent)) {
+      i_tags_setn(&im->tags, "png_srgb_intent", intent);
+    }
+  }
+  else {
+    /* Ignore these if there's an sRGB chunk, libpng simulates
+       their existence if there's an sRGB chunk, and the PNG spec says
+       that these are ignored if the sRGB is present, so ignore them.
+    */
+    double gamma;
+    double chroma[8];
+
+    if (png_get_gAMA(png_ptr, info_ptr, &gamma)) {
+      i_tags_set_float2(&im->tags, "png_gamma", 0, gamma, 4);
+    }
+
+    if (png_get_cHRM(png_ptr, info_ptr, chroma+0, chroma+1,
+                    chroma+2, chroma+3, chroma+4, chroma+5,
+                    chroma+6, chroma+7)) {
+      int i;
+
+      for (i = 0; i < chroma_tag_count; ++i)
+       i_tags_set_float2(&im->tags, chroma_tags[i], 0, chroma[i], 4);
+    }
+  }
+
+  {
+    int num_text;
+    png_text *text;
+
+    if (png_get_text(png_ptr, info_ptr, &text, &num_text)) {
+      int i;
+      int custom_index = 0;
+      for (i = 0; i < num_text; ++i) {
+       int j;
+       int found = 0;
+       int compressed = text[i].compression == PNG_ITXT_COMPRESSION_zTXt
+         || text[i].compression == PNG_TEXT_COMPRESSION_zTXt;
+
+       for (j = 0; j < text_tags_count; ++j) {
+         if (strcmp(text_tags[j].keyword, text[i].key) == 0) {
+           char tag_name[50];
+           i_tags_set(&im->tags, text_tags[j].tagname, text[i].text, -1);
+           if (compressed) {
+             sprintf(tag_name, "%s_compressed", text_tags[j].tagname);
+             i_tags_setn(&im->tags, tag_name, 1);
+           }
+           found = 1;
+           break;
+         }
+       }
+
+       if (!found) {
+         char tag_name[50];
+         sprintf(tag_name, "png_text%d_key", custom_index);
+         i_tags_set(&im->tags, tag_name, text[i].key, -1);
+         sprintf(tag_name, "png_text%d_text", custom_index);
+         i_tags_set(&im->tags, tag_name, text[i].text, -1);
+         sprintf(tag_name, "png_text%d_type", custom_index);
+         i_tags_set(&im->tags, tag_name, 
+                    (text[i].compression == PNG_TEXT_COMPRESSION_NONE
+                     || text[i].compression == PNG_TEXT_COMPRESSION_zTXt) ?
+                    "text" : "itxt", -1);
+         if (compressed) {
+           sprintf(tag_name, "png_text%d_compressed", custom_index);
+           i_tags_setn(&im->tags, tag_name, 1);
+         }
+         ++custom_index;
+       }
+      }
+    }
+  }
+
+  {
+    png_time *mod_time;
+
+    if (png_get_tIME(png_ptr, info_ptr, &mod_time)) {
+      char time_formatted[80];
+
+      sprintf(time_formatted, "%d-%02d-%02dT%02d:%02d:%02d",
+             mod_time->year, mod_time->month, mod_time->day,
+             mod_time->hour, mod_time->minute, mod_time->second);
+      i_tags_set(&im->tags, "png_time", time_formatted, -1);
+    }
+  }
+
+  {
+    png_color_16 *back;
+    i_color c;
+
+    if (png_get_bKGD(png_ptr, info_ptr, &back)) {
+      switch (color_type) {
+      case PNG_COLOR_TYPE_GRAY:
+      case PNG_COLOR_TYPE_GRAY_ALPHA:
+       {
+         /* lib png stores the raw gray value rather than scaling it
+            to 16-bit (or 8), we use 8-bit color for i_background */
+
+         int gray;
+         switch (bit_depth) {
+         case 16:
+           gray = back->gray >> 8;
+           break;
+         case 8:
+           gray = back->gray;
+           break;
+         case 4:
+           gray = 0x11 * back->gray;
+           break;
+         case 2:
+           gray = 0x55 * back->gray;
+           break;
+         case 1:
+           gray = back->gray ? 0xFF : 0;
+           break;
+         default:
+           gray = 0;
+         }
+         c.rgb.r = c.rgb.g = c.rgb.b = gray;
+         break;
+       }
+
+      case PNG_COLOR_TYPE_RGB:
+      case PNG_COLOR_TYPE_RGB_ALPHA:
+       {
+         c.rgb.r = bit_depth == 16 ? (back->red   >> 8) : back->red;
+         c.rgb.g = bit_depth == 16 ? (back->green >> 8) : back->green;
+         c.rgb.b = bit_depth == 16 ? (back->blue  >> 8) : back->blue;
+         break;
+       }
+
+      case PNG_COLOR_TYPE_PALETTE:
+       c.rgb.r = back->red;
+       c.rgb.g = back->green;
+       c.rgb.b = back->blue;
+       break;
+      }
+
+      c.rgba.a = 255;
+      i_tags_set_color(&im->tags, "i_background", 0, &c);
+    }
+  }
+}
+
+#define GET_STR_BUF_SIZE 40
+
+static int
+set_png_tags(i_img *im, png_structp png_ptr, png_infop info_ptr) {
+  double xres, yres;
+  int aspect_only, have_res = 1;
+
+  if (i_tags_get_float(&im->tags, "i_xres", 0, &xres)) {
+    if (i_tags_get_float(&im->tags, "i_yres", 0, &yres))
+      ; /* nothing to do */
+    else
+      yres = xres;
+  }
+  else {
+    if (i_tags_get_float(&im->tags, "i_yres", 0, &yres))
+      xres = yres;
+    else
+      have_res = 0;
+  }
+  if (have_res) {
+    aspect_only = 0;
+    i_tags_get_int(&im->tags, "i_aspect_only", 0, &aspect_only);
+    xres /= 0.0254;
+    yres /= 0.0254;
+    png_set_pHYs(png_ptr, info_ptr, xres + 0.5, yres + 0.5, 
+                 aspect_only ? PNG_RESOLUTION_UNKNOWN : PNG_RESOLUTION_METER);
+  }
+
+  {
+    int intent;
+    if (i_tags_get_int(&im->tags, "png_srgb_intent", 0, &intent)) {
+      if (intent < 0 || intent >= PNG_sRGB_INTENT_LAST) {
+       i_push_error(0, "tag png_srgb_intent out of range");
+       return 0;
+      }
+      png_set_sRGB(png_ptr, info_ptr, intent);
+    }
+    else {
+      double chroma[8], gamma;
+      int i;
+      int found_chroma_count = 0;
+
+      for (i = 0; i < chroma_tag_count; ++i) {
+       if (i_tags_get_float(&im->tags, chroma_tags[i], 0, chroma+i))
+         ++found_chroma_count;
+      }
+
+      if (found_chroma_count) {
+       if (found_chroma_count != chroma_tag_count) {
+         i_push_error(0, "all png_chroma_* tags must be supplied or none");
+         return 0;
+       }
+
+       png_set_cHRM(png_ptr, info_ptr, chroma[0], chroma[1], chroma[2],
+                    chroma[3], chroma[4], chroma[5], chroma[6], chroma[7]);
+      }
+
+      if (i_tags_get_float(&im->tags, "png_gamma", 0, &gamma)) {
+       png_set_gAMA(png_ptr, info_ptr, gamma);
+      }
+    }
+  }
+
+  {
+    /* png_set_text() is sparsely documented, it isn't indicated whether
+       multiple calls add to or replace the lists of texts, and
+       whether the text/keyword data is copied or not.
+
+       Examining the linpng code reveals that png_set_text() adds to
+       the list and that the text is copied.
+    */
+    int i;
+
+    /* do our standard tags */
+    for (i = 0; i < text_tags_count; ++i) {
+      char buf[GET_STR_BUF_SIZE];
+      size_t size;
+      const char *data;
+      
+      data = get_string2(&im->tags, text_tags[i].tagname, buf, &size);
+      if (data) {
+       png_text text;
+       int compression = size > 1000;
+       char compress_tag[40];
+
+       if (memchr(data, '\0',  size)) {
+         i_push_errorf(0, "tag %s may not contain NUL characters", text_tags[i].tagname);
+         return 0;
+       }
+      
+       sprintf(compress_tag, "%s_compressed", text_tags[i].tagname);
+       i_tags_get_int(&im->tags, compress_tag, 0, &compression);
+       
+       text.compression = compression ? PNG_TEXT_COMPRESSION_zTXt
+         : PNG_TEXT_COMPRESSION_NONE;
+       text.key = (char *)text_tags[i].keyword;
+       text.text_length = size;
+       text.text = (char *)data;
+#ifdef PNG_iTXt_SUPPORTED
+       text.itxt_length = 0;
+       text.lang = NULL;
+       text.lang_key = NULL;
+#endif
+
+       png_set_text(png_ptr, info_ptr, &text, 1);
+      }
+    }
+
+    /* for non-standard tags ensure keywords are limited to 1 to 79
+       characters */
+    i = 0;
+    while (1) {
+      char tag_name[50];
+      char key_buf[GET_STR_BUF_SIZE], value_buf[GET_STR_BUF_SIZE];
+      const char *key, *value;
+      size_t key_size, value_size;
+
+      sprintf(tag_name, "png_text%d_key", i);
+      key = get_string2(&im->tags, tag_name, key_buf, &key_size);
+      
+      if (key) {
+       size_t k;
+       if (key_size < 1 || key_size > 79) {
+         i_push_errorf(0, "tag %s must be between 1 and 79 characters in length", tag_name);
+         return 0;
+       }
+
+       if (key[0] == ' ' || key[key_size-1] == ' ') {
+         i_push_errorf(0, "tag %s may not contain leading or trailing spaces", tag_name);
+         return 0;
+       }
+
+       if (strstr(key, "  ")) {
+         i_push_errorf(0, "tag %s may not contain consecutive spaces", tag_name);
+         return 0;
+       }
+
+       for (k = 0; k < key_size; ++k) {
+         if (key[k] < 32 || key[k] > 126 && key[k] < 161) {
+           i_push_errorf(0, "tag %s may only contain Latin1 characters 32-126, 161-255", tag_name);
+           return 0;
+         }
+       }
+      }
+
+      sprintf(tag_name, "png_text%d_text", i);
+      value = get_string2(&im->tags, tag_name, value_buf, &value_size);
+
+      if (value) {
+       if (memchr(value, '\0', value_size)) {
+         i_push_errorf(0, "tag %s may not contain NUL characters", tag_name);
+         return 0;
+       }
+      }
+
+      if (key && value) {
+       png_text text;
+       int compression = value_size > 1000;
+
+       sprintf(tag_name, "png_text%d_compressed", i);
+       i_tags_get_int(&im->tags, tag_name, 0, &compression);
+
+       text.compression = compression ? PNG_TEXT_COMPRESSION_zTXt
+         : PNG_TEXT_COMPRESSION_NONE;
+       text.key = (char *)key;
+       text.text_length = value_size;
+       text.text = (char *)value;
+#ifdef PNG_iTXt_SUPPORTED
+       text.itxt_length = 0;
+       text.lang = NULL;
+       text.lang_key = NULL;
+#endif
+
+       png_set_text(png_ptr, info_ptr, &text, 1);
+      }
+      else if (key) {
+       i_push_errorf(0, "tag png_text%d_key found but not png_text%d_text", i, i);
+       return 0;
+      }
+      else if (value) {
+       i_push_errorf(0, "tag png_text%d_text found but not png_text%d_key", i, i);
+       return 0;
+      }
+      else {
+       break;
+      }
+      ++i;
+    }
+  }
+
+  {
+    char buf[GET_STR_BUF_SIZE];
+    size_t time_size;
+    const char *timestr = get_string2(&im->tags, "png_time", buf, &time_size);
+
+    if (timestr) {
+      int year, month, day, hour, minute, second;
+      png_time mod_time;
+
+      if (sscanf(timestr, "%d-%d-%dT%d:%d:%d", &year, &month, &day, &hour, &minute, &second) == 6) {
+       /* rough validation */
+       if (month < 1 || month > 12
+           || day < 1 || day > 31
+           || hour < 0 || hour > 23
+           || minute < 0 || minute > 59
+           || second < 0 || second > 60) {
+         i_push_error(0, "invalid date/time for png_time");
+         return 0;
+       }
+       mod_time.year = year;
+       mod_time.month = month;
+       mod_time.day = day;
+       mod_time.hour = hour;
+       mod_time.minute = minute;
+       mod_time.second = second;
+
+       png_set_tIME(png_ptr, info_ptr, &mod_time);
+      }
+      else {
+       i_push_error(0, "png_time must be formatted 'y-m-dTh:m:s'");
+       return 0;
+      }
+    }
+  }
+
+  {
+    /* no bKGD support yet, maybe later
+       it may be simpler to do it in the individual writers
+     */
+  }
+
+  return 1;
+}
+
+static const char *
+get_string2(i_img_tags *tags, const char *name, char *buf, size_t *size) {
+  int index;
+
+  if (i_tags_find(tags, name, 0, &index)) {
+    const i_img_tag *entry = tags->tags + index;
+    
+    if (entry->data) {
+      *size = entry->size;
+
+      return entry->data;
+    }
+    else {
+      *size = sprintf(buf, "%d", entry->idata);
+
+      return buf;
+    }
+  }
+  return NULL;
+}
+
+static int
+write_direct8(png_structp png_ptr, png_infop info_ptr, i_img *im) {
+  unsigned char *data, *volatile vdata = NULL;
+  i_img_dim y;
+
+  if (setjmp(png_jmpbuf(png_ptr))) {
+    if (vdata)
+      myfree(vdata);
+
+    return 0;
+  }
+
+  png_write_info(png_ptr, info_ptr);
+
+  vdata = data = mymalloc(im->xsize * im->channels);
+  for (y = 0; y < im->ysize; y++) {
+    i_gsamp(im, 0, im->xsize, y, data, NULL, im->channels);
+    png_write_row(png_ptr, (png_bytep)data);
+  }
+  myfree(data);
+
+  return 1;
+}
+
+static int
+write_direct16(png_structp png_ptr, png_infop info_ptr, i_img *im) {
+  unsigned *data, *volatile vdata = NULL;
+  unsigned char *tran_data, * volatile vtran_data = NULL;
+  i_img_dim samples_per_row = im->xsize * im->channels;
+  
+  i_img_dim y;
+
+  if (setjmp(png_jmpbuf(png_ptr))) {
+    if (vdata)
+      myfree(vdata);
+    if (vtran_data)
+      myfree(vtran_data);
+
+    return 0;
+  }
+
+  png_write_info(png_ptr, info_ptr);
+
+  vdata = data = mymalloc(samples_per_row * sizeof(unsigned));
+  vtran_data = tran_data = mymalloc(samples_per_row * 2);
+  for (y = 0; y < im->ysize; y++) {
+    i_img_dim i;
+    unsigned char *p = tran_data;
+    i_gsamp_bits(im, 0, im->xsize, y, data, NULL, im->channels, 16);
+    for (i = 0; i < samples_per_row; ++i) {
+      p[0] = data[i] >> 8;
+      p[1] = data[i] & 0xff;
+      p += 2;
+    }
+    png_write_row(png_ptr, (png_bytep)tran_data);
+  }
+  myfree(tran_data);
+  myfree(data);
+
+  return 1;
+}
+
+static int
+write_paletted(png_structp png_ptr, png_infop info_ptr, i_img *im, int bits) {
+  unsigned char *data, *volatile vdata = NULL;
+  i_img_dim y;
+  unsigned char pal_map[256];
+  png_color pcolors[256];
+  i_color colors[256];
+  int count = i_colorcount(im);
+  int i;
+
+  if (setjmp(png_jmpbuf(png_ptr))) {
+    if (vdata)
+      myfree(vdata);
+
+    return 0;
+  }
+
+  i_getcolors(im, 0, colors, count);
+  if (im->channels < 3) {
+    /* convert the greyscale palette to color */
+    int i;
+    for (i = 0; i < count; ++i) {
+      i_color *c = colors + i;
+      c->channel[3] = c->channel[1];
+      c->channel[2] = c->channel[1] = c->channel[0];
+    }
+  }
+
+  if (i_img_has_alpha(im)) {
+    int i;
+    int bottom_index = 0, top_index = count-1;
+
+    /* fill out the palette map */
+    for (i = 0; i < count; ++i)
+      pal_map[i] = i;
+
+    /* the PNG spec suggests sorting the palette by alpha, but that's
+       unnecessary - all we want to do is move the opaque entries to
+       the end */
+    while (bottom_index < top_index) {
+      if (colors[bottom_index].rgba.a == 255) {
+       pal_map[bottom_index] = top_index;
+       pal_map[top_index--] = bottom_index;
+      }
+      ++bottom_index;
+    }
+  }
+
+  for (i = 0; i < count; ++i) {
+    int srci = i_img_has_alpha(im) ? pal_map[i] : i;
+
+    pcolors[i].red = colors[srci].rgb.r;
+    pcolors[i].green = colors[srci].rgb.g;
+    pcolors[i].blue = colors[srci].rgb.b;
+  }
+
+  png_set_PLTE(png_ptr, info_ptr, pcolors, count);
+
+  if (i_img_has_alpha(im)) {
+    unsigned char trans[256];
+    int i;
+
+    for (i = 0; i < count && colors[pal_map[i]].rgba.a != 255; ++i) {
+      trans[i] = colors[pal_map[i]].rgba.a;
+    }
+    png_set_tRNS(png_ptr, info_ptr, trans, i, NULL);
+  }
+
+  png_write_info(png_ptr, info_ptr);
+
+  png_set_packing(png_ptr);
+
+  vdata = data = mymalloc(im->xsize);
+  for (y = 0; y < im->ysize; y++) {
+    i_gpal(im, 0, im->xsize, y, data);
+    if (i_img_has_alpha(im)) {
+      i_img_dim x;
+      for (x = 0; x < im->xsize; ++x)
+       data[x] = pal_map[data[x]];
+    }
+    png_write_row(png_ptr, (png_bytep)data);
+  }
+  myfree(data);
+
+  return 1;
+}
+
+static int
+write_bilevel(png_structp png_ptr, png_infop info_ptr, i_img *im) {
+  unsigned char *data, *volatile vdata = NULL;
+  i_img_dim y;
+
+  if (setjmp(png_jmpbuf(png_ptr))) {
+    if (vdata)
+      myfree(vdata);
+
+    return 0;
+  }
+
+  png_write_info(png_ptr, info_ptr);
+
+  png_set_packing(png_ptr);
+
+  vdata = data = mymalloc(im->xsize);
+  for (y = 0; y < im->ysize; y++) {
+    i_gsamp(im, 0, im->xsize, y, data, NULL, 1);
+    png_write_row(png_ptr, (png_bytep)data);
+  }
+  myfree(data);
+
+  return 1;
+}
+
+static void
+read_warn_handler(png_structp png_ptr, png_const_charp msg) {
+  i_png_read_statep rs = (i_png_read_statep)png_get_error_ptr(png_ptr);
+  char *workp;
+  size_t new_size;
+
+  mm_log((1, "PNG read warning '%s'\n", msg));
+
+  /* in case this is part of an error report */
+  i_push_error(0, msg);
+  
+  /* and save in the warnings so if we do manage to succeed, we 
+   * can save it as a tag
+   */
+  new_size = (rs->warnings ? strlen(rs->warnings) : 0)
+    + 1 /* NUL */
+    + strlen(msg) /* new text */
+    + 1; /* newline */
+  workp = myrealloc(rs->warnings, new_size);
+  if (!rs->warnings)
+    *workp = '\0';
+  strcat(workp, msg);
+  strcat(workp, "\n");
+  rs->warnings = workp;
+}
+
+static void
+cleanup_read_state(i_png_read_statep rs) {
+  if (rs->warnings)
+    myfree(rs->warnings);
 }
index afd5af1bbbe7c0bb9941cf7113f6e16cca627ec9..ed4f180df99250206f4d18731ed438f79409e7f4 100644 (file)
@@ -5,5 +5,6 @@
 
 i_img    *i_readpng_wiol(io_glue *ig);
 undef_int i_writepng_wiol(i_img *im, io_glue *ig);
+unsigned i_png_lib_version(void);
 
 #endif
index 4077a99e68a487315694ef8725ef239af5f8e486..8b332d34ff7af0eed589ae8e3b25f524295dd89f 100644 (file)
@@ -2,16 +2,20 @@
 use strict;
 use Imager qw(:all);
 use Test::More;
-use Imager::Test qw(test_image_raw test_image);
+use Imager::Test qw(test_image_raw test_image is_image is_imaged test_image_16 test_image_double);
+
+my $debug_writes = 1;
 
 -d "testout" or mkdir "testout";
 
 init_log("testout/t102png.log",1);
 
-$Imager::formats{"png"}
-  or plan skip_all => "No png support";
+plan tests => 248;
+
+# this loads Imager::File::PNG too
+ok($Imager::formats{"png"}, "must have png format");
 
-plan tests => 35;
+diag("Library version " . Imager::File::PNG::i_png_lib_version());
 
 my $green  = i_color_new(0,   255, 0,   255);
 my $blue   = i_color_new(0,   0,   255, 255);
@@ -31,7 +35,8 @@ Imager::i_tags_add($img, "i_yres", 0, undef, 200);
 open(FH,">testout/t102.png") || die "cannot open testout/t102.png for writing\n";
 binmode(FH);
 my $IO = Imager::io_new_fd(fileno(FH));
-ok(Imager::File::PNG::i_writepng_wiol($img, $IO), "write");
+ok(Imager::File::PNG::i_writepng_wiol($img, $IO), "write")
+  or diag(Imager->_error_as_msg());
 close(FH);
 
 open(FH,"testout/t102.png") || die "cannot open testout/t102.png\n";
@@ -151,6 +156,7 @@ EOS
   my $im = test_image();
   my $fail_close = sub {
     Imager::i_push_error(0, "synthetic close failure");
+    print "# closecb called\n";
     return 0;
   };
   ok(!$im->write(type => "png", callback => sub { 1 },
@@ -165,3 +171,619 @@ EOS
   ok(grep($_ eq 'png', Imager->write_types), "check png in write types");
 }
 
+{ # read error reporting
+  my $im = Imager->new;
+  ok(!$im->read(file => "testimg/badcrc.png", type => "png"),
+     "read png with bad CRC chunk should fail");
+  is($im->errstr, "IHDR: CRC error", "check error message");
+}
+
+{ # write error reporting
+  my $im = test_image();
+  ok(!$im->write(type => "png", callback => limited_write(1), buffered => 0),
+     "write limited to 1 byte should fail");
+  is($im->errstr, "Write error on an iolayer source.: limit reached",
+     "check error message");
+}
+
+SKIP:
+{ # https://sourceforge.net/tracker/?func=detail&aid=3314943&group_id=5624&atid=105624
+  # large images
+  Imager::File::PNG::i_png_lib_version() >= 10503
+      or skip("older libpng limits image sizes", 12);
+
+  {
+    my $im = Imager->new(xsize => 1000001, ysize => 1, channels => 1);
+    ok($im, "make a wide image");
+    my $data;
+    ok($im->write(data => \$data, type => "png"),
+       "write wide image as png")
+      or diag("write wide: " . $im->errstr);
+    my $im2 = Imager->new;
+    ok($im->read(data => $data, type => "png"),
+       "read wide image as png")
+      or diag("read wide: " . $im->errstr);
+    is($im->getwidth, 1000001, "check width");
+    is($im->getheight, 1, "check height");
+    is($im->getchannels, 1, "check channels");
+  }
+
+  {
+    my $im = Imager->new(xsize => 1, ysize => 1000001, channels => 1);
+    ok($im, "make a tall image");
+    my $data;
+    ok($im->write(data => \$data, type => "png"),
+       "write wide image as png")
+      or diag("write tall: " . $im->errstr);
+    my $im2 = Imager->new;
+    ok($im->read(data => $data, type => "png"),
+       "read tall image as png")
+      or diag("read tall: " . $im->errstr);
+    is($im->getwidth, 1, "check width");
+    is($im->getheight, 1000001, "check height");
+    is($im->getchannels, 1, "check channels");
+  }
+}
+
+{ # test grayscale read as greyscale
+  my $im = Imager->new;
+  ok($im->read(file => "testimg/gray.png", type => "png"),
+     "read grayscale");
+  is($im->getchannels, 1, "check channel count");
+  is($im->type, "direct", "check type");
+  is($im->bits, 8, "check bits");
+  is($im->tags(name => "png_bits"), 8, "check png_bits tag");
+  is($im->tags(name => "png_interlace"), 0, "check png_interlace tag");
+}
+
+{ # test grayscale + alpha read as greyscale + alpha
+  my $im = Imager->new;
+  ok($im->read(file => "testimg/graya.png", type => "png"),
+     "read grayscale + alpha");
+  is($im->getchannels, 2, "check channel count");
+  is($im->type, "direct", "check type");
+  is($im->bits, 8, "check bits");
+  is($im->tags(name => "png_bits"), 8, "check png_bits tag");
+  is($im->tags(name => "png_interlace"), 0, "check png_interlace tag");
+}
+
+{ # test paletted + alpha read as paletted
+  my $im = Imager->new;
+  ok($im->read(file => "testimg/paltrans.png", type => "png"),
+     "read paletted with alpha");
+  is($im->getchannels, 4, "check channel count");
+  is($im->type, "paletted", "check type");
+  is($im->tags(name => "png_bits"), 8, "check png_bits tag");
+  is($im->tags(name => "png_interlace"), 0, "check png_interlace tag");
+}
+
+{ # test paletted read as paletted
+  my $im = Imager->new;
+  ok($im->read(file => "testimg/pal.png", type => "png"),
+     "read paletted");
+  is($im->getchannels, 3, "check channel count");
+  is($im->type, "paletted", "check type");
+  is($im->tags(name => "png_bits"), 8, "check png_bits tag");
+  is($im->tags(name => "png_interlace"), 0, "check png_interlace tag");
+}
+
+{ # test 16-bit rgb read as 16 bit
+  my $im = Imager->new;
+  ok($im->read(file => "testimg/rgb16.png", type => "png"),
+     "read 16-bit rgb");
+  is($im->getchannels, 3, "check channel count");
+  is($im->type, "direct", "check type");
+  is($im->tags(name => "png_interlace"), 0, "check png_interlace tag");
+  is($im->bits, 16, "check bits");
+  is($im->tags(name => "png_bits"), 16, "check png_bits tag");
+}
+
+{ # test 1-bit grey read as mono
+  my $im = Imager->new;
+  ok($im->read(file => "testimg/bilevel.png", type => "png"),
+     "read bilevel png");
+  is($im->getchannels, 1, "check channel count");
+  is($im->tags(name => "png_interlace"), 0, "check png_interlace tag");
+  is($im->type, "paletted", "check type");
+  ok($im->is_bilevel, "should be bilevel");
+  is($im->tags(name => "png_bits"), 1, "check png_bits tag");
+}
+
+SKIP:
+{ # test interlaced read as interlaced and matches original
+  my $im_i = Imager->new(file => "testimg/rgb8i.png", filetype => "png");
+  ok($im_i, "read interlaced")
+    or skip("Could not read rgb8i.png: " . Imager->errstr, 7);
+  is($im_i->getchannels, 3, "check channel count");
+  is($im_i->type, "direct", "check type");
+  is($im_i->tags(name => "png_bits"), 8, "check png_bits");
+  is($im_i->tags(name => "png_interlace"), 1, "check png_interlace");
+
+  my $im = Imager->new(file => "testimg/rgb8.png", filetype => "png");
+  ok($im, "read non-interlaced")
+    or skip("Could not read testimg/rgb8.png: " . Imager->errstr, 2);
+  is($im->tags(name => "png_interlace"), 0, "check png_interlace");
+  is_image($im_i, $im, "compare interlaced and non-interlaced");
+}
+
+{
+  my @match =
+    (
+     [ "cover.png", "coveri.png" ],
+     [ "cover16.png", "cover16i.png" ],
+     [ "coverpal.png", "coverpali.png" ],
+    );
+  for my $match (@match) {
+    my ($normal, $interlace) = @$match;
+
+    my $n_im = Imager->new(file => "testimg/$normal");
+    ok($n_im, "read $normal")
+      or diag "reading $normal: ", Imager->errstr;
+    my $i_im = Imager->new(file => "testimg/$interlace");
+    ok($i_im, "read $interlace")
+      or diag "reading $interlace: ", Imager->errstr;
+  SKIP:
+    {
+      $n_im && $i_im
+       or skip("Couldn't read a file", 1);
+      is_image($i_im, $n_im, "check normal and interlace files read the same");
+    }
+  }
+}
+
+{
+  my $interlace = 0;
+  for my $name ("cover.png", "coveri.png") {
+  SKIP: {
+      my $im = Imager->new(file => "testimg/$name");
+      ok($im, "read $name")
+       or diag "Failed to read $name: ", Imager->errstr;
+      $im
+       or skip("Couldn't load $name", 5);
+      is($im->tags(name => "i_format"), "png", "$name: i_format");
+      is($im->tags(name => "png_bits"), 8, "$name: png_bits");
+      is($im->tags(name => "png_interlace"), $interlace,
+        "$name: png_interlace");
+      is($im->getchannels, 4, "$name: four channels");
+      is($im->type, "direct", "$name: direct type");
+
+      is_deeply([ $im->getsamples(y => 0, width => 5) ],
+               [ ( 255, 255, 0, 255 ), ( 255, 255, 0, 191 ),
+                 ( 255, 255, 0, 127 ), ( 255, 255, 0, 63 ),
+                 ( 0, 0, 0, 0) ],
+               "$name: check expected samples row 0");
+      is_deeply([ $im->getsamples(y => 1, width => 5) ],
+               [ ( 255, 0, 0, 255 ), ( 255, 0, 0, 191 ),
+                 ( 255, 0, 0, 127 ), ( 255, 0, 0, 63 ),
+                 ( 0, 0, 0, 0) ],
+               "$name: check expected samples row 1");
+    }
+    $interlace = 1;
+  }
+}
+
+{
+  my $interlace = 0;
+  for my $name ("coverpal.png", "coverpali.png") {
+  SKIP: {
+      my $im = Imager->new(file => "testimg/$name");
+      ok($im, "read $name")
+       or diag "Failed to read $name: ", Imager->errstr;
+      $im
+       or skip("Couldn't load $name", 5);
+      is($im->tags(name => "i_format"), "png", "$name: i_format");
+      is($im->tags(name => "png_bits"), 4, "$name: png_bits");
+      is($im->tags(name => "png_interlace"), $interlace,
+        "$name: png_interlace");
+      is($im->getchannels, 4, "$name: four channels");
+      is($im->type, "paletted", "$name: paletted type");
+
+      is_deeply([ $im->getsamples(y => 0, width => 5) ],
+               [ ( 255, 255, 0, 255 ), ( 255, 255, 0, 191 ),
+                 ( 255, 255, 0, 127 ), ( 255, 255, 0, 63 ),
+                 ( 0, 0, 0, 0) ],
+               "$name: check expected samples row 0");
+      is_deeply([ $im->getsamples(y => 1, width => 5) ],
+               [ ( 255, 0, 0, 255 ), ( 255, 0, 0, 191 ),
+                 ( 255, 0, 0, 127 ), ( 255, 0, 0, 63 ),
+                 ( 0, 0, 0, 0) ],
+               "$name: check expected samples row 1");
+    }
+    $interlace = 1;
+  }
+}
+
+{
+  my $interlace = 0;
+  for my $name ("cover16.png", "cover16i.png") {
+  SKIP: {
+      my $im = Imager->new(file => "testimg/$name");
+      ok($im, "read $name")
+       or diag "Failed to read $name: ", Imager->errstr;
+      $im
+       or skip("Couldn't load $name", 5);
+      is($im->tags(name => "i_format"), "png", "$name: i_format");
+      is($im->tags(name => "png_bits"), 16, "$name: png_bits");
+      is($im->tags(name => "png_interlace"), $interlace,
+        "$name: png_interlace");
+      is($im->getchannels, 4, "$name: four channels");
+      is($im->type, "direct", "$name: direct type");
+
+      is_deeply([ $im->getsamples(y => 0, width => 5, type => "16bit") ],
+               [ ( 65535, 65535, 0, 65535 ), ( 65535, 65535, 0, 49087 ),
+                 ( 65535, 65535, 0, 32639 ), ( 65535, 65535, 0, 16191 ),
+                 ( 65535, 65535, 65535, 0) ],
+               "$name: check expected samples row 0");
+      is_deeply([ $im->getsamples(y => 1, width => 5, type => "16bit") ],
+               [ ( 65535, 0, 0, 65535 ), ( 65535, 0, 0, 49087 ),
+                 ( 65535, 0, 0, 32639 ), ( 65535, 0, 0, 16191 ),
+                 ( 65535, 65535, 65535, 0) ],
+               "$name: check expected samples row 1");
+    }
+    $interlace = 1;
+  }
+}
+
+{
+  my $pim = Imager->new(xsize => 5, ysize => 2, channels => 3, type => "paletted");
+  ok($pim, "make a 3 channel paletted image");
+  ok($pim->addcolors(colors => [ qw(000000 FFFFFF FF0000 00FF00 0000FF) ]),
+     "add some colors");
+  is($pim->setscanline(y => 0, type => "index",
+                      pixels => [ 0, 1, 2, 4, 3 ]), 5, "set some pixels");
+  is($pim->setscanline(y => 1, type => "index",
+                      pixels => [ 4, 1, 0, 4, 2 ]), 5, "set some more pixels");
+  ok($pim->write(file => "testout/pal3.png"),
+     "write to testout/pal3.png")
+    or diag("Cannot save testout/pal3.png: ".$pim->errstr);
+  my $in = Imager->new(file => "testout/pal3.png");
+  ok($in, "read it back in")
+    or diag("Cann't read pal3.png back: " . Imager->errstr);
+  is_image($pim, $in, "check it matches");
+  is($in->type, "paletted", "make sure the result is paletted");
+  is($in->tags(name => "png_bits"), 4, "4 bit representation");
+}
+
+{
+  # make sure the code that pushes maxed alpha to the end doesn't break
+  my $pim = Imager->new(xsize => 8, ysize => 2, channels => 4, type => "paletted");
+  ok($pim, "make a 4 channel paletted image");
+  ok($pim->addcolors
+     (colors => [ NC(255, 255, 0, 128), qw(000000 FFFFFF FF0000 00FF00 0000FF),
+                 NC(0, 0, 0, 0), NC(255, 0, 128, 64) ]),
+     "add some colors");
+  is($pim->setscanline(y => 0, type => "index",
+                      pixels => [ 5, 0, 1, 7, 2, 4, 6, 3 ]), 8,
+     "set some pixels");
+  is($pim->setscanline(y => 1, type => "index",
+                      pixels => [ 7, 4, 6, 1, 0, 4, 2, 5 ]), 8,
+     "set some more pixels");
+  ok($pim->write(file => "testout/pal4.png"),
+     "write to testout/pal4.png")
+    or diag("Cannot save testout/pal4.png: ".$pim->errstr);
+  my $in = Imager->new(file => "testout/pal4.png");
+  ok($in, "read it back in")
+    or diag("Cann't read pal4.png back: " . Imager->errstr);
+  is_image($pim, $in, "check it matches");
+  is($in->type, "paletted", "make sure the result is paletted");
+  is($in->tags(name => "png_bits"), 4, "4 bit representation");
+}
+
+{
+  my $pim = Imager->new(xsize => 8, ysize => 2, channels => 1, type => "paletted");
+  ok($pim, "make a 1 channel paletted image");
+  ok($pim->addcolors(colors => [ map NC($_, 0, 0), 0, 7, 127, 255 ]),
+     "add some colors^Wgreys");
+  is($pim->setscanline(y => 0, type => "index",
+                      pixels => [ 0, 2, 1, 3, 2, 1, 0, 3 ]), 8,
+     "set some pixels");
+  is($pim->setscanline(y => 1, type => "index",
+                      pixels => [ 3, 0, 2, 1, 0, 0, 2, 3 ]), 8,
+     "set some more pixels");
+  ok($pim->write(file => "testout/pal1.png"),
+     "write to testout/pal1.png")
+    or diag("Cannot save testout/pal1.png: ".$pim->errstr);
+  my $in = Imager->new(file => "testout/pal1.png");
+  ok($in, "read it back in")
+    or diag("Cann't read pal1.png back: " . Imager->errstr);
+  # PNG doesn't have a paletted greyscale type, so it's written as
+  # paletted color, convert our source image for the comparison
+  my $cmpim = $pim->convert(preset => "rgb");
+  is_image($in, $cmpim, "check it matches");
+  is($in->type, "paletted", "make sure the result is paletted");
+  is($in->tags(name => "png_bits"), 2, "2 bit representation");
+}
+
+{
+  my $pim = Imager->new(xsize => 8, ysize => 2, channels => 2, type => "paletted");
+  ok($pim, "make a 2 channel paletted image");
+  ok($pim->addcolors(colors => [ NC(0, 255, 0), NC(128, 255, 0), NC(255, 255, 0), NC(128, 128, 0) ]),
+     "add some colors^Wgreys")
+    or diag("adding colors: " . $pim->errstr);
+  is($pim->setscanline(y => 0, type => "index",
+                      pixels => [ 0, 2, 1, 3, 2, 1, 0, 3 ]), 8,
+     "set some pixels");
+  is($pim->setscanline(y => 1, type => "index",
+                      pixels => [ 3, 0, 2, 1, 0, 0, 2, 3 ]), 8,
+     "set some more pixels");
+  ok($pim->write(file => "testout/pal2.png"),
+     "write to testout/pal2.png")
+    or diag("Cannot save testout/pal2.png: ".$pim->errstr);
+  my $in = Imager->new(file => "testout/pal2.png");
+  ok($in, "read it back in")
+    or diag("Can't read pal1.png back: " . Imager->errstr);
+  # PNG doesn't have a paletted greyscale type, so it's written as
+  # paletted color, convert our source image for the comparison
+  my $cmpim = $pim->convert(preset => "rgb");
+  is_image($in, $cmpim, "check it matches");
+  is($in->type, "paletted", "make sure the result is paletted");
+  is($in->tags(name => "png_bits"), 2, "2 bit representation");
+}
+
+{
+  my $imbase = test_image();
+  my $mono = $imbase->convert(preset => "gray")
+    ->to_paletted(make_colors => "mono", translate => "errdiff");
+
+  ok($mono->write(file => "testout/bilevel.png"),
+     "write bilevel.png");
+  my $in = Imager->new(file => "testout/bilevel.png");
+  ok($in, "read it back in")
+    or diag("Can't read bilevel.png: " . Imager->errstr);
+  is_image($in, $mono, "check it matches");
+  is($in->type, "paletted", "make sure the result is paletted");
+  is($in->tags(name => "png_bits"), 1, "1 bit representation");
+}
+
+SKIP:
+{
+  my $im = test_image_16();
+  ok($im->write(file => "testout/rgb16.png", type => "png"),
+     "write 16-bit/sample image")
+    or diag("Could not write rgb16.png: ".$im->errstr);
+  my $in = Imager->new(file => "testout/rgb16.png")
+    or diag("Could not read rgb16.png: ".Imager->errstr);
+  ok($in, "read rgb16.png back in")
+    or skip("Could not load image to check", 4);
+  is_imaged($in, $im, 0, "check image matches");
+  is($in->bits, 16, "check we got a 16-bit image");
+  is($in->type, "direct", "check it's direct");
+  is($in->tags(name => "png_bits"), 16, "check png_bits");
+}
+
+SKIP:
+{
+  my $im = test_image_double();
+  my $cmp = $im->to_rgb16;
+  ok($im->write(file => "testout/rgbdbl.png", type => "png"),
+     "write double/sample image - should write as 16-bit/sample")
+    or diag("Could not write rgbdbl.png: ".$im->errstr);
+  my $in = Imager->new(file => "testout/rgbdbl.png")
+    or diag("Could not read rgbdbl.png: ".Imager->errstr);
+  ok($in, "read pngdbl.png back in")
+    or skip("Could not load image to check", 4);
+  is_imaged($in, $cmp, 0, "check image matches");
+  is($in->bits, 16, "check we got a 16-bit image");
+  is($in->type, "direct", "check it's direct");
+  is($in->tags(name => "png_bits"), 16, "check png_bits");
+}
+
+SKIP:
+{
+  my $im = Imager->new(file => "testimg/comment.png");
+  ok($im, "read file with comment")
+    or diag("Cannot read comment.png: ".Imager->errstr);
+  $im
+    or skip("Cannot test tags file I can't read", 5);
+  is($im->tags(name => "i_comment"), "Test comment", "check i_comment");
+  is($im->tags(name => "png_interlace"), "0", "no interlace");
+  is($im->tags(name => "png_interlace_name"), "none", "no interlace (text)");
+  is($im->tags(name => "png_srgb_intent"), "0", "srgb perceptual");
+  is($im->tags(name => "png_time"), "2012-04-16T07:37:36",
+     "modification time");
+  is($im->tags(name => "i_background"), "color(255,255,255,255)",
+     "background color");
+}
+
+SKIP:
+{ # test tag writing
+  my $im = Imager->new(xsize => 1, ysize => 1);
+  ok($im->write(file => "testout/tags.png",
+               i_comment => "A Comment",
+               png_author => "An Author",
+               png_author_compressed => 1,
+               png_copyright => "A Copyright",
+               png_creation_time => "16 April 2012 22:56:30+1000",
+               png_description => "A Description",
+               png_disclaimer => "A Disclaimer",
+               png_software => "Some Software",
+               png_source => "A Source",
+               png_title => "A Title",
+               png_warning => "A Warning",
+               png_text0_key => "Custom Key",
+               png_text0_text => "Custom Value",
+               png_text0_compressed => 1,
+               png_text1_key => "Custom Key2",
+               png_text1_text => "Another Custom Value",
+               png_time => "2012-04-20T00:15:10",
+              ),
+     "write with many tags")
+    or diag("Cannot write with many tags: ", $im->errstr);
+
+  my $imr = Imager->new(file => "testout/tags.png");
+  ok($imr, "read it back in")
+    or skip("Couldn't read it back: ". Imager->errstr, 1);
+
+  is_deeply({ map @$_, $imr->tags },
+           {
+            i_format => "png",
+            i_comment => "A Comment",
+            png_author => "An Author",
+            png_author_compressed => 1,
+            png_copyright => "A Copyright",
+            png_creation_time => "16 April 2012 22:56:30+1000",
+            png_description => "A Description",
+            png_disclaimer => "A Disclaimer",
+            png_software => "Some Software",
+            png_source => "A Source",
+            png_title => "A Title",
+            png_warning => "A Warning",
+            png_text0_key => "Custom Key",
+            png_text0_text => "Custom Value",
+            png_text0_compressed => 1,
+            png_text0_type => "text",
+            png_text1_key => "Custom Key2",
+            png_text1_text => "Another Custom Value",
+            png_text1_type => "text",
+            png_time => "2012-04-20T00:15:10",
+            png_interlace => 0,
+            png_interlace_name => "none",
+            png_bits => 8,
+           }, "check tags are what we expected");
+}
+
+SKIP:
+{ # cHRM test
+  my $im = Imager->new(xsize => 1, ysize => 1);
+  ok($im->write(file => "testout/tagschrm.png", type => "png",
+               png_chroma_white_x => 0.3,
+               png_chroma_white_y => 0.32,
+               png_chroma_red_x => 0.7,
+               png_chroma_red_y => 0.28,
+               png_chroma_green_x => 0.075,
+               png_chroma_green_y => 0.8,
+               png_chroma_blue_x => 0.175,
+               png_chroma_blue_y => 0.05),
+     "write cHRM chunk");
+  my $imr = Imager->new(file => "testout/tagschrm.png", ftype => "png");
+  ok($imr, "read tagschrm.png")
+    or diag("reading tagschrm.png: ".Imager->errstr);
+  $imr
+    or skip("read of tagschrm.png failed", 1);
+  is_deeply({ map @$_, $imr->tags },
+           {
+            i_format => "png",
+            png_interlace => 0,
+            png_interlace_name => "none",
+            png_bits => 8,
+            png_chroma_white_x => 0.3,
+            png_chroma_white_y => 0.32,
+            png_chroma_red_x => 0.7,
+            png_chroma_red_y => 0.28,
+            png_chroma_green_x => 0.075,
+            png_chroma_green_y => 0.8,
+            png_chroma_blue_x => 0.175,
+            png_chroma_blue_y => 0.05,
+           }, "check chroma tags written");
+}
+
+{ # gAMA
+  my $im = Imager->new(xsize => 1, ysize => 1);
+  ok($im->write(file => "testout/tagsgama.png", type => "png",
+              png_gamma => 2.22),
+     "write with png_gammma tag");
+  my $imr = Imager->new(file => "testout/tagsgama.png", ftype => "png");
+  ok($imr, "read tagsgama.png")
+    or diag("reading tagsgama.png: ".Imager->errstr);
+  $imr
+    or skip("read of tagsgama.png failed", 1);
+  is_deeply({ map @$_, $imr->tags },
+           {
+            i_format => "png",
+            png_interlace => 0,
+            png_interlace_name => "none",
+            png_bits => 8,
+            png_gamma => "2.22",
+           }, "check gamma tag written");
+}
+
+{ # various bad tag failures
+  my @tests =
+    (
+     [
+      [ png_chroma_white_x => 0.5 ],
+      "all png_chroma_* tags must be supplied or none"
+     ],
+     [
+      [ png_srgb_intent => 4 ],
+      "tag png_srgb_intent out of range"
+     ],
+     [
+      [ i_comment => "test\0with nul" ],
+      "tag i_comment may not contain NUL characters"
+     ],
+     [
+      [ png_text0_key => "" ],
+      "tag png_text0_key must be between 1 and 79 characters in length"
+     ],
+     [
+      [ png_text0_key => ("x" x 80) ],
+      "tag png_text0_key must be between 1 and 79 characters in length"
+     ],
+     [
+      [ png_text0_key => " x" ],
+      "tag png_text0_key may not contain leading or trailing spaces"
+     ],
+     [
+      [ png_text0_key => "x " ],
+      "tag png_text0_key may not contain leading or trailing spaces"
+     ],
+     [
+      [ png_text0_key => "x  y" ],
+      "tag png_text0_key may not contain consecutive spaces"
+     ],
+     [
+      [ png_text0_key => "\x7F" ],
+      "tag png_text0_key may only contain Latin1 characters 32-126, 161-255"
+     ],
+     [
+      [ png_text0_key => "x", png_text0_text => "a\0b" ],
+      "tag png_text0_text may not contain NUL characters"
+     ],
+     [
+      [ png_text0_key => "test" ],
+      "tag png_text0_key found but not png_text0_text"
+     ],
+     [
+      [ png_text0_text => "test" ],
+      "tag png_text0_text found but not png_text0_key"
+     ],
+     [
+      [ png_time => "bad format" ],
+      "png_time must be formatted 'y-m-dTh:m:s'"
+     ],
+     [
+      [ png_time => "2012-13-01T00:00:00" ],
+      "invalid date/time for png_time"
+     ],
+    ); 
+  my $im = Imager->new(xsize => 1, ysize => 1);
+  for my $test (@tests) {
+    my ($tags, $error) = @$test;
+    my $im2 = $im->copy;
+    my $data;
+    ok(!$im2->write(data => \$data, type => "png", @$tags),
+       "expect $error");
+    is($im2->errstr, $error, "check error message");
+  }
+}
+
+sub limited_write {
+  my ($limit) = @_;
+
+  return
+     sub {
+       my ($data) = @_;
+       $limit -= length $data;
+       if ($limit >= 0) {
+         print "# write of ", length $data, " bytes successful ($limit left)\n" if $debug_writes;
+         return 1;
+       }
+       else {
+         print "# write of ", length $data, " bytes failed\n";
+         Imager::i_push_error(0, "limit reached");
+         return;
+       }
+     };
+}
+
diff --git a/PNG/testimg/badcrc.png b/PNG/testimg/badcrc.png
new file mode 100644 (file)
index 0000000..36f6d6b
Binary files /dev/null and b/PNG/testimg/badcrc.png differ
diff --git a/PNG/testimg/bilevel.png b/PNG/testimg/bilevel.png
new file mode 100644 (file)
index 0000000..7be3a21
Binary files /dev/null and b/PNG/testimg/bilevel.png differ
diff --git a/PNG/testimg/comment.png b/PNG/testimg/comment.png
new file mode 100644 (file)
index 0000000..0faf2c2
Binary files /dev/null and b/PNG/testimg/comment.png differ
diff --git a/PNG/testimg/cover.png b/PNG/testimg/cover.png
new file mode 100644 (file)
index 0000000..a13a2e0
Binary files /dev/null and b/PNG/testimg/cover.png differ
diff --git a/PNG/testimg/cover16.png b/PNG/testimg/cover16.png
new file mode 100644 (file)
index 0000000..15278ba
Binary files /dev/null and b/PNG/testimg/cover16.png differ
diff --git a/PNG/testimg/cover16i.png b/PNG/testimg/cover16i.png
new file mode 100644 (file)
index 0000000..8b3907b
Binary files /dev/null and b/PNG/testimg/cover16i.png differ
diff --git a/PNG/testimg/coveri.png b/PNG/testimg/coveri.png
new file mode 100644 (file)
index 0000000..61273da
Binary files /dev/null and b/PNG/testimg/coveri.png differ
diff --git a/PNG/testimg/coverpal.png b/PNG/testimg/coverpal.png
new file mode 100644 (file)
index 0000000..3bfe909
Binary files /dev/null and b/PNG/testimg/coverpal.png differ
diff --git a/PNG/testimg/coverpali.png b/PNG/testimg/coverpali.png
new file mode 100644 (file)
index 0000000..b0321d0
Binary files /dev/null and b/PNG/testimg/coverpali.png differ
diff --git a/PNG/testimg/gray.png b/PNG/testimg/gray.png
new file mode 100644 (file)
index 0000000..b81838f
Binary files /dev/null and b/PNG/testimg/gray.png differ
diff --git a/PNG/testimg/graya.png b/PNG/testimg/graya.png
new file mode 100644 (file)
index 0000000..3eaae35
Binary files /dev/null and b/PNG/testimg/graya.png differ
diff --git a/PNG/testimg/pal.png b/PNG/testimg/pal.png
new file mode 100644 (file)
index 0000000..2fc0fe7
Binary files /dev/null and b/PNG/testimg/pal.png differ
diff --git a/PNG/testimg/paltrans.png b/PNG/testimg/paltrans.png
new file mode 100644 (file)
index 0000000..6d7c3b3
Binary files /dev/null and b/PNG/testimg/paltrans.png differ
diff --git a/PNG/testimg/rgb16.png b/PNG/testimg/rgb16.png
new file mode 100644 (file)
index 0000000..232a6f0
Binary files /dev/null and b/PNG/testimg/rgb16.png differ
diff --git a/PNG/testimg/rgb8.png b/PNG/testimg/rgb8.png
new file mode 100644 (file)
index 0000000..0eb3060
Binary files /dev/null and b/PNG/testimg/rgb8.png differ
diff --git a/PNG/testimg/rgb8i.png b/PNG/testimg/rgb8i.png
new file mode 100644 (file)
index 0000000..ecac1a9
Binary files /dev/null and b/PNG/testimg/rgb8i.png differ
diff --git a/fileformatdocs/pngdump.pl b/fileformatdocs/pngdump.pl
new file mode 100644 (file)
index 0000000..f2dcf24
--- /dev/null
@@ -0,0 +1,341 @@
+#!perl
+use strict;
+use IO::Uncompress::Inflate qw(inflate);
+use Getopt::Long;
+
+my $dumpall = 0;
+my $image = 0;
+GetOptions(dumpall => \$dumpall,
+          image => \$image);
+
+my $file = shift
+  or die "Usage: $0 filename\n";
+
+open my $fh, "<", $file
+  or die "$0: cannot open '$file': $!\n";
+
+binmode $fh;
+
+my $head;
+read($fh, $head, 8) == 8
+  or die "Cann't read header: $!\n";
+
+my $offset = 0;
+dump_data($offset, $head);
+print "  Header\n";
+$offset += length $head;
+unless ($head eq "\x89PNG\x0d\x0A\cZ\x0A") {
+  die "Header isn't a PNG header\n";
+}
+
+my $colour_type;
+my $bits;
+my $sline_len;
+my $sline_left = 0;
+my $row = 0;
+while (my ($dlen, $data, $len, $type, $payload, $crc) = read_chunk($fh)) {
+  dump_data($offset, $data);
+  $offset += $dlen;
+  my $calc_crc = crc($type . $payload);
+  my $src_crc = unpack("N", $crc);
+
+  $type =~ s/([^ -\x7f])/sprintf("\\x%02x", ord $1)/ge;
+  print "  Type: $type\n";
+  print "  Length: $len\n";
+  printf "  CRC: %x (calculated: %x)\n", $src_crc, $calc_crc;
+  if ($type eq 'IHDR') {
+    my ($w, $h, $d, $ct, $comp, $filter, $inter) =
+      unpack("NNCCCCC", $payload);
+    print <<EOS;
+  Width : $w
+  Height: $h
+  Depth: $d
+  Colour type: $ct
+  Filter: $filter
+  Interlace: $inter
+EOS
+    $colour_type = $ct;
+    $bits = $d;
+    my $channels = $ct == 2 ? 3 : $ct == 4 ? 2 : $ct == 6 ? 4 : 1;
+    my $bitspp = $channels * $d;
+    $sline_len = int((($w * $bitspp) + 7) / 8);
+    ++$sline_len; # filter byte
+    print "  Line length: $sline_len\n";
+  }
+  elsif ($type eq 'sRGB') {
+    print "  Rendering intent: ", ord($payload), "\n";
+  }
+  elsif ($type eq 'PLTE') {
+    my $index = 0;
+    while ($index * 3 < $len) {
+      my @rgb = unpack("CCC", substr($payload, $index * 3, 3));
+      print "  $index: @rgb\n";
+      ++$index;
+    }
+  }
+  elsif ($type eq 'tRNS') {
+    if ($colour_type == 0) {
+      my $g = unpack("n", $payload);
+      printf "  Grey: %d (%x)\n", $g, $g;
+    }
+    elsif ($colour_type == 2) {
+      my @rgb = unpack("nnn", $payload);
+      printf "  RGB: %d, %d, %d (%x, %x, %x)\n", @rgb, @rgb;
+    }
+    elsif ($colour_type == 3) {
+      my $index = 0;
+      for my $alpha (unpack("C*", $payload)) {
+       print "  Index: $index: $alpha\n";
+       ++$index;
+      }
+    }
+    else {
+      print "  Unexpected tRNS for colour type $colour_type\n";
+    }
+  }
+  elsif ($type eq 'pHYs') {
+    my ($hres, $vres, $unit) = unpack("NNC", $payload);
+    my $unitname = $unit == 1 ? "metre" : "unknown";
+    print <<EOS;
+  hRes: $hres / $unitname
+  vRes: $vres / $unitname
+  Unit: $unit ($unitname)
+EOS
+  }
+  elsif ($type eq 'tEXt') {
+    my ($key, $value) = split /\0/, $payload, 2;
+    print <<EOS;
+  Keyword: $key
+  Value: $value
+EOS
+    do_more_text($key, $value);
+  }
+  elsif ($type eq 'zTXt') {
+    my ($key, $rest) = split /\0/, $payload, 2;
+    my $ctype = ord $rest;
+    my $ztxt = substr($rest, 1);
+    my $value = do_inflate($ztxt);
+    print <<EOS;
+  Keyword: $key
+  Value: $value
+EOS
+    do_more_text($key, $value);
+  }
+  elsif ($type eq 'tIME') {
+    my @when = unpack("nCCCCC", $payload);
+    printf "  Date: %d-%02d-%02d %02d:%02d:%02d\n", @when;
+  }
+  elsif ($type eq 'bKGD') {
+    if ($colour_type == 2 || $colour_type == 6) {
+      my @rgb = unpack("nnn", $payload);
+      printf "  Background: rgb$bits(%d,%d,%d)\n", @rgb;
+    }
+    elsif ($colour_type == 0 || $colour_type == 4) {
+      my $g = unpack("n", $payload);
+      printf "  Background: grey$bits(%d)\n", $g;
+    }
+    if ($colour_type == 3) {
+      my $index = unpack("C", $payload);
+      printf "  Background: index(%d)\n", $index;
+    }
+  }
+  elsif ($type eq "IDAT" && $image) {
+    $sline_len
+      or die "IDAT before IHDR!?";
+    my $raw = do_inflate($payload);
+    if ($sline_left) {
+      print "  Continuing $row:\n";
+      print "  ", unpack("H*", substr($raw, 0, $sline_left, "")), "\n";
+      $sline_left = 0;
+      ++$row;
+    }
+    while (length $raw >= $sline_len) {
+      my $row_data = substr($raw, 0, $sline_len, "");
+      my ($filter, $data) = unpack("CH*", $row_data);
+      print "  Row $row, filter $filter\n";
+      print "    $data\n";
+      ++$row;
+    }
+    if (length $raw) {
+      $sline_left = $sline_len - length $raw;
+      my ($filter, $data) = unpack("CH*", $raw);
+      print "  Row $row, filter $filter (partial)\n";
+      print "    $data\n" if length $data;
+    }
+  }
+
+  $type eq "IEND"
+    and last;
+}
+
+sub do_more_text {
+  my ($key, $text) = @_;
+
+  if ($key eq 'Raw profile type xmp'
+     && $text =~ s/^\s*xmp\s+\d+\s+//) {
+    print "  XMP: ", pack("H*", join('', split ' ', $text)), "\n";
+  }
+}
+
+sub read_chunk {
+  my ($fh) = @_;
+
+  my $rlen;
+  read($fh, $rlen, 4)
+    or die "Cannot read chunk length\n";
+  my $len = unpack("N", $rlen);
+  my $type;
+  read($fh, $type, 4)
+    or die "Cannot read chunk type\n";
+  my $payload = "";
+  if ($rlen) {
+    read($fh, $payload, $len) == $len
+      or die "Cannot read payload\n";
+  }
+  my $crc;
+  read($fh, $crc, 4) == 4
+    or die "Cannot read CRC\n";
+
+  return ( $len + 12, $rlen . $type . $payload . $crc, $len, $type, $payload, $crc );
+}
+
+sub dump_data {
+  my ($offset, $data) = @_;
+
+  if (length $data > 28) {
+    if ($dumpall) {
+      for my $i (0 .. int((15 + length $data) / 16) - 1) {
+       my $row = substr($data, $i * 16, 16);
+       (my $clean = $row) =~ tr/ -~/./c;
+       printf("%08x: %-32s %s\n", $offset, unpack("H*", $row), $clean);
+      }
+    }
+    else {
+      printf "%08x: %s...\n", $offset, unpack("H*", substr($data, 0, 26));
+    }
+  }
+  else {
+    printf "%08x: %s\n", $offset, unpack("H*", $data), "\n";
+  }
+}
+
+#unsigned long crc_table[256];
+my @crc_table;
+
+#/* Flag: has the table been computed? Initially false. */
+#   int crc_table_computed = 0;
+
+#   /* Make the table for a fast CRC. */
+#   void make_crc_table(void)
+#   {
+sub make_crc_table {
+#     unsigned long c;
+#     int n, k;
+#
+#     for (n = 0; n < 256; n++) {
+  for my $n (0 .. 255) {
+#       c = (unsigned long) n;
+    my $c = $n;
+#       for (k = 0; k < 8; k++) {
+    for my $k (0 .. 7) {
+#         if (c & 1)
+#           c = 0xedb88320L ^ (c >> 1);
+#         else
+#           c = c >> 1;
+      if ($c & 1) {
+       $c = 0xedb88320 ^ ($c >> 1);
+      }
+      else {
+       $c = $c >> 1;
+      }
+#   }
+    }
+#       crc_table[n] = c;
+    $crc_table[$n] = $c;
+#     }
+  }
+#     crc_table_computed = 1;
+#   }
+}
+
+# /* Update a running CRC with the bytes buf[0..len-1]--the CRC
+#    should be initialized to all 1's, and the transmitted value
+#    is the 1's complement of the final running CRC (see the
+#    crc() routine below). */
+
+# unsigned long update_crc(unsigned long crc, unsigned char *buf,
+#                          int len)
+#   {
+sub update_crc {
+  my ($crc, $data) = @_;
+#     unsigned long c = crc;
+#     int n;
+   
+#     if (!crc_table_computed)
+#       make_crc_table();
+  @crc_table or make_crc_table();
+#     for (n = 0; n < len; n++) {
+#       c = crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8);
+#     }
+  for my $code (unpack("C*", $data)) {
+    $crc = $crc_table[($crc ^ $code) & 0xFF] ^ ($crc >> 8);
+  }
+#     return c;
+#   }
+  return $crc;
+}
+   
+#   /* Return the CRC of the bytes buf[0..len-1]. */
+#   unsigned long crc(unsigned char *buf, int len)
+#   {
+#     return update_crc(0xffffffffL, buf, len) ^ 0xffffffffL;
+#   }
+
+sub crc {
+  my $data = shift;
+
+  return update_crc(0xFFFFFFFF, $data) ^ 0xFFFFFFFF;
+}
+
+sub do_inflate {
+  my $z = shift;
+  my $out = '';
+  inflate(\$z, \$out);
+
+  return $out;
+}
+
+=head HEAD
+
+pngdump.pl - dump the structure of a PNG image file.
+
+=head1 SYNOPSIS
+
+  perl [-dumpall] [-image] pngdump.pl filename
+
+=head1 DESCRIPTION
+
+Dumps the structure of a PNG image file, listing chunk types, length,
+CRC and optionally the entire content of each chunk.
+
+Options:
+
+=over
+
+=item *
+
+C<-dumpall> - dump the entire contents of each chunk rather than just
+the leading bytes.
+
+=item *
+
+C<-image> - decompress the image data (IDAT chunk) and break the
+result into rows, listing the filter and filtered (raw) data for each
+row.
+
+=back
+
+Several chunk types are extracted and displayed in a more readable
+form.
+
+=cut
index 9e2f855cdad180dbdccca045dc580ea8e5783065..4eac6400e9f16de06fef48d9fa0d7212b959f642 100644 (file)
@@ -1269,7 +1269,175 @@ C<datachannels>.
 
 =head2 PNG
 
-There are no PNG specific tags.
+=head3 PNG Image modes
+
+PNG files can be read and written in the following modes:
+
+=over
+
+=item *
+
+bi-level - written as a 1-bit per sample gray scale image
+
+=item *
+
+paletted - Imager gray scale paletted images are written as RGB
+paletted images.  PNG palettes can include alpha values for each entry
+and this is honored as an Imager four channel paletted image.
+
+=item *
+
+8 and 16-bit per sample gray scale, optionally with an alpha channel.
+
+=item *
+
+8 and 16-bit per sample RGB, optionally with an alpha channel.
+
+=back
+
+Unlike GIF, there is no automatic conversion to a paletted image,
+since PNG supports direct color.
+
+=head3 PNG Text tags
+
+Text tags are retrieved from and written to PNG C<tEXT> or C<zTXT>
+chunks.  The following standard tags from the PNG specification are
+directly supported:
+
+=over
+
+=item *
+
+C<i_comment>X<tags,i_comment> - keyword of "Comment".
+
+=item *
+
+C<png_author>X<tags,PNG,png_author> - keyword "Author".
+
+=item *
+
+C<png_copyright>X<tags,PNG,png_copyright> - keyword "Copyright".
+
+=item *
+
+C<png_creation_time>X<tags,PNG,png_creation_time> - keyword "Creation Time".
+
+=item *
+
+C<png_description>X<tags,PNG,png_description> - keyword "Description".
+
+=item *
+
+C<png_disclaimer>X<tags,PNG,png_disclaimer> - keyword "Disclaimer".
+
+=item *
+
+C<png_software>X<tags,PNG,png_software> - keyword "Software".
+
+=item *
+
+C<png_title>X<tags,PNG,png_title> - keyword "Title".
+
+=item *
+
+C<png_warning>X<tags,PNG,png_warning> - keyword "Warning".
+
+=back
+
+Each of these tags has a corresponding C< I<base-tag-name>_compressed
+>> tag, eg. C<png_comment_compressed>.  When reading, if the PNG chunk
+is compressed this tag will be set to 1, but is otherwise unset.  When
+writing, Imager will honor the compression tag if set and non-zero,
+otherwise the chunk text will be compressed if the value is longer
+than 1000 characters, as recommended by the C<libpng> documentation.
+
+PNG C<tEXT> or C<zTXT> chunks outside of those above are read into or
+written from Imager tags named like:
+
+=over
+
+=item *
+
+C<< png_textI<N>_key >> - the key for the text chunk.  This can be 1
+to 79 characters, may not contain any leading, trailing or consecutive
+spaces, and may contain only Latin-1 characters from 32-126, 161-255.
+
+=item *
+
+C<< png_textI<N>_text >> - the text for the text chunk.  This may not
+contain any C<NUL> characters.
+
+=item *
+
+C<< png_textI<N>_compressed >> - whether or not the text chunk is
+compressed.  This behaves similarly to the C<<
+I<base-tag-name>_compressed >> tags described above.
+
+=back
+
+Where I<N> starts from 0.  When writing both the C<..._key> and
+C<..._text> tags must be present or the write will fail.  If the key
+or text do not satisfy the requirements above the write will fail.
+
+=head3 Other PNG metadata tags
+
+=over
+
+=item *
+
+X<tags, png_interlace>C<png_interlace>, C<png_interlace_name> - only
+set when reading, C<png_interlace> is set to the type of interlacing
+used by the file, 0 for one, 1 for Adam7.  C<png_interlace_name> is
+set to a keyword describing the interlacing, either C<none> or
+C<adam7>.
+
+=item *
+
+X<tags, png_srgb_intent>C<png_srgb_intent> - the sRGB rendering intent
+for the image. an integer from 0 to 3, per the PNG specification.  If
+this chunk is found in the PNG file the C<gAMA> and C<cHRM> are
+ignored and the C<png_gamme> and C<png_chroma_...> tags are not set.
+Similarly when writing if C<png_srgb_intent> is set the C<gAMA> and
+C<cHRM> chunks are not written.
+
+=item *
+
+C<tags, png_gamma>C<png_gamma> - the gamma of the image. This value is
+not currently used by Imager when processing the image, but this may
+change in the future.
+
+=item *
+
+X<tags, png_chroma_...>C<png_chroma_white_x>, C<png_chroma_white_y>,
+C<png_chroma_red_x>, C<png_chroma_red_y>, C<png_chroma_green_x>,
+C<png_chroma_green_y>, C<png_chroma_blue_x>, C<png_chroma_blue_y> -
+the primary chromaticities of the image, defining the color model.
+This is currently not used by Imager when processing the image, but
+this may change in the future.
+
+=item *
+
+C<i_xres>, C<i_yres>, C<i_aspect_only> - processed per
+I<Imager::ImageTypes/CommonTags>.
+
+=item *
+
+X<tags, png_bits>C<png_bits> - the number of bits per sample in the
+representation.  Ignored when writing.
+
+=item *
+
+X<tags, png_time>X<png_time> - the creation time of the file formatted
+as C<< I<year>-I<month>-I<day>TI<hour>:I<minute>:I<second> >>.  This
+is stored as time data structure in the file, not a string.  If you
+set C<png_time> and it cannot be parsed as above, writing the PNG file
+will fail.
+
+=item *
+
+C<i_background> - set from the C<sBKG> when reading an image file.
+
+=back
 
 =head2 ICO (Microsoft Windows Icon) and CUR (Microsoft Windows Cursor)
 
index 74e4e46b561b9230571d00a02252c78eba953f7f..450fdaaecc0ab30f8e097aa10d76bf4d25cf3ecd 100644 (file)
@@ -14,6 +14,7 @@ Arnar
 BMP
 Blit
 CGI
+chromaticities
 CMYK
 CPAN
 FreeType
@@ -31,6 +32,7 @@ PNM
 RGB
 RGBA
 SGI
+sRGB
 TGA
 TIFF
 UTF-8