add new comparison method rgb_difference that resembles arithmetical difference per...
[imager.git] / pnm.c
diff --git a/pnm.c b/pnm.c
index a4e1753..970994b 100644 (file)
--- a/pnm.c
+++ b/pnm.c
@@ -1,7 +1,7 @@
-#include "image.h"
-#include "io.h"
+#include "imager.h"
 #include "log.h"
 #include "iolayer.h"
+#include "imageri.h"
 
 #include <stdlib.h>
 #include <errno.h>
@@ -15,7 +15,7 @@ pnm.c - implements reading and writing ppm/pnm/pbm files, uses io layer.
 =head1 SYNOPSIS
 
    io_glue *ig = io_new_fd( fd );
-   i_img *im   = i_readpnm_wiol(ig, -1); // no limit on how much is read
+   i_img *im   = i_readpnm_wiol(ig, 0); // no limit on how much is read
    // or 
    io_glue *ig = io_new_fd( fd );
    return_code = i_writepnm_wiol(im, ig); 
@@ -36,147 +36,66 @@ Some of these functions are internal.
 */
 
 
-#define BSIZ 1024
 #define misspace(x) (x==' ' || x=='\n' || x=='\r' || x=='\t' || x=='\f' || x=='\v')
 #define misnumber(x) (x <= '9' && x>='0')
 
 static char *typenames[]={"ascii pbm", "ascii pgm", "ascii ppm", "binary pbm", "binary pgm", "binary ppm"};
 
 /*
- * Type to encapsulate the local buffer
- * management skipping over in a file 
- */
-
-typedef struct {
-  io_glue *ig;
-  int len;
-  int cp;
-  char buf[BSIZ];
-} mbuf;
-
-
-static
-void init_buf(mbuf *mb, io_glue *ig) {
-  mb->len = 0;
-  mb->cp  = 0;
-  mb->ig  = ig;
-}
-
-
-
-/*
-=item gnext(mbuf *mb)
-
-Fetches a character and advances in stream by one character.  
-Returns a pointer to the byte or NULL on failure (internal).
-
-   mb - buffer object
-
-=cut
-*/
-
-static
-char *
-gnext(mbuf *mb) {
-  io_glue *ig = mb->ig;
-  if (mb->cp == mb->len) {
-    mb->cp = 0;
-    mb->len = ig->readcb(ig, mb->buf, BSIZ);
-    if (mb->len == -1) {
-      i_push_error(errno, "file read error");
-      mm_log((1, "i_readpnm: read error\n"));
-      return NULL;
-    }
-    if (mb->len == 0) {
-      i_push_error(errno, "unexpected end of file");
-      mm_log((1, "i_readpnm: end of file\n"));
-      return NULL;
-    }
-  }
-  return &mb->buf[mb->cp++];
-}
-
-
-/*
-=item gnext(mbuf *mb)
-
-Fetches a character but does NOT advance.  Returns a pointer to
-the byte or NULL on failure (internal).
-
-   mb - buffer object
-
-=cut
-*/
-
-static
-char *
-gpeek(mbuf *mb) {
-  io_glue *ig = mb->ig;
-  if (mb->cp == mb->len) {
-    mb->cp = 0;
-    mb->len = ig->readcb(ig, mb->buf, BSIZ);
-    if (mb->len == -1) {
-      i_push_error(errno, "read error");
-      mm_log((1, "i_readpnm: read error\n"));
-      return NULL;
-    }
-    if (mb->len == 0) {
-      i_push_error(0, "unexpected end of file");
-      mm_log((1, "i_readpnm: end of file\n"));
-      return NULL;
-    }
-  }
-  return &mb->buf[mb->cp];
-}
-
-
-
-
-/*
-=item skip_spaces(mb)
+=item skip_spaces(ig)
 
 Advances in stream until it is positioned at a
 non white space character. (internal)
 
-   mb - buffer object
+   ig - io_glue
 
 =cut
 */
 
 static
 int
