PNG re-work: paletted file writes
authorTony Cook <tony@develop-help.com>
Thu, 12 Apr 2012 09:55:56 +0000 (19:55 +1000)
committerTony Cook <tony@develop-help.com>
Sun, 29 Apr 2012 03:40:56 +0000 (13:40 +1000)
PNG/impng.c
PNG/t/10png.t

index f95d4343c89679077e25118033cc667544457300..9e25b94987dc4f418dc19c9d1e80c5f2c6f25c6f 100644 (file)
@@ -1,5 +1,6 @@
 #include "impng.h"
 #include "png.h"
+#include <stdlib.h>
 
 /* this is a way to get number of channels from color space 
  * Color code to channel number */
@@ -23,6 +24,13 @@ read_bilevel(png_structp png_ptr, png_infop info_ptr, i_img_dim width, i_img_dim
 static int
 write_direct8(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 void
+pack_to_bits(unsigned char *dest, const unsigned char *src, size_t count,
+            int bits);
+
 unsigned
 i_png_lib_version(void) {
   return png_access_version_number();
@@ -81,6 +89,7 @@ i_writepng_wiol(i_img *im, io_glue *ig) {
   int aspect_only, have_res;
   unsigned char *data;
   unsigned char * volatile vdata = NULL;
+  int bits;
 
   mm_log((1,"i_writepng(im %p ,ig %p)\n", im, ig));
 
@@ -111,13 +120,39 @@ i_writepng_wiol(i_img *im, io_glue *ig) {
 
   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 (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, "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 = 8;
+    mm_log((1, "direct output\n"));
+  }
 
-  channels = im->channels;
+  mm_log((1,"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,
@@ -167,8 +202,10 @@ i_writepng_wiol(i_img *im, io_glue *ig) {
    */
   png_set_user_limits(png_ptr, width, height);
 
-  png_set_IHDR(png_ptr, info_ptr, width, height, 8, cspace,
+  mm_log((1, ">png_set_IHDR\n"));
+  png_set_IHDR(png_ptr, info_ptr, width, height, bits, cspace,
               PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
+  mm_log((1, "<png_set_IHDR\n"));
 
   have_res = 1;
   if (i_tags_get_float(&im->tags, "i_xres", 0, &xres)) {
@@ -192,11 +229,17 @@ i_writepng_wiol(i_img *im, io_glue *ig) {
                  aspect_only ? PNG_RESOLUTION_UNKNOWN : PNG_RESOLUTION_METER);
   }
 
-  png_write_info(png_ptr, info_ptr);
-
-  if (!write_direct8(png_ptr, info_ptr, im)) {
-    png_destroy_write_struct(&png_ptr, &info_ptr);
-    return 0;
+  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;
+    }
+  }
+  else  {
+    if (!write_direct8(png_ptr, info_ptr, im)) {
+      png_destroy_write_struct(&png_ptr, &info_ptr);
+      return 0;
+    }
   }
 
   png_write_end(png_ptr, info_ptr);
@@ -733,6 +776,8 @@ write_direct8(png_structp png_ptr, png_infop info_ptr, i_img *im) {
     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);
@@ -743,6 +788,147 @@ write_direct8(png_structp png_ptr, png_infop info_ptr, i_img *im) {
   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;
+}
+
+#if 0
+/* the source size is required to be rounded up to a number of samples
+   per byte bounday */
+static void
+pack_to_bits(unsigned char *dest, const unsigned char *src, size_t count,
+            int bits) {
+  ptr_diff_t out_count;
+  switch (bits) {
+  case 1:
+    out_count = (count + 7) / 8;
+    while (out_count > 0) {
+      unsigned mask = 0x80;
+      unsigned out = 0;
+      while (mask) {
+       if (*src++)
+         out |= mask;
+       mask >>= 1;
+      }
+      *dest++ = out;
+      --out_count;
+    }
+    break;
+
+  case 2:
+    out_count = (count + 3) / 4;
+    while (out_count > 0) {
+      int shift = 6;
+      unsigned out = 0;
+      while (shift >= 0) {
+       out |= *src << shift;
+       ++src;
+       shift -= 2;
+      }
+      *dest++ = out;
+      --out_count;
+    }
+    break;
+
+  case 4:
+    out_count = (count + 1) / 2;
+    while (out_count > 0) {
+      *dest++ = (src[0] << 4) | src[1];
+      src += 2;
+      --out_count;
+    }
+    break
+
+  case 8:
+    break;
+  }
+}
+
+#endif
+
 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);
index 33680ba17e41aaa11c51168bd62bbb1887b3d5ad..b32b6b5432f65c3894cc77eba9a472ed5f1e259d 100644 (file)
@@ -10,7 +10,7 @@ my $debug_writes = 1;
 
 init_log("testout/t102png.log",1);
 
-plan tests => 151;
+plan tests => 187;
 
 # this loads Imager::File::PNG too
 ok($Imager::formats{"png"}, "must have png format");
@@ -35,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";
@@ -423,6 +424,102 @@ SKIP:
   }
 }
 
+{
+  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(#000 #FFF #F00 #0F0 #00f) ]),
+     "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(#000 #FFF #F00 #0F0 #00f),
+                 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("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");
+}
+
 sub limited_write {
   my ($limit) = @_;
 
@@ -441,3 +538,4 @@ sub limited_write {
        }
      };
 }
+