-skip_spaces(mbuf *mb) {
-  char *cp;
-  while( (cp = gpeek(mb)) && misspace(*cp) ) if ( !gnext(mb) ) break;
-  if (!cp) return 0;
+skip_spaces(io_glue *ig) {
+  int c;
+  while( (c = i_io_peekc(ig)) != EOF && misspace(c) ) {
+    if ( i_io_getc(ig) == EOF )
+      break;
+  }
+  if (c == EOF)
+    return 0;
+
   return 1;
 }
 
 
 /*
-=item skip_spaces(mb)
+=item skip_comment(ig)
 
 Advances in stream over whitespace and a comment if one is found. (internal)
 
-   mb - buffer object
+   ig - io_glue object
 
 =cut
 */
 
 static
 int
-skip_comment(mbuf *mb) {
-  char *cp;
+skip_comment(io_glue *ig) {
+  int c;
 
-  if (!skip_spaces(mb)) return 0;
+  if (!skip_spaces(ig))
+    return 0;
 
-  if (!(cp = gpeek(mb))) return 0;
-  if (*cp == '#') {
-    while( (cp = gpeek(mb)) && (*cp != '\n' && *cp != '\r') ) {
-      if ( !gnext(mb) ) break;
+  if ((c = i_io_peekc(ig)) == EOF)
+    return 0;
+
+  if (c == '#') {
+    while( (c = i_io_peekc(ig)) != EOF && (c != '\n' && c != '\r') ) {
+      if ( i_io_getc(ig) == EOF )
+       break;
     }
   }
-  if (!cp) return 0;
+  if (c == EOF)
+    return 0;
   
   return 1;
 }
@@ -196,82 +115,346 @@ on success else false.
 
 static
 int
-gnum(mbuf *mb, int *i) {
-  char *cp;
+gnum(io_glue *ig, int *i) {
+  int c;
   *i = 0;
 
-  if (!skip_spaces(mb)) return 0; 
-
-  while( (cp = gpeek(mb)) && misnumber(*cp) ) {
-    *i = *i*10+(*cp-'0');
-    cp = gnext(mb);
+  if (!skip_spaces(ig)) return 0; 
+
+  if ((c = i_io_peekc(ig)) == EOF) 
+    return 0;
+  if (!misnumber(c))
+    return 0;
+  while( (c = i_io_peekc(ig)) != EOF && misnumber(c) ) {
+    int work = *i * 10 + (c - '0');
+    if (work < *i) {
+      /* overflow */
+      i_push_error(0, "integer overflow");
+      return 0;
+    }
+    *i = work;
+    i_io_nextc(ig);
   }
+
   return 1;
 }
 
+static
+i_img *
+read_pgm_ppm_bin8(io_glue *ig, i_img *im, int width, int height, 
+                  int channels, int maxval, int allow_incomplete) {
+  i_color *line, *linep;
+  int read_size;
+  unsigned char *read_buf, *readp;
+  int x, y, ch;
+  int rounder = maxval / 2;
+
+  line = mymalloc(width * sizeof(i_color));
+  read_size = channels * width;
+  read_buf = mymalloc(read_size);
+  for(y=0;y<height;y++) {
+    linep = line;
+    readp = read_buf;
+    if (i_io_read(ig, read_buf, read_size) != read_size) {
+      myfree(line);
+      myfree(read_buf);
+      if (allow_incomplete) {
+        i_tags_setn(&im->tags, "i_incomplete", 1);
+        i_tags_setn(&im->tags, "i_lines_read", y);
+        return im;
+      }
+      else {
+        i_push_error(0, "short read - file truncated?");
+        i_img_destroy(im);
+        return NULL;
+      }
+    }
+    if (maxval == 255) {
+      for(x=0; x<width; x++) {
+        for(ch=0; ch<channels; ch++) {
+          linep->channel[ch] = *readp++;
+        }
+        ++linep;
+      }
+    }
+    else {
+      for(x=0; x<width; x++) {
+        for(ch=0; ch<channels; ch++) {
+          /* we just clamp samples to the correct range */
+          unsigned sample = *readp++;
+          if (sample > maxval)
+            sample = maxval;
+          linep->channel[ch] = (sample * 255 + rounder) / maxval;
+        }
+        ++linep;
+      }
+    }
+    i_plin(im, 0, width, y, line);
+  }
+  myfree(read_buf);
+  myfree(line);
+
+  return im;
+}
+
+static
+i_img *
+read_pgm_ppm_bin16(io_glue *ig, i_img *im, int width, int height, 
+                  int channels, int maxval, int allow_incomplete) {
+  i_fcolor *line, *linep;
+  int read_size;
+  unsigned char *read_buf, *readp;
+  int x, y, ch;
+  double maxvalf = maxval;
+
+  line = mymalloc(width * sizeof(i_fcolor));
+  read_size = channels * width * 2;
+  read_buf = mymalloc(read_size);
+  for(y=0;y<height;y++) {
+    linep = line;
+    readp = read_buf;
+    if (i_io_read(ig, read_buf, read_size) != read_size) {
+      myfree(line);
+      myfree(read_buf);
+      if (allow_incomplete) {
+        i_tags_setn(&im->tags, "i_incomplete", 1);
+        i_tags_setn(&im->tags, "i_lines_read", y);
+        return im;
+      }
+      else {
+        i_push_error(0, "short read - file truncated?");
+        i_img_destroy(im);
+        return NULL;
+      }
+    }
+    for(x=0; x<width; x++) {
+      for(ch=0; ch<channels; ch++) {
+        unsigned sample = (readp[0] << 8) + readp[1];
+        if (sample > maxval)
+          sample = maxval;
+        readp += 2;
+        linep->channel[ch] = sample / maxvalf;
+      }
+      ++linep;
+    }
+    i_plinf(im, 0, width, y, line);
+  }
+  myfree(read_buf);
+  myfree(line);
 
-/*
-=item i_readpnm_wiol(ig, length)
+  return im;
+}
 
-Retrieve an image and stores in the iolayer object. Returns NULL on fatal error.
+static 
+i_img *
+read_pbm_bin(io_glue *ig, i_img *im, int width, int height, int allow_incomplete) {
+  i_palidx *line, *linep;
+  int read_size;
+  unsigned char *read_buf, *readp;
+  int x, y;
+  unsigned mask;
+
+  line = mymalloc(width * sizeof(i_palidx));
+  read_size = (width + 7) / 8;
+  read_buf = mymalloc(read_size);
+  for(y = 0; y < height; y++) {
+    if (i_io_read(ig, read_buf, read_size) != read_size) {
+      myfree(line);
+      myfree(read_buf);
+      if (allow_incomplete) {
+        i_tags_setn(&im->tags, "i_incomplete", 1);
+        i_tags_setn(&im->tags, "i_lines_read", y);
+        return im;
+      }
+      else {
+        i_push_error(0, "short read - file truncated?");
+        i_img_destroy(im);
+        return NULL;
+      }
+    }
+    linep = line;
+    readp = read_buf;
+    mask = 0x80;
+    for(x = 0; x < width; ++x) {
+      *linep++ = *readp & mask ? 1 : 0;
+      mask >>= 1;
+      if (mask == 0) {
+        ++readp;
+        mask = 0x80;
+      }
+    }
+    i_ppal(im, 0, width, y, line);
+  }
+  myfree(read_buf);
+  myfree(line);
 
-   ig     - io_glue object
-   length - maximum length to read from data source, before closing it -1 
-            signifies no limit.
+  return im;
+}
 
-=cut
+/* unlike pgm/ppm pbm:
+  - doesn't require spaces between samples (bits)
+  - 1 (maxval) is black instead of white
 */
+static 
+i_img *
+read_pbm_ascii(io_glue *ig, i_img *im, int width, int height, int allow_incomplete) {
+  i_palidx *line, *linep;
+  int x, y;
+
+  line = mymalloc(width * sizeof(i_palidx));
+  for(y = 0; y < height; y++) {
+    linep = line;
+    for(x = 0; x < width; ++x) {
+      int c;
+      skip_spaces(ig);
+      if ((c = i_io_getc(ig)) == EOF || (c != '0' && c != '1')) {
+        myfree(line);
+        if (allow_incomplete) {
+          i_tags_setn(&im->tags, "i_incomplete", 1);
+          i_tags_setn(&im->tags, "i_lines_read", y);
+          return im;
+        }
+        else {
+          if (c != EOF)
+            i_push_error(0, "invalid data for ascii pnm");
+          else
+            i_push_error(0, "short read - file truncated?");
+          i_img_destroy(im);
+          return NULL;
+        }
+      }
+      *linep++ = c == '0' ? 0 : 1;
+    }
+    i_ppal(im, 0, width, y, line);
+  }
+  myfree(line);
 
+  return im;
+}
 
+static
 i_img *
-i_readpnm_wiol(io_glue *ig, int length) {
-  i_img* im;
-  int type;
+read_pgm_ppm_ascii(io_glue *ig, i_img *im, int width, int height, int channels, 
+                   int maxval, int allow_incomplete) {
+  i_color *line, *linep;
   int x, y, ch;
-  int width, height, maxval, channels, pcount;
-  char *cp;
-  unsigned char *uc;
-  mbuf buf;
-  i_color val;
-  int mult;
+  int rounder = maxval / 2;
 
-  i_clear_error();
+  line = mymalloc(width * sizeof(i_color));
+  for(y=0;y<height;y++) {
+    linep = line;
+    for(x=0; x<width; x++) {
+      for(ch=0; ch<channels; ch++) {
+        int sample;
+        
+        if (!gnum(ig, &sample)) {
+          myfree(line);
+          if (allow_incomplete) {
+            i_tags_setn(&im->tags, "i_incomplete", 1);
+            i_tags_setn(&im->tags, "i_lines_read", 1);
+            return im;
+          }
+          else {
+            if (i_io_peekc(ig) != EOF)
+              i_push_error(0, "invalid data for ascii pnm");
+            else
+              i_push_error(0, "short read - file truncated?");
+            i_img_destroy(im);
+            return NULL;
+          }
+        }
+        if (sample > maxval)
+          sample = maxval;
+        linep->channel[ch] = (sample * 255 + rounder) / maxval;
+      }
+      ++linep;
+    }
+    i_plin(im, 0, width, y, line);
+  }
+  myfree(line);
 
-  /*  char *pp; */
+  return im;
+}
 
-  mm_log((1,"i_readpnm(ig %p, length %d)\n", ig, length));
+static
+i_img *
+read_pgm_ppm_ascii_16(io_glue *ig, i_img *im, int width, int height, 
+                      int channels, int maxval, int allow_incomplete) {
+  i_fcolor *line, *linep;
+  int x, y, ch;
+  double maxvalf = maxval;
 
-  /*
-  pp = mymalloc(20);
-  
-  pp[-1]= 'c';
-  pp[-2]= 'c';
-  
-  bndcheck_all();
+  line = mymalloc(width * sizeof(i_fcolor));
+  for(y=0;y<height;y++) {
+    linep = line;
+    for(x=0; x<width; x++) {
+      for(ch=0; ch<channels; ch++) {
+        int sample;
+        
+        if (!gnum(ig, &sample)) {
+          myfree(line);
+          if (allow_incomplete) {
+           i_tags_setn(&im->tags, "i_incomplete", 1);
+           i_tags_setn(&im->tags, "i_lines_read", y);
+           return im;
+          }
+          else {
+            if (i_io_peekc(ig) != EOF)
+              i_push_error(0, "invalid data for ascii pnm");
+            else
+              i_push_error(0, "short read - file truncated?");
+            i_img_destroy(im);
+            return NULL;
+          }
+        }
+        if (sample > maxval)
+          sample = maxval;
+        linep->channel[ch] = sample / maxvalf;
+      }
+      ++linep;
+    }
+    i_plinf(im, 0, width, y, line);
+  }
+  myfree(line);
+
+  return im;
+}
+
+/*
+=item i_readpnm_wiol(ig, allow_incomplete)
 
-  myfree(pp);
+Retrieve an image and stores in the iolayer object. Returns NULL on fatal error.
 
-  mm_log((1, "Hack is exiting\n"));
+   ig     - io_glue object
+   allow_incomplete - allows a partial file to be read successfully
 
+=cut
 */
-  
-  io_glue_commit_types(ig);
-  init_buf(&buf, ig);
 
-  cp = gnext(&buf);
+i_img *
+i_readpnm_wiol( io_glue *ig, int allow_incomplete) {
+  i_img* im;
+  int type;
+  int width, height, maxval, channels;
+  int c;
 
-  if (!cp || *cp != 'P') {
+  i_clear_error();
+  mm_log((1,"i_readpnm(ig %p, allow_incomplete %d)\n", ig, allow_incomplete));
+
+  c = i_io_getc(ig);
+
+  if (c != 'P') {
     i_push_error(0, "bad header magic, not a PNM file");
     mm_log((1, "i_readpnm: Could not read header of file\n"));
     return NULL;
   }
 
-  if ( !(cp = gnext(&buf)) ) {
+  if ((c = i_io_getc(ig)) == EOF ) {
     mm_log((1, "i_readpnm: Could not read header of file\n"));
     return NULL;
   }
   
-  type = *cp-'0';
+  type = c - '0';
 
   if (type < 1 || type > 6) {
     i_push_error(0, "unknown PNM file type, not a PNM file");
@@ -279,12 +462,12 @@ i_readpnm_wiol(io_glue *ig, int length) {
     return NULL;
   }
 
-  if ( !(cp = gnext(&buf)) ) {
+  if ( (c = i_io_getc(ig)) == EOF ) {
     mm_log((1, "i_readpnm: Could not read header of file\n"));
     return NULL;
   }
   
-  if ( !misspace(*cp) ) {
+  if ( !misspace(c) ) {
     i_push_error(0, "unexpected character, not a PNM file");
     mm_log((1, "i_readpnm: Not a pnm file\n"));
     return NULL;
@@ -295,118 +478,298 @@ i_readpnm_wiol(io_glue *ig, int length) {
   
   /* Read sizes and such */
 
-  if (!skip_comment(&buf)) {
+  if (!skip_comment(ig)) {
     i_push_error(0, "while skipping to width");
     mm_log((1, "i_readpnm: error reading before width\n"));
     return NULL;
   }
   
-  if (!gnum(&buf, &width)) {
+  if (!gnum(ig, &width)) {
     i_push_error(0, "could not read image width");
     mm_log((1, "i_readpnm: error reading width\n"));
     return NULL;
   }
 
-  if (!skip_comment(&buf)) {
+  if (!skip_comment(ig)) {
     i_push_error(0, "while skipping to height");
     mm_log((1, "i_readpnm: error reading before height\n"));
     return NULL;
   }
 
-  if (!gnum(&buf, &height)) {
+  if (!gnum(ig, &height)) {
     i_push_error(0, "could not read image height");
     mm_log((1, "i_readpnm: error reading height\n"));
     return NULL;
   }
   
   if (!(type == 1 || type == 4)) {
-    if (!skip_comment(&buf)) {
+    if (!skip_comment(ig)) {
       i_push_error(0, "while skipping to maxval");
       mm_log((1, "i_readpnm: error reading before maxval\n"));
       return NULL;
     }
 
-    if (!gnum(&buf, &maxval)) {
+    if (!gnum(ig, &maxval)) {
       i_push_error(0, "could not read maxval");
       mm_log((1, "i_readpnm: error reading maxval\n"));
       return NULL;
     }
+
+    if (maxval == 0) {
+      i_push_error(0, "maxval is zero - invalid pnm file");
+      mm_log((1, "i_readpnm: maxval is zero, invalid pnm file\n"));
+      return NULL;
+    }
+    else if (maxval > 65535) {
+      i_push_errorf(0, "maxval of %d is over 65535 - invalid pnm file", 
+                   maxval);
+      mm_log((1, "i_readpnm: maxval of %d is over 65535 - invalid pnm file\n", maxval));
+      return NULL;
+    }
   } else maxval=1;
 
-  if (!(cp = gnext(&buf)) || !misspace(*cp)) {
+  if ((c = i_io_getc(ig)) == EOF || !misspace(c)) {
     i_push_error(0, "garbage in header, invalid PNM file");
     mm_log((1, "i_readpnm: garbage in header\n"));
     return NULL;
   }
 
   channels = (type == 3 || type == 6) ? 3:1;
-  pcount = width*height*channels;
+
+  if (!i_int_check_image_file_limits(width, height, channels, sizeof(i_sample_t))) {
+    mm_log((1, "i_readpnm: image size exceeds limits\n"));
+    return NULL;
+  }
 
   mm_log((1, "i_readpnm: (%d x %d), channels = %d, maxval = %d\n", width, height, channels, maxval));
-  
-  im = i_img_empty_ch(NULL, width, height, channels);
+
+  if (type == 1 || type == 4) {
+    i_color pbm_pal[2];
+    pbm_pal[0].channel[0] = 255;
+    pbm_pal[1].channel[0] = 0;
+    
+    im = i_img_pal_new(width, height, 1, 256);
+    i_addcolors(im, pbm_pal, 2);
+  }
+  else {
+    if (maxval > 255)
+      im = i_img_16_new(width, height, channels);
+    else
+      im = i_img_8_new(width, height, channels);
+  }
 
   switch (type) {
   case 1: /* Ascii types */
+    im = read_pbm_ascii(ig, im, width, height, allow_incomplete);
+    break;
+
   case 2:
   case 3:
-    mult = type == 1 ? 255 : 1;
-    for(y=0;y<height;y++) for(x=0; x<width; x++) {
-      for(ch=0; ch<channels; ch++) {
-       int t;
-       if (gnum(&buf, &t)) val.channel[ch] = t * mult;
-       else {
-         mm_log((1,"i_readpnm: gnum() returned false in data\n"));
-         return im;
-       }
-      }
-      i_ppix(im, x, y, &val);
-    }
+    if (maxval > 255)
+      im = read_pgm_ppm_ascii_16(ig, im, width, height, channels, maxval, allow_incomplete);
+    else
+      im = read_pgm_ppm_ascii(ig, im, width, height, channels, maxval, allow_incomplete);
     break;
     
   case 4: /* binary pbm */
-    for(y=0;y<height;y++) for(x=0; x<width; x+=8) {
-      if ( (uc = (unsigned char*)gnext(&buf)) ) {
-       int xt;
-       int pc = width-x < 8 ? width-x : 8;
-       /*      mm_log((1,"i_readpnm: y=%d x=%d pc=%d\n", y, x, pc)); */
-       for(xt = 0; xt<pc; xt++) {
-         val.channel[0] = (*uc & (128>>xt)) ? 0 : 255; 
-         i_ppix(im, x+xt, y, &val);
-       }
-      } else {
-       mm_log((1,"i_readpnm: gnext() returned false in data\n"));
-       return im;
-      }
-    }
+    im = read_pbm_bin(ig, im, width, height, allow_incomplete);
     break;
 
   case 5: /* binary pgm */
   case 6: /* binary ppm */
-    for(y=0;y<height;y++) for(x=0; x<width; x++) {
-      for(ch=0; ch<channels; ch++) {
-       if ( (uc = (unsigned char*)gnext(&buf)) ) val.channel[ch] = *uc;
-       else {
-         mm_log((1,"i_readpnm: gnext() returned false in data\n"));
-         return im;
-       }
-      }
-      i_ppix(im, x, y, &val);
-    }
+    if (maxval > 255)
+      im = read_pgm_ppm_bin16(ig, im, width, height, channels, maxval, allow_incomplete);
+    else
+      im = read_pgm_ppm_bin8(ig, im, width, height, channels, maxval, allow_incomplete);
     break;
+
   default:
     mm_log((1, "type %s [P%d] unsupported\n", typenames[type-1], type));
     return NULL;
   }
+
+  if (!im)
+    return NULL;
+
+  i_tags_add(&im->tags, "i_format", 0, "pnm", -1, 0);
+  i_tags_setn(&im->tags, "pnm_maxval", maxval);
+  i_tags_setn(&im->tags, "pnm_type", type);
+
   return im;
 }
 
+static void free_images(i_img **imgs, int count) {
+  int i;
+
+  if (count) {
+    for (i = 0; i < count; ++i)
+      i_img_destroy(imgs[i]);
+    myfree(imgs);
+  }
+}
+
+i_img **i_readpnm_multi_wiol(io_glue *ig, int *count, int allow_incomplete) {
+    i_img **results = NULL;
+    i_img *img = NULL;
+    char c = EOF;
+    int result_alloc = 0, 
+        value = 0, 
+        eof = 0;
+    *count=0;
+
+    do {
+        mm_log((1, "read image %i\n", 1+*count));
+        img = i_readpnm_wiol( ig, allow_incomplete );
+        if( !img ) {
+            free_images( results, *count );
+            return NULL;
+        }
+        ++*count;
+        if (*count > result_alloc) {
+            if (result_alloc == 0) {
+                result_alloc = 5;
+                results = mymalloc(result_alloc * sizeof(i_img *));
+            }
+            else {
+                /* myrealloc never fails (it just dies if it can't allocate) */
+                result_alloc *= 2;
+                results = myrealloc(results, result_alloc * sizeof(i_img *));
+            }
+        }
+        results[*count-1] = img;
+
+
+        if( i_tags_get_int(&img->tags, "i_incomplete", 0, &value ) && value) {
+            eof = 1;
+        }
+        else if( skip_spaces( ig ) && ( c=i_io_peekc( ig ) ) != EOF && c == 'P' ) {
+            eof = 0;
+        }
+        else {
+            eof = 1;
+        }
+    } while(!eof);
+    return results;
+}
+
+
+
+static
+int
+write_pbm(i_img *im, io_glue *ig, int zero_is_white) {
+  int x, y;
+  i_palidx *line;
+  i_img_dim write_size;
+  unsigned char *write_buf;
+  unsigned char *writep;
+  char header[255];
+  unsigned mask;
+
+  sprintf(header, "P4\012# CREATOR: Imager\012%" i_DF " %" i_DF "\012", 
+          i_DFc(im->xsize), i_DFc(im->ysize));
+  if (i_io_write(ig, header, strlen(header)) < 0) {
+    i_push_error(0, "could not write pbm header");
+    return 0;
+  }
+  write_size = (im->xsize + 7) / 8;
+  line = mymalloc(sizeof(i_palidx) * im->xsize);
+  write_buf = mymalloc(write_size);
+  for (y = 0; y < im->ysize; ++y) {
+    i_gpal(im, 0, im->xsize, y, line);
+    mask = 0x80;
+    writep = write_buf;
+    memset(write_buf, 0, write_size);
+    for (x = 0; x < im->xsize; ++x) {
+      if (zero_is_white ? line[x] : !line[x])
+        *writep |= mask;
+      mask >>= 1;
+      if (!mask) {
+        ++writep;
+        mask = 0x80;
+      }
+    }
+    if (i_io_write(ig, write_buf, write_size) != write_size) {
+      i_push_error(0, "write failure");
+      myfree(write_buf);
+      myfree(line);
+      return 0;
+    }
+  }
+  myfree(write_buf);
+  myfree(line);
+
+  return 1;
+}
+
+static
+int
+write_ppm_data_8(i_img *im, io_glue *ig, int want_channels) {
+  size_t write_size = im->xsize * want_channels;
+  size_t buf_size = im->xsize * im->channels;
+  unsigned char *data = mymalloc(buf_size);
+  i_img_dim y = 0;
+  int rc = 1;
+  i_color bg;
+
+  i_get_file_background(im, &bg);
+  while (y < im->ysize && rc >= 0) {
+    i_gsamp_bg(im, 0, im->xsize, y, data, want_channels, &bg);
+    if (i_io_write(ig, data, write_size) != write_size) {
+      i_push_error(errno, "could not write ppm data");
+      rc = 0;
+      break;
+    }
+    ++y;
+  }
+  myfree(data);
+
+  return rc;
+}
+
+static
+int
+write_ppm_data_16(i_img *im, io_glue *ig, int want_channels) {
+  size_t line_size = im->channels * im->xsize * sizeof(i_fsample_t);
+  size_t sample_count = want_channels * im->xsize;
+  size_t write_size = sample_count * 2;
+  i_fsample_t *line_buf = mymalloc(line_size);
+  i_fsample_t *samplep;
+  unsigned char *write_buf = mymalloc(write_size);
+  unsigned char *writep;
+  size_t sample_num;
+  i_img_dim y = 0;
+  int rc = 1;
+  i_fcolor bg;
+
+  i_get_file_backgroundf(im, &bg);
+
+  while (y < im->ysize) {
+    i_gsampf_bg(im, 0, im->xsize, y, line_buf, want_channels, &bg);
+    samplep = line_buf;
+    writep = write_buf;
+    for (sample_num = 0; sample_num < sample_count; ++sample_num) {
+      unsigned sample16 = SampleFTo16(*samplep++);
+      *writep++ = sample16 >> 8;
+      *writep++ = sample16 & 0xFF;
+    }
+    if (i_io_write(ig, write_buf, write_size) != write_size) {
+      i_push_error(errno, "could not write ppm data");
+      rc = 0;
+      break;
+    }
+    ++y;
+  }
+  myfree(line_buf);
+  myfree(write_buf);
+
+  return rc;
+}
 
 undef_int
 i_writeppm_wiol(i_img *im, io_glue *ig) {
   char header[255];
-  int rc;
-  writep write_func;
+  int zero_is_white;
+  int wide_data;
 
   mm_log((1,"i_writeppm(im %p, ig %p)\n", im, ig));
   i_clear_error();
@@ -414,91 +777,66 @@ i_writeppm_wiol(i_img *im, io_glue *ig) {
   /* Add code to get the filename info from the iolayer */
   /* Also add code to check for mmapped code */
 
-  io_glue_commit_types(ig);
+  if (i_img_is_monochrome(im, &zero_is_white)) {
+    if (!write_pbm(im, ig, zero_is_white))
+      return 0;
+  }
+  else {
+    int type;
+    int maxval;
+    int want_channels = im->channels;
 
-  if (im->channels == 3) {
-    sprintf(header,"P6\n#CREATOR: Imager\n%d %d\n255\n",im->xsize,im->ysize);
-    if (ig->writecb(ig,header,strlen(header))<0) {
-      i_push_error(errno, "could not write ppm header");
-      mm_log((1,"i_writeppm: unable to write ppm header.\n"));
-      return(0);
-    }
+    if (want_channels == 2 || want_channels == 4)
+      --want_channels;
+
+    if (!i_tags_get_int(&im->tags, "pnm_write_wide_data", 0, &wide_data))
+      wide_data = 0;
 
-    if (!im->virtual && im->bits == i_8_bits && im->type == i_direct_type) {
-      rc = ig->writecb(ig,im->idata,im->bytes);
+    if (want_channels == 3) {
+      type = 6;
     }
-    else {
-      unsigned char *data = mymalloc(3 * im->xsize);
-      if (data != NULL) {
-        int y = 0;
-        int x, ch;
-        unsigned char *p;
-        static int rgb_chan[3] = { 0, 1, 2 };
-
-        rc = 0;
-        while (y < im->ysize && rc >= 0) {
-          i_gsamp(im, 0, im->xsize, y, data, rgb_chan, 3);
-          rc = ig->writecb(ig, data, im->xsize * 3);
-          ++y;
-        }
-        myfree(data);
-      }
-      else {
-        i_push_error(0, "Out of memory");
-        return 0;
-      }
+    else if (want_channels == 1) {
+      type = 5;
     }
-    if (rc<0) {
-      i_push_error(errno, "could not write ppm data");
-      mm_log((1,"i_writeppm: unable to write ppm data.\n"));
+    else {
+      i_push_error(0, "can only save 1 or 3 channel images to pnm");
+      mm_log((1,"i_writeppm: ppm/pgm is 1 or 3 channel only (current image is %d)\n",im->channels));
       return(0);
     }
-  }
-  else if (im->channels == 1) {
-    sprintf(header, "P5\n#CREATOR: Imager\n%d %d\n255\n",
-           im->xsize, im->ysize);
-    if (ig->writecb(ig,header, strlen(header)) < 0) {
-      i_push_error(errno, "could not write pgm header");
-      mm_log((1,"i_writeppm: unable to write pgm header.\n"));
+    if (im->bits <= 8 || !wide_data)
+      maxval = 255;
+    else
+      maxval = 65535;
+
+    sprintf(header,"P%d\n#CREATOR: Imager\n%" i_DF " %" i_DF"\n%d\n", 
+            type, i_DFc(im->xsize), i_DFc(im->ysize), maxval);
+
+    if (i_io_write(ig,header,strlen(header)) != strlen(header)) {
+      i_push_error(errno, "could not write ppm header");
+      mm_log((1,"i_writeppm: unable to write ppm header.\n"));
       return(0);
     }
 
-    if (!im->virtual && im->bits == i_8_bits && im->type == i_direct_type) {
-      rc=ig->writecb(ig,im->idata,im->bytes);
-    }
-    else {
-      unsigned char *data = mymalloc(im->xsize);
-      if (data != NULL) {
-        int y = 0;
-        int x, ch;
-        int chan = 0;
-        unsigned char *p;
-
-        rc = 0;
-        while (y < im->ysize && rc >= 0) {
-          i_gsamp(im, 0, im->xsize, y, data, &chan, 1);
-          rc = ig->writecb(ig, data, im->xsize);
-          ++y;
-        }
-        myfree(data);
-      }
-      else {
-        i_push_error(0, "Out of memory");
+    if (!im->virtual && im->bits == i_8_bits && im->type == i_direct_type
+       && im->channels == want_channels) {
+      if (i_io_write(ig,im->idata,im->bytes) != im->bytes) {
+        i_push_error(errno, "could not write ppm data");
         return 0;
       }
     }
-    if (rc<0) {
-      i_push_error(errno, "could not write pgm data");
-      mm_log((1,"i_writeppm: unable to write pgm data.\n"));
-      return(0);
+    else if (maxval == 255) {
+      if (!write_ppm_data_8(im, ig, want_channels))
+        return 0;
+    }
+    else {
+      if (!write_ppm_data_16(im, ig, want_channels))
+        return 0;
     }
   }
-  else {
-    i_push_error(0, "can only save 1 or 3 channel images to pnm");
-    mm_log((1,"i_writeppm: ppm/pgm is 1 or 3 channel only (current image is %d)\n",im->channels));
-    return(0);
+  if (i_io_close(ig)) {
+    i_push_errorf(i_io_error(ig), "Error closing stream: %d", i_io_error(ig));
+    return 0;
   }
-  ig->closecb(ig);
 
   return(1);
 }
@@ -508,7 +846,8 @@ i_writeppm_wiol(i_img *im, io_glue *ig) {
 
 =head1 AUTHOR
 
-Arnar M. Hrafnkelsson <addi@umich.edu>
+Arnar M. Hrafnkelsson <addi@umich.edu>, Tony Cook <tonyc@cpan.org>,
+Philip Gwyn <gwyn@cpan.org>.
 
 =head1 SEE ALSO