add support for file write plugins
authorTony Cook <tony@develop=help.com>
Mon, 26 Jun 2006 12:20:27 +0000 (12:20 +0000)
committerTony Cook <tony@develop=help.com>
Mon, 26 Jun 2006 12:20:27 +0000 (12:20 +0000)
fix write_multi() writing to scalars
  https://rt.cpan.org/Ticket/Display.html?id=19982

add support for writing to ICO/CUR image files

added file limit check functions to the API

34 files changed:
ICO/ICO.pm
ICO/ICO.xs
ICO/Makefile.PL
ICO/imicon.c
ICO/imicon.h
ICO/lib/Imager/File/CUR.pm [new file with mode: 0644]
ICO/msicon.c
ICO/msicon.h
ICO/t/t10icon.t
ICO/t/t30cursor.t [new file with mode: 0644]
ICO/t/t40readcurone.t [new file with mode: 0644]
ICO/t/t41curmultread.t [new file with mode: 0644]
ICO/t/t50readfail.t [new file with mode: 0644]
ICO/t/t60writefail.t [new file with mode: 0644]
ICO/t/t70icosing.t [new file with mode: 0644]
ICO/t/t71icomult.t [new file with mode: 0644]
ICO/t/t72cursing.t [new file with mode: 0644]
ICO/t/t73curmult.t [new file with mode: 0644]
ICO/testimg/pal43232.cur [new file with mode: 0755]
Imager.pm
Imager.xs
MANIFEST
MANIFEST.SKIP
Makefile.PL
image.c
imager.h
imageri.h
imext.c
imext.h
imexttypes.h
iolayer.c
iolayert.h
lib/Imager/Files.pod
t/t106tiff.t

index 6f038d6..1b187a9 100644 (file)
@@ -46,6 +46,88 @@ Imager->register_reader
    },
   );
 
+# the readers can read CUR files too
+Imager->register_reader
+  (
+   type=>'cur',
+   single => 
+   sub { 
+     my ($im, $io, %hsh) = @_;
+     $im->{IMG} = i_readico_single($io, $hsh{page} || 0);
+
+     unless ($im->{IMG}) {
+       $im->_set_error(Imager->_error_as_msg);
+       return;
+     }
+     return $im;
+   },
+   multiple =>
+   sub {
+     my ($io, %hsh) = @_;
+     
+     my @imgs = i_readico_multi($io);
+     unless (@imgs) {
+       Imager->_set_error(Imager->_error_as_msg);
+       return;
+     }
+     return map { 
+       bless { IMG => $_, DEBUG => $Imager::DEBUG, ERRSTR => undef }, 'Imager'
+     } @imgs;
+   },
+  );
+
+Imager->register_writer
+  (
+   type=>'ico',
+   single => 
+   sub { 
+     my ($im, $io, %hsh) = @_;
+
+     unless (i_writeico_wiol($io, $im->{IMG})) {
+       $im->_set_error(Imager->_error_as_msg);
+       return;
+     }
+     return $im;
+   },
+   multiple =>
+   sub {
+     my ($class, $io, $opts, @images) = @_;
+
+     if (!i_writeico_multi_wiol($io, map $_->{IMG}, @images)) {
+       Imager->_set_error(Imager->_error_as_msg);
+       return;
+     }
+
+     return 1;
+   },
+  );
+
+Imager->register_writer
+  (
+   type=>'cur',
+   single => 
+   sub { 
+     my ($im, $io, %hsh) = @_;
+
+     unless (i_writecur_wiol($io, $im->{IMG})) {
+       $im->_set_error(Imager->_error_as_msg);
+       return;
+     }
+     return $im;
+   },
+   multiple =>
+   sub {
+     my ($class, $io, $opts, @images) = @_;
+
+     if (!i_writecur_multi_wiol($io, map $_->{IMG}, @images)) {
+       Imager->_set_error(Imager->_error_as_msg);
+       return;
+     }
+
+     return 1;
+   },
+  );
+
 1;
 
 __END__
@@ -65,12 +147,20 @@ Imager::File::ICO - read MS Icon files
   my @imgs = Imager->read_multi(file => "foo.ico")
     or die Imager->errstr;
 
-=head1 DESCRIPTION
+  $img->write(file => "foo.ico")
+    or die $img->errstr;
 
+  Imager->write_multi({ file => "foo.ico" }, @imgs)
+    or die Imager->errstr;
+
+=head1 DESCRIPTION
 
+Imager's MS Icon support is documented in L<Imager::Files>.
 
 =head1 AUTHOR
 
+Tony Cook <tony@imager.perl.org>
+
 =head1 SEE ALSO
 
 Imager, Imager::Files.
index d33bd2b..bb23981 100644 (file)
@@ -35,6 +35,97 @@ i_readico_multi(ig)
           myfree(imgs);
         }
 
+int
+i_writeico_wiol(ig, im)
+       Imager::IO ig
+       Imager::ImgRaw im
+
+undef_int
+i_writeico_multi_wiol(ig, ...)
+        Imager::IO     ig
+      PREINIT:
+        int i;
+        int img_count;
+        i_img **imgs;
+      CODE:
+        if (items < 2)
+          croak("Usage: i_writeico_multi_wiol(ig, images...)");
+        img_count = items - 1;
+        RETVAL = 1;
+       if (img_count < 1) {
+         RETVAL = 0;
+         i_clear_error();
+         i_push_error(0, "You need to specify images to save");
+       }
+       else {
+          imgs = mymalloc(sizeof(i_img *) * img_count);
+          for (i = 0; i < img_count; ++i) {
+           SV *sv = ST(1+i);
+           imgs[i] = NULL;
+           if (SvROK(sv) && sv_derived_from(sv, "Imager::ImgRaw")) {
+             imgs[i] = INT2PTR(i_img *, SvIV((SV*)SvRV(sv)));
+           }
+           else {
+             i_clear_error();
+             i_push_error(0, "Only images can be saved");
+              myfree(imgs);
+             RETVAL = 0;
+             break;
+            }
+         }
+          if (RETVAL) {
+           RETVAL = i_writeico_multi_wiol(ig, imgs, img_count);
+          }
+         myfree(imgs);
+       }
+      OUTPUT:
+        RETVAL
+
+int
+i_writecur_wiol(ig, im)
+       Imager::IO ig
+       Imager::ImgRaw im
+
+undef_int
+i_writecur_multi_wiol(ig, ...)
+        Imager::IO     ig
+      PREINIT:
+        int i;
+        int img_count;
+        i_img **imgs;
+      CODE:
+        if (items < 2)
+          croak("Usage: i_writecur_multi_wiol(ig, images...)");
+        img_count = items - 1;
+        RETVAL = 1;
+       if (img_count < 1) {
+         RETVAL = 0;
+         i_clear_error();
+         i_push_error(0, "You need to specify images to save");
+       }
+       else {
+          imgs = mymalloc(sizeof(i_img *) * img_count);
+          for (i = 0; i < img_count; ++i) {
+           SV *sv = ST(1+i);
+           imgs[i] = NULL;
+           if (SvROK(sv) && sv_derived_from(sv, "Imager::ImgRaw")) {
+             imgs[i] = INT2PTR(i_img *, SvIV((SV*)SvRV(sv)));
+           }
+           else {
+             i_clear_error();
+             i_push_error(0, "Only images can be saved");
+              myfree(imgs);
+             RETVAL = 0;
+             break;
+            }
+         }
+          if (RETVAL) {
+           RETVAL = i_writecur_multi_wiol(ig, imgs, img_count);
+          }
+         myfree(imgs);
+       }
+      OUTPUT:
+        RETVAL
 
 BOOT:
        PERL_INITIALIZE_IMAGER_CALLBACKS;
index aa037ae..346d178 100644 (file)
@@ -1,3 +1,5 @@
+#!perl -w
+use strict;
 use ExtUtils::MakeMaker;
 
 my %opts = 
index 5655536..00b516a 100644 (file)
@@ -1,6 +1,7 @@
 #include "imext.h"
 #include "imicon.h"
 #include "msicon.h"
+#include <string.h>
 
 static void
 ico_push_error(int error) {
@@ -19,24 +20,30 @@ read_one_icon(ico_reader_t *file, int index) {
 
   image = ico_image_read(file, index, &error);
   if (!image) {
-    ico_reader_close(file);
     ico_push_error(error);
+    i_push_error(0, "error reading ICO/CUR image");
     return NULL;
   }
 
   if (image->direct) {
     int x, y;
-    i_color *line_buf = mymalloc(image->width * sizeof(i_color));
+    i_color *line_buf;
     i_color *outp;
     ico_color_t *inp = image->image_data;
 
+    if (!i_int_check_image_file_limits(image->width, image->height, 4, 1)) {
+      ico_image_release(image);
+      return NULL;
+    }
+
     result = i_img_8_new(image->width, image->height, 4);
     if (!result) {
       ico_image_release(image);
-      ico_reader_close(file);
       return NULL;
     }
 
+    line_buf = mymalloc(image->width * sizeof(i_color));
+
     for (y = 0; y < image->height; ++y) {
       outp = line_buf;
       for (x = 0; x < image->width; ++x) {
@@ -57,10 +64,14 @@ read_one_icon(ico_reader_t *file, int index) {
     int y;
     unsigned char *image_data;
 
+    if (!i_int_check_image_file_limits(image->width, image->height, 3, 1)) {
+      ico_image_release(image);
+      return NULL;
+    }
+
     result = i_img_pal_new(image->width, image->height, 3, 256);
     if (!result) {
       ico_image_release(image);
-      ico_reader_close(file);
       return NULL;
     }
     
@@ -75,7 +86,6 @@ read_one_icon(ico_reader_t *file, int index) {
       if (i_addcolors(result, &c, 1) < 0) {
        i_push_error(0, "could not add color to palette");
        ico_image_release(image);
-       ico_reader_close(file);
        i_img_destroy(result);
        return NULL;
       }
@@ -111,13 +121,23 @@ read_one_icon(ico_reader_t *file, int index) {
     }
     *outp++ = '\0';
 
-    i_tags_set(&result->tags, "ico_mask", mask, (outp-mask)-1);
+    if (ico_type(file) == ICON_ICON)
+      i_tags_set(&result->tags, "ico_mask", mask, (outp-mask)-1);
+    else
+      i_tags_set(&result->tags, "cur_mask", mask, (outp-mask)-1);
     
     myfree(mask);
   }
-  i_tags_setn(&result->tags, "ico_bits", image->bit_count);
-  i_tags_set(&result->tags, "ico_type", ico_type(file) == ICON_ICON ? "icon" : "cursor", -1);
-  i_tags_set(&result->tags, "i_format", "ico", 3);
+  if (ico_type(file) == ICON_ICON) {
+    i_tags_setn(&result->tags, "ico_bits", image->bit_count);
+    i_tags_set(&result->tags, "i_format", "ico", 3);
+  }
+  else {
+    i_tags_setn(&result->tags, "cur_bits", image->bit_count);
+    i_tags_set(&result->tags, "i_format", "cur", 3);
+    i_tags_setn(&result->tags, "cur_hotspotx", image->hotspot_x);
+    i_tags_setn(&result->tags, "cur_hotspoty", image->hotspot_y);
+  }
 
   ico_image_release(image);
 
@@ -130,16 +150,16 @@ i_readico_single(io_glue *ig, int index) {
   i_img *result;
   int error;
 
+  i_clear_error();
+
   file = ico_reader_open(ig, &error);
   if (!file) {
     ico_push_error(error);
+    i_push_error(0, "error opening ICO/CUR file");
     return NULL;
   }
 
-  if (index < 0 && index >= ico_image_count(file)) {
-    i_push_error(0, "page out of range");
-    return NULL;
-  }
+  /* the index is range checked by msicon.c - don't duplicate it here */
 
   result = read_one_icon(file, index);
   ico_reader_close(file);
@@ -154,9 +174,12 @@ i_readico_multi(io_glue *ig, int *count) {
   int error;
   i_img **imgs;
 
+  i_clear_error();
+
   file = ico_reader_open(ig, &error);
   if (!file) {
     ico_push_error(error);
+    i_push_error(0, "error opening ICO/CUR file");
     return NULL;
   }
 
@@ -165,7 +188,7 @@ i_readico_multi(io_glue *ig, int *count) {
   *count = 0;
   for (index = 0; index < ico_image_count(file); ++index) {
     i_img *im = read_one_icon(file, index);
-    if (!im) 
+    if (!im)
       break;
 
     imgs[(*count)++] = im;
@@ -180,3 +203,402 @@ i_readico_multi(io_glue *ig, int *count) {
 
   return imgs;
 }
+
+static int
+validate_image(i_img *im) {
+  if (im->xsize > 255 || im->ysize > 255) {
+    i_push_error(0, "image too large for ico file");
+    return 0;
+  }
+  if (im->channels < 1 || im->channels > 4) {
+    /* this shouldn't happen, but check anyway */
+    i_push_error(0, "invalid channels");
+    return 0;
+  }
+
+  return 1;
+}
+
+static int
+translate_mask(i_img *im, unsigned char *out, const char *in) {
+  int x, y;
+  int one, zero;
+  int len = strlen(in);
+  int pos;
+  int newline; /* set to the first newline type we see */
+  int notnewline; /* set to whatever in ( "\n\r" newline isn't ) */
+
+  if (len < 3)
+    return 0;
+
+  zero = in[0];
+  one = in[1];
+  if (in[2] == '\n' || in[2] == '\r') {
+    newline = in[2];
+    notnewline = '\n' + '\r' - newline;
+  }
+  else {
+    return 0;
+  }
+
+  pos = 3;
+  y = 0;
+  while (y < im->ysize && pos < len) {
+    x = 0;
+    while (x < im->xsize && pos < len) {
+      if (in[pos] == newline) {
+       /* don't process it, we look for it later */
+       break;
+      }
+      else if (in[pos] == notnewline) {
+       ++pos; /* just drop it */
+      }
+      else if (in[pos] == one) {
+       *out++ = 1;
+        ++x;
+       ++pos;
+      }
+      else if (in[pos] == zero) {
+       *out++ = 0;
+        ++x;
+       ++pos;
+      }
+      else if (in[pos] == ' ' || in[pos] == '\t') {
+       /* just ignore whitespace */
+       ++pos;
+      }
+      else {
+       return 0;
+      }
+    }
+    while (x++ < im->xsize) {
+      *out++ = 0;
+    }
+    while (pos < len && in[pos] != newline)
+      ++pos;
+    if (pos < len && in[pos] == newline)
+      ++pos; /* actually skip the newline */
+
+    ++y;
+  }
+  while (y++ < im->ysize) {
+    for (x = 0; x < im->xsize; ++x)
+      *out++ = 0;
+  }
+
+  return 1;
+}
+
+static void 
+derive_mask(i_img *im, ico_image_t *ico) {
+
+  if (im->channels == 1 || im->channels == 3) {
+    /* msicon.c's default mask is what we want */
+    myfree(ico->mask_data);
+    ico->mask_data = NULL;
+  }
+  else {
+    int channel = im->channels - 1;
+    i_sample_t *linebuf = mymalloc(sizeof(i_sample_t) * im->xsize);
+    int x, y;
+    unsigned char *out = ico->mask_data;
+
+    for (y = 0; y < im->ysize; ++y) {
+      i_gsamp(im, 0, im->xsize, y, linebuf, &channel, 1);
+      for (x = 0; x < im->xsize; ++x) {
+       *out++ = linebuf[x] == 255 ? 0 : 1;
+      }
+    }
+    myfree(linebuf);
+  }
+}
+
+static void
+fill_image_base(i_img *im, ico_image_t *ico, const char *mask_name) {
+  int x, y;
+
+  ico->width = im->xsize;
+  ico->height = im->ysize;
+  ico->direct = im->type == i_direct_type;
+  if (ico->direct) {
+    int channels[4];
+    int set_alpha = 0;
+    ico_color_t *out;
+    i_sample_t *in;
+    unsigned char *linebuf = mymalloc(ico->width * 4);
+    ico->image_data = mymalloc(sizeof(ico_color_t) * ico->width * ico->height);
+    
+    switch (im->channels) {
+    case 1:
+      channels[0] = channels[1] = channels[2] = channels[3] = 0;
+      ++set_alpha;
+      break;
+
+    case 2:
+      channels[0] = channels[1] = channels[2] = 0;
+      channels[3] = 1;
+      break;
+
+    case 3:
+      channels[0] = 0;
+      channels[1] = 1;
+      channels[2] = 2;
+      channels[3] = 2;
+      ++set_alpha;
+      break;
+
+    case 4:
+      channels[0] = 0;
+      channels[1] = 1;
+      channels[2] = 2;
+      channels[3] = 3;
+      break;
+    }
+    
+    out = ico->image_data;
+    for (y = 0; y < im->ysize; ++y) {
+      i_gsamp(im, 0, im->xsize, y, linebuf, channels, 4);
+      in = linebuf;
+      for (x = 0; x < im->xsize; ++x) {
+       out->r = *in++;
+       out->g = *in++;
+       out->b = *in++;
+       out->a = set_alpha ? 255 : *in;
+       in++;
+       ++out;
+      }
+    }
+    myfree(linebuf);
+    ico->palette = NULL;
+  }
+  else {
+    unsigned char *out;
+    i_color *colors;
+    int i;
+    i_palidx *in;
+    i_palidx *linebuf = mymalloc(sizeof(i_palidx) * ico->width);
+
+    ico->image_data = mymalloc(sizeof(ico_color_t) * ico->width * ico->height);
+
+    out = ico->image_data;
+    for (y = 0; y < im->ysize; ++y) {
+      i_gpal(im, 0, im->xsize, y, linebuf);
+      in = linebuf;
+      for (x = 0; x < im->xsize; ++x) {
+       *out++ = *in++;
+      }
+    }
+    myfree(linebuf);
+
+    ico->palette_size = i_colorcount(im);
+    ico->palette = mymalloc(sizeof(ico_color_t) * ico->palette_size);
+    colors = mymalloc(sizeof(i_color) * ico->palette_size);
+    i_getcolors(im, 0, colors, ico->palette_size);
+    for (i = 0; i < ico->palette_size; ++i) {
+      if (im->channels == 1 || im->channels == 2) {
+       ico->palette[i].r = ico->palette[i].g =
+         ico->palette[i].b = colors[i].rgba.r;
+      }
+      else {
+       ico->palette[i].r = colors[i].rgba.r;
+       ico->palette[i].g = colors[i].rgba.g;
+       ico->palette[i].b = colors[i].rgba.b;
+      }
+    }
+    myfree(colors);
+  }
+
+  {
+    /* build the mask */
+    int mask_index;
+
+    ico->mask_data = mymalloc(im->xsize * im->ysize);
+
+    if (!i_tags_find(&im->tags, mask_name, 0, &mask_index)
+        || !im->tags.tags[mask_index].data
+        || !translate_mask(im, ico->mask_data, 
+                           im->tags.tags[mask_index].data)) {
+      derive_mask(im, ico);
+    }
+  }
+}
+
+static void
+unfill_image(ico_image_t *ico) {
+  myfree(ico->image_data);
+  if (ico->palette)
+    myfree(ico->palette);
+  if (ico->mask_data)
+    myfree(ico->mask_data);
+}
+
+static void
+fill_image_icon(i_img *im, ico_image_t *ico) {
+  fill_image_base(im, ico, "ico_mask");
+  ico->hotspot_x = ico->hotspot_y = 0;
+}
+
+int
+i_writeico_wiol(i_io_glue_t *ig, i_img *im) {
+  ico_image_t ico;
+  int error;
+
+  i_clear_error();
+
+  if (!validate_image(im))
+    return 0;
+
+  fill_image_icon(im, &ico);
+
+  if (!ico_write(ig, &ico, 1, ICON_ICON, &error)) {
+    ico_push_error(error);
+    unfill_image(&ico);
+    return 0;
+  }
+
+  unfill_image(&ico);
+
+  if (i_io_close(ig) < 0) {
+    i_push_error(0, "error closing output");
+    return 0;
+  }
+
+  return 1;
+}
+
+int
+i_writeico_multi_wiol(i_io_glue_t *ig, i_img **ims, int count) {
+  ico_image_t *icons;
+  int error;
+  int i;
+
+  i_clear_error();
+
+  if (count > 0xFFFF) {
+    i_push_error(0, "too many images for ico files");
+    return 0;
+  }
+
+  for (i = 0; i < count; ++i)
+    if (!validate_image(ims[i]))
+      return 0;
+
+  icons = mymalloc(sizeof(ico_image_t) * count);
+
+  for (i = 0; i < count; ++i)
+    fill_image_icon(ims[i], icons + i);
+
+  if (!ico_write(ig, icons, count, ICON_ICON, &error)) {
+    ico_push_error(error);
+    for (i = 0; i < count; ++i)
+      unfill_image(icons + i);
+    myfree(icons);
+    return 0;
+  }
+
+  for (i = 0; i < count; ++i)
+    unfill_image(icons + i);
+  myfree(icons);
+
+  if (i_io_close(ig) < 0) {
+    i_push_error(0, "error closing output");
+    return 0;
+  }
+
+  return 1;
+}
+
+void
+fill_image_cursor(i_img *im, ico_image_t *ico) {
+  int hotx, hoty;
+  fill_image_base(im, ico, "ico_mask");
+
+  if (!i_tags_get_int(&im->tags, "cur_hotspotx", 0, &hotx))
+    hotx = 0;
+  if (!i_tags_get_int(&im->tags, "cur_hotspoty", 0, &hoty))
+    hoty = 0;
+
+  if (hotx < 0)
+    hotx = 0;
+  else if (hotx >= im->xsize)
+    hotx = im->xsize - 1;
+
+  if (hoty < 0)
+    hoty = 0;
+  else if (hoty >= im->ysize)
+    hoty = im->ysize - 1;
+  
+  ico->hotspot_x = hotx;
+  ico->hotspot_y = hoty;
+}
+
+int
+i_writecur_wiol(i_io_glue_t *ig, i_img *im) {
+  ico_image_t ico;
+  int error;
+
+  i_clear_error();
+
+  if (!validate_image(im))
+    return 0;
+
+  fill_image_cursor(im, &ico);
+
+  if (!ico_write(ig, &ico, 1, ICON_CURSOR, &error)) {
+    ico_push_error(error);
+    unfill_image(&ico);
+    return 0;
+  }
+
+  unfill_image(&ico);
+
+  if (i_io_close(ig) < 0) {
+    i_push_error(0, "error closing output");
+    return 0;
+  }
+
+  return 1;
+}
+
+int
+i_writecur_multi_wiol(i_io_glue_t *ig, i_img **ims, int count) {
+  ico_image_t *icons;
+  int error;
+  int i;
+
+  i_clear_error();
+
+  if (count > 0xFFFF) {
+    i_push_error(0, "too many images for ico files");
+    return 0;
+  }
+
+  for (i = 0; i < count; ++i)
+    if (!validate_image(ims[i]))
+      return 0;
+
+  icons = mymalloc(sizeof(ico_image_t) * count);
+
+  for (i = 0; i < count; ++i)
+    fill_image_cursor(ims[i], icons + i);
+
+  if (!ico_write(ig, icons, count, ICON_CURSOR, &error)) {
+    ico_push_error(error);
+    for (i = 0; i < count; ++i)
+      unfill_image(icons + i);
+    myfree(icons);
+    return 0;
+  }
+
+  for (i = 0; i < count; ++i)
+    unfill_image(icons + i);
+  myfree(icons);
+
+  if (i_io_close(ig) < 0) {
+    i_push_error(0, "error closing output");
+    return 0;
+  }
+
+  return 1;
+}
+
index 855e7c9..bde4821 100644 (file)
@@ -8,4 +8,16 @@ i_readico_single(io_glue *ig, int index);
 extern i_img **
 i_readico_multi(io_glue *ig, int *count);
 
+extern int
+i_writeico_wiol(i_io_glue_t *ig, i_img *im);
+
+extern int
+i_writeico_multi_wiol(i_io_glue_t *ig, i_img **im, int count);
+
+extern int
+i_writecur_wiol(i_io_glue_t *ig, i_img *im);
+
+extern int
+i_writecur_multi_wiol(i_io_glue_t *ig, i_img **im, int count);
+
 #endif
diff --git a/ICO/lib/Imager/File/CUR.pm b/ICO/lib/Imager/File/CUR.pm
new file mode 100644 (file)
index 0000000..7c3570c
--- /dev/null
@@ -0,0 +1,7 @@
+package Imager::File::CUR;
+use strict;
+
+# all the work is done by Imager::File::ICO
+use Imager::File::ICO;
+
+1;
index 5e07fde..e8e2d89 100644 (file)
@@ -3,7 +3,10 @@
 #include <stdlib.h>
 #include <stdio.h>
 #include <stdarg.h>
+#include <assert.h>
 
+static
+int read_packed(io_glue *ig, const char *format, ...);
 static int 
 read_palette(ico_reader_t *file, ico_image_t *image, int *error);
 static int 
@@ -16,12 +19,31 @@ static int
 read_1bit_data(ico_reader_t *file, ico_image_t *image, int *error);
 static int 
 read_mask(ico_reader_t *file, ico_image_t *image, int *error);
+static int
+ico_write_validate(ico_image_t const *images, int image_count, int *error);
+static int
+ico_image_size(ico_image_t const *image, int *bits, int *colors);
+static int
+write_packed(i_io_glue_t *ig, char const *format, ...);
+static int
+write_palette(i_io_glue_t *ig, ico_image_t const *image, int *error);
+static int
+write_32_bit(i_io_glue_t *ig, ico_image_t const *image, int *error);
+static int
+write_8_bit(i_io_glue_t *ig, ico_image_t const *image, int *error);
+static int
+write_4_bit(i_io_glue_t *ig, ico_image_t const *image, int *error);
+static int
+write_1_bit(i_io_glue_t *ig, ico_image_t const *image, int *error);
+static int
+write_mask(i_io_glue_t *ig, ico_image_t const *image, int *error);
 
 typedef struct {
   int width;
   int height;
   long offset;
   long size;
+  int hotspot_x, hotspot_y;
 } ico_reader_image_entry;
 
 /* this was previously declared, now define it */
@@ -39,9 +61,6 @@ struct ico_reader_tag {
   ico_reader_image_entry *images;
 };
 
-static
-int read_packed(io_glue *ig, const char *format, ...);
-
 /*
 =head1 NAME 
 
@@ -133,13 +152,31 @@ ico_reader_open(i_io_glue_t *ig, int *error) {
 
   for (i = 0; i < count; ++i) {
     long width, height, bytes_in_res, image_offset;
+
     ico_reader_image_entry *image = file->images + i;
-    if (!read_packed(ig, "bbxxxxxxdd", &width, &height, &bytes_in_res, 
-                    &image_offset)) {
-      free(file->images);
-      free(file);
-      *error = ICOERR_Short_File;
-      return NULL;
+    if (type == ICON_ICON) {
+      if (!read_packed(ig, "bb xxxxxx dd", &width, &height, &bytes_in_res, 
+                      &image_offset)) {
+       free(file->images);
+       free(file);
+       *error = ICOERR_Short_File;
+       return NULL;
+      }
+      image->hotspot_x = image->hotspot_y = 0;
+    }
+    else {
+      long hotspot_x, hotspot_y;
+
+      if (!read_packed(ig, "bb xx ww dd", &width, &height, 
+                      &hotspot_x, &hotspot_y, &bytes_in_res, 
+                      &image_offset)) {
+       free(file->images);
+       free(file);
+       *error = ICOERR_Short_File;
+       return NULL;
+      }
+      image->hotspot_x = hotspot_x;
+      image->hotspot_y = hotspot_y;
     }
 
     image->width = width;
@@ -168,7 +205,7 @@ ico_image_count(ico_reader_t *file) {
 /*
 =item ico_type
 
-  // type of file - 1 for icon, 2 for cursor
+  // type of file - ICON_ICON for icon, ICON_CURSOR for cursor
   type = ico_type(file);
 
 =cut
@@ -219,6 +256,11 @@ ico_image_read(ico_reader_t *file, int index, int *error) {
     return NULL;
   }
 
+  if (bit_count != 1 && bit_count != 4 && bit_count != 8 && bit_count != 32) {
+    *error = ICOERR_Unknown_Bits;
+    return 0;
+  }
+
   result = malloc(sizeof(ico_image_t));
   if (!result) {
     *error = ICOERR_Out_Of_Memory;
@@ -231,8 +273,10 @@ ico_image_read(ico_reader_t *file, int index, int *error) {
   result->palette = NULL;
   result->image_data = NULL;
   result->mask_data = NULL;
+  result->hotspot_x = im->hotspot_x;
+  result->hotspot_y = im->hotspot_y;
     
-  if (result->direct) {
+  if (bit_count == 32) {
     result->palette_size = 0;
 
     result->image_data = malloc(result->width * result->height * sizeof(ico_color_t));
@@ -241,15 +285,7 @@ ico_image_read(ico_reader_t *file, int index, int *error) {
       *error = ICOERR_Out_Of_Memory;
       return NULL;
     }
-    if (bit_count == 32) {
-      if (!read_32bit_data(file, result, error)) {
-       free(result->image_data);
-       free(result);
-       return NULL;
-      }
-    }
-    else {
-      *error = ICOERR_Unknown_Bits;
+    if (!read_32bit_data(file, result, error)) {
       free(result->image_data);
       free(result);
       return NULL;
@@ -257,15 +293,23 @@ ico_image_read(ico_reader_t *file, int index, int *error) {
   }
   else {
     int read_result;
+
     result->palette_size = 1 << bit_count;
     result->palette = malloc(sizeof(ico_color_t) * result->palette_size);
-    result->image_data = malloc(result->width * result->height);
     if (!result->palette) {
       free(result);
       *error = ICOERR_Out_Of_Memory;
       return NULL;
     }
 
+    result->image_data = malloc(result->width * result->height);
+    if (!result->image_data) {
+      *error = ICOERR_Out_Of_Memory;
+      free(result->palette);
+      free(result);
+      return 0;
+    }      
+    
     if (!read_palette(file, result, error)) {
       free(result->palette);
       free(result->image_data);
@@ -287,8 +331,8 @@ ico_image_read(ico_reader_t *file, int index, int *error) {
       break;
 
     default:
+      assert(0); /* this can't happen in theory */
       read_result = 0;
-      *error = ICOERR_Unknown_Bits;
       break;
     }
 
@@ -339,7 +383,7 @@ ico_image_release(ico_image_t *image) {
 /*
 =item ico_reader_close
 
-Releases the file structure.
+Releases the read file structure.
 
 =cut
 */
@@ -352,6 +396,141 @@ ico_reader_close(ico_reader_t *file) {
 }
 
 /*
+=back
+
+=head1 WRITING ICON FILES
+
+=over
+
+=item ico_write(ig, images, image_count, type, &error)
+
+Parameters:
+
+=over
+
+=item *
+
+io_glue *ig - an Imager IO object.  This only needs to implement
+writing for ico_write()
+
+=item *
+
+ico_image_t *images - array of images to be written.
+
+=item *
+
+int image_count - number of images
+
+=item *
+
+int type - must be ICON_ICON or ICON_CURSOR
+
+=item *
+
+int *error - set to an error code on failure.
+
+=back
+
+Returns non-zero on success.
+
+=cut
+*/
+
+int
+ico_write(i_io_glue_t *ig, ico_image_t const *images, int image_count,
+         int type, int *error) {
+  int i;
+  int start_offset = 6 + 16 * image_count;
+  int current_offset = start_offset;
+
+  if (type != ICON_ICON && type != ICON_CURSOR) {
+    *error = ICOERR_Bad_File_Type;
+    return 0;
+  }
+
+  /* validate the images */
+  if (!ico_write_validate(images, image_count, error))
+    return 0;
+
+  /* write the header */
+  if (!write_packed(ig, "www", 0, type, image_count)) {
+    *error = ICOERR_Write_Failure;
+    return 0;
+  }
+
+  /* work out the offsets of each image */
+  for (i = 0; i < image_count; ++i) {
+    ico_image_t const *image = images + i;
+    int bits, colors;
+    int size = ico_image_size(image, &bits, &colors);
+
+    if (type == ICON_ICON) {
+      if (!write_packed(ig, "bbbbwwdd", image->width, image->height,
+                       colors, 0, 1, bits, (unsigned long)size, 
+                       (unsigned long)current_offset)) {
+       *error = ICOERR_Write_Failure;
+       return 0;
+      }
+    }
+    else {
+      int hotspot_x = image->hotspot_x;
+      int hotspot_y = image->hotspot_y;
+
+      if (hotspot_x < 0)
+       hotspot_x = 0;
+      else if (hotspot_x >= image->width)
+       hotspot_x = image->width - 1;
+      if (hotspot_y < 0)
+       hotspot_y = 0;
+      else if (hotspot_y >= image->height)
+       hotspot_y = image->height - 1;
+
+      if (!write_packed(ig, "bbbbwwdd", image->width, image->height,
+                       colors, 0, hotspot_x, hotspot_y, (unsigned long)size, 
+                       (unsigned long)current_offset)) {
+       *error = ICOERR_Write_Failure;
+       return 0;
+      }
+    }
+    current_offset += size;
+  }
+  
+  /* write out each image */
+  for (i = 0; i < image_count; ++i) {
+    ico_image_t const *image = images + i;
+
+    if (image->direct) {
+      if (!write_32_bit(ig, image, error))
+       return 0;
+    }
+    else {
+      if (image->palette_size <= 2) {
+       if (!write_1_bit(ig, image, error))
+         return 0;
+      }
+      else if (image->palette_size <= 16) {
+       if (!write_4_bit(ig, image, error))
+         return 0;
+      }
+      else {
+       if (!write_8_bit(ig, image, error))
+         return 0;
+      }
+    }
+    if (!write_mask(ig, image, error))
+      return 0;
+  }
+
+  return 1;
+}
+
+/*
+=back
+
+=head1 ERROR MESSAGES
+
+=over
+
 =item ico_error_message
 
 Converts an error code into an error message.
@@ -373,6 +552,10 @@ ico_error_message(int error, char *buffer, size_t buffer_size) {
     msg = "I/O error";
     break;
 
+  case ICOERR_Write_Failure:
+    msg = "Write failure";
+    break;
+
   case ICOERR_Invalid_File:
     msg = "Not an icon file";
     break;
@@ -385,6 +568,26 @@ ico_error_message(int error, char *buffer, size_t buffer_size) {
     msg = "Image index out of range";
     break;
 
+  case ICOERR_Bad_File_Type:
+    msg = "Bad file type parameter";
+    break;
+
+  case ICOERR_Invalid_Width:
+    msg = "Invalid image width";
+    break;
+
+  case ICOERR_Invalid_Height:
+    msg = "Invalid image height";
+    break;
+    
+  case ICOERR_Invalid_Palette:
+    msg = "Invalid Palette";
+    break;
+
+  case ICOERR_No_Data:
+    msg = "No image data in image supplied to ico_write";
+    break;
+
   case ICOERR_Out_Of_Memory:
     msg = "Out of memory";
     break;
@@ -725,6 +928,7 @@ read_mask(ico_reader_t *file, ico_image_t *image, int *error) {
   unsigned char *read_buffer = malloc(line_bytes);
   int y;
   int x;
+  int mask;
   unsigned char *inp, *outp;
 
   if (!read_buffer) {
@@ -741,10 +945,14 @@ read_mask(ico_reader_t *file, ico_image_t *image, int *error) {
     
     outp = image->mask_data + y * image->width;
     inp = read_buffer;
+    mask = 0x80;
     for (x = 0; x < image->width; ++x) {
-      *outp++ = (*inp >> (7 - (x & 7))) & 1;
-      if ((x & 7) == 7)
+      *outp++ = (*inp & mask) ? 1 : 0;
+      mask >>= 1;
+      if (!mask) {
+        mask = 0x80;
        ++inp;
+      }
     }
   }
   free(read_buffer);
@@ -752,6 +960,506 @@ read_mask(ico_reader_t *file, ico_image_t *image, int *error) {
   return 1;
 }
 
+/*
+=item ico_write_validate
+
+Check each image to make sure it can go into an icon file.
+
+=cut
+*/
+
+static int
+ico_write_validate(ico_image_t const *images, int image_count, int *error) {
+  int i;
+
+  for (i = 0; i < image_count; ++i) {
+    ico_image_t const *image = images + i;
+
+    if (image->width < 1 || image->width > 255) {
+      *error = ICOERR_Invalid_Width;
+      return 0;
+    }
+    if (image->height < 1 || image->height > 255) {
+      *error = ICOERR_Invalid_Height;
+      return 0;
+    }
+    if (!image->image_data) {
+      *error = ICOERR_No_Data;
+      return 0;
+    }
+    if (!image->direct) {
+      if (image->palette_size < 0 || image->palette_size > 256 
+         || !image->palette) {
+       *error = ICOERR_Invalid_Palette;
+       return 0;
+      }
+    }
+  }
+
+  return 1;
+}
+
+/*
+=item ico_image_size
+
+Calculate how much space the icon takes up in the file.
+
+=cut
+*/
+
+static int
+ico_image_size(ico_image_t const *image, int *bits, int *colors) {
+  int size = 40; /* start with the BITMAPINFOHEADER */
+
+  /* add in the image area */
+  if (image->direct) {
+    *bits = 32;
+    *colors = 0;
+    size += image->width * 4 * image->height;
+  }
+  else {
+    if (image->palette_size <= 2) {
+      *bits = 1;
+      *colors = 2;
+    }
+    else if (image->palette_size <= 16) {
+      *bits = 4;
+      *colors = 16;
+    }
+    else {
+      *bits = 8;
+      *colors = 0;
+    }
+
+    /* palette size */
+    size += *colors * 4;
+
+    /* image data size */
+    size += (image->width * *bits + 31) / 32 * 4 * image->height;
+  }
+
+  /* add in the mask */
+  size += (image->width + 31) / 32 * 4 * image->height;
+
+  return size;
+}
+
+/*
+=item write_packed
+
+Pack numbers given a format to a stream.
+
+=cut
+*/
+
+static int 
+write_packed(i_io_glue_t *ig, char const *format, ...) {
+  unsigned char buffer[100];
+  va_list ap;
+  unsigned long p;
+  int size;
+  const char *formatp;
+  unsigned char *bufp;
+
+  /* write efficiently, work out the size of the buffer */
+  size = 0;
+  formatp = format;
+  while (*formatp) {
+    switch (*formatp++) {
+    case 'b': size++; break;
+    case 'w': size += 2; break;
+    case 'd': size += 4; break;
+    case ' ': break; /* space to separate components */
+    default:
+      fprintf(stderr, "invalid unpack char in %s\n", format);
+      exit(1);
+    }
+  }
+
+  if (size > sizeof(buffer)) {
+    /* catch if we need a bigger buffer, but 100 is plenty */
+    fprintf(stderr, "format %s too long for buffer\n", format);
+    exit(1);
+  }
+
+  va_start(ap, format);
+
+  bufp = buffer;
+  while (*format) {
+
+    switch (*format) {
+    case 'b':
+      p = va_arg(ap, int);
+      *bufp++ = p;
+      break;
+
+    case 'w':
+      p = va_arg(ap, int);
+      *bufp++ = p & 0xFF;
+      *bufp++  = (p >> 8) & 0xFF;
+      break;
+
+    case 'd':
+      p = va_arg(ap, unsigned long);
+      *bufp++ = p & 0xFF;
+      *bufp++ = (p >> 8) & 0xFF;
+      *bufp++ = (p >> 16) & 0xFF;
+      *bufp++ = (p >> 24) & 0xFF;
+      break;
+
+    case ' ':
+      /* nothing to do */
+      break;
+    }
+    ++format;
+  }
+
+  if (i_io_write(ig, buffer, size) != size)
+    return 0;
+  
+  return 1;
+}
+
+/*
+=item write_palette
+
+Write the palette for an icon.
+
+=cut
+*/
+
+static int
+write_palette(i_io_glue_t *ig, ico_image_t const *image, int *error) {
+  int full_size = image->palette_size;
+  unsigned char *writebuf, *outp;
+  ico_color_t *colorp;
+  int i;
+
+  if (image->palette_size <= 2)
+    full_size = 2;
+  else if (image->palette_size <= 16)
+    full_size = 16;
+  else
+    full_size = 256;
+
+  writebuf = calloc(full_size, 4);
+  if (!writebuf) {
+    *error = ICOERR_Out_Of_Memory;
+    return 0;
+  }
+  outp = writebuf;
+  colorp = image->palette;
+  for (i = 0; i < image->palette_size; ++i) {
+    *outp++ = colorp->b;
+    *outp++ = colorp->g;
+    *outp++ = colorp->r;
+    *outp++ = 0xFF;
+    ++colorp;
+  }
+  for (; i < full_size; ++i) {
+    *outp++ = 0;
+    *outp++ = 0;
+    *outp++ = 0;
+    *outp++ = 0;
+  }
+
+  if (i_io_write(ig, writebuf, full_size * 4) != full_size * 4) {
+    *error = ICOERR_Write_Failure;
+    free(writebuf);
+    return 0;
+  }
+
+  free(writebuf);
+
+  return 1;
+}
+
+/*
+=item write_bitmapinfoheader
+
+Write the BITMAPINFOHEADER for an icon image.
+
+=cut
+*/
+
+static int
+write_bitmapinfoheader(i_io_glue_t *ig, ico_image_t const *image, int *error,
+                       int bit_count, int clr_used) {
+  if (!write_packed(ig, "d dd w w d d dd dd", 
+                   40UL, /* biSize */
+                   (unsigned long)image->width, 
+                    (unsigned long)2 * image->height, /* biWidth/biHeight */
+                   1, bit_count, /* biPlanes, biBitCount */
+                   0UL, 0UL, /* biCompression, biSizeImage */
+                   0UL, 0UL, /* bi(X|Y)PetsPerMeter */
+                   (unsigned long)clr_used, /* biClrUsed */
+                    0UL)) { /* biClrImportant */
+    *error = ICOERR_Write_Failure;
+    return 0;
+  }
+
+  return 1;
+}
+
+/*
+=item write_32_bit
+
+Write 32-bit image data to the icon.
+
+=cut
+*/
+
+static int
+write_32_bit(i_io_glue_t *ig, ico_image_t const *image, int *error) {
+  unsigned char *writebuf;
+  ico_color_t *data = image->image_data, *colorp;
+  unsigned char *writep;
+  int x, y;
+
+  if (!write_bitmapinfoheader(ig, image, error, 32, 0)) {
+    return 0;
+  }
+
+  writebuf = malloc(image->width * 4);
+  if (!writebuf) {
+    *error = ICOERR_Out_Of_Memory;
+    return 0;
+  }
+
+  for (y = image->height-1; y >= 0; --y) {
+    writep = writebuf;
+    colorp = data + y * image->width;
+    for (x = 0; x < image->width; ++x) {
+      *writep++ = colorp->b;
+      *writep++ = colorp->g;
+      *writep++ = colorp->r;
+      *writep++ = colorp->a;
+      ++colorp;
+    }
+    if (i_io_write(ig, writebuf, image->width * 4) != image->width * 4) {
+      *error = ICOERR_Write_Failure;
+      free(writebuf);
+      return 0;
+    }
+  }
+
+  free(writebuf);
+
+  return 1;
+}
+
+/*
+=item write_8_bit
+
+Write 8 bit image data.
+
+=cut
+*/
+
+static int
+write_8_bit(i_io_glue_t *ig, ico_image_t const *image, int *error) {
+  static const unsigned char zeros[3] = { '\0' };
+  int y;
+  const unsigned char *data = image->image_data;
+  int zero_count = (0U - (unsigned)image->width) & 3;
+
+  if (!write_bitmapinfoheader(ig, image, error, 8, 256)) {
+    return 0;
+  }
+
+  if (!write_palette(ig, image, error))
+    return 0;
+
+  for (y = image->height-1; y >= 0; --y) {
+    if (i_io_write(ig, data + y * image->width, 
+                  image->width) != image->width) {
+      *error = ICOERR_Write_Failure;
+      return 0;
+    }
+    if (zero_count) {
+      if (i_io_write(ig, zeros, zero_count) != zero_count) {
+       *error = ICOERR_Write_Failure;
+       return 0;
+      }
+    }
+  }
+
+  return 1;
+}
+
+/*
+=item write_4_bit
+
+Write 4 bit image data.
+
+=cut
+*/
+
+static int
+write_4_bit(i_io_glue_t *ig, ico_image_t const *image, int *error) {
+  int line_size = ((image->width + 1) / 2 + 3) / 4 * 4;
+  unsigned char *writebuf, *outp;
+  int x, y;
+  unsigned char const *data = image->image_data;
+  unsigned char const *pixelp;
+  
+  if (!write_bitmapinfoheader(ig, image, error, 4, 16)) {
+    return 0;
+  }
+
+  if (!write_palette(ig, image, error))
+    return 0;
+
+  writebuf = malloc(line_size);
+  if (!writebuf) {
+    *error = ICOERR_Out_Of_Memory;
+    return 0;
+  }
+
+  for (y = image->height-1; y >= 0; --y) {
+    pixelp = data + y * image->width;
+    outp = writebuf;
+    memset(writebuf, 0, line_size);
+    for (x = 0; x < image->width; ++x) {
+      if (x & 1) {
+       *outp |= *pixelp++ & 0x0F;
+       ++outp;
+      }
+      else {
+       *outp |= *pixelp++ << 4;
+      }
+    }
+
+    if (i_io_write(ig, writebuf, line_size) != line_size) {
+      *error = ICOERR_Write_Failure;
+      free(writebuf);
+      return 0;
+    }
+  }
+
+  free(writebuf);
+
+  return 1;
+}
+
+/*
+=item write_1_bit
+
+Write 1 bit image data.
+
+=cut
+*/
+
+static int
+write_1_bit(i_io_glue_t *ig, ico_image_t const *image, int *error) {
+  int line_size = (image->width + 31) / 32 * 4;
+  unsigned char *writebuf = malloc(line_size);
+  unsigned char *outp;
+  unsigned char const *data, *pixelp;
+  int x,y;
+  unsigned mask;
+
+  if (!write_bitmapinfoheader(ig, image, error, 1, 2)) {
+    return 0;
+  }
+
+  if (!write_palette(ig, image, error))
+    return 0;
+
+  if (!writebuf) {
+    *error = ICOERR_Out_Of_Memory;
+    return 0;
+  }
+  
+  data = image->image_data;
+  for (y = image->height-1; y >= 0; --y) {
+    memset(writebuf, 0, line_size);
+    pixelp = data + y * image->width;
+    outp = writebuf;
+    mask = 0x80;
+    for (x = 0; x < image->width; ++x) {
+      if (*pixelp)
+       *outp |= mask;
+      mask >>= 1;
+      if (!mask) {
+       mask = 0x80;
+       outp++;
+      }
+    }
+    if (i_io_write(ig, writebuf, line_size) != line_size) {
+      *error = ICOERR_Write_Failure;
+      free(writebuf);
+      return 0;
+    }
+  }
+
+  free(writebuf);
+
+  return 1;
+}
+
+/*
+=item write_mask
+
+Write the AND mask.
+
+=cut
+*/
+
+static int
+write_mask(i_io_glue_t *ig, ico_image_t const *image, int *error) {
+  int line_size = (image->width + 31) / 32 * 4;
+  unsigned char *writebuf = malloc(line_size);
+  unsigned char *outp;
+  unsigned char const *data, *pixelp;
+  int x,y;
+  unsigned mask;
+
+  if (!writebuf) {
+    *error = ICOERR_Out_Of_Memory;
+    return 0;
+  }
+  
+  data = image->mask_data;
+  if (data) {
+    for (y = image->height-1; y >= 0; --y) {
+      memset(writebuf, 0, line_size);
+      pixelp = data + y * image->width;
+      outp = writebuf;
+      mask = 0x80;
+      for (x = 0; x < image->width; ++x) {
+       if (*pixelp)
+         *outp |= mask;
+       mask >>= 1;
+       if (!mask) {
+         mask = 0x80;
+         outp++;
+       }
+        ++pixelp;
+      }
+      if (i_io_write(ig, writebuf, line_size) != line_size) {
+       *error = ICOERR_Write_Failure;
+       free(writebuf);
+       return 0;
+      }
+    }
+  }
+  else {
+    memset(writebuf, 0, line_size);
+    for (y = image->height-1; y >= 0; --y) {
+      if (i_io_write(ig, writebuf, line_size) != line_size) {
+       *error = ICOERR_Write_Failure;
+       free(writebuf);
+       return 0;
+      }
+    }
+  }
+
+  free(writebuf);
+
+  return 1;
+}
+
 /*
 =back
 
index 0b26b60..5b66704 100644 (file)
@@ -21,6 +21,7 @@ typedef struct {
   int palette_size;
   ico_color_t *palette;
   unsigned char *mask_data;
+  int hotspot_x, hotspot_y;
 } ico_image_t;
 
 extern ico_reader_t *ico_reader_open(i_io_glue_t *ig, int *error);
@@ -30,17 +31,26 @@ extern ico_image_t *ico_image_read(ico_reader_t *file, int index, int *error);
 extern void ico_image_release(ico_image_t *image);
 extern void ico_reader_close(ico_reader_t *file);
 
+extern int ico_write(i_io_glue_t *ig, ico_image_t const *images, 
+                    int image_count, int type, int *error);
+
 extern size_t ico_error_message(int error, char *buffer, size_t buffer_size);
 
 #define ICO_MAX_MESSAGE 80
 
 #define ICOERR_Short_File 100
 #define ICOERR_File_Error 101
+#define ICOERR_Write_Failure 102
 
 #define ICOERR_Invalid_File 200
 #define ICOERR_Unknown_Bits 201
 
 #define ICOERR_Bad_Image_Index 300
+#define ICOERR_Bad_File_Type 301
+#define ICOERR_Invalid_Width 302
+#define ICOERR_Invalid_Height 303
+#define ICOERR_Invalid_Palette 304
+#define ICOERR_No_Data 305
 
 #define ICOERR_Out_Of_Memory 400
 
index c89ae9a..dd37858 100644 (file)
@@ -1,12 +1,14 @@
 #!perl -w
 use strict;
-use Test::More tests => 60;
+use Test::More tests => 94;
 
 BEGIN { use_ok('Imager::File::ICO'); }
 
 -d 'testout' or mkdir 'testout';
 
 my $im = Imager->new;
+# type=>'ico' or 'cur' and read ico and cur since they're pretty much
+# the same
 ok($im->read(file => "testimg/rgba3232.ico", type=>"ico"),
    "read 32 bit")
   or print "# ", $im->errstr, "\n";
@@ -15,7 +17,6 @@ is($im->getwidth, 32, "check height");
 is($im->type, 'direct', "check type");
 is($im->tags(name => 'ico_bits'), 32, "check ico_bits tag");
 is($im->tags(name => 'i_format'), 'ico', "check i_format tag");
-is($im->tags(name => 'ico_type'), 'icon', "check ico_type tag");
 my $mask = '.*
 ..........................******
 ..........................******
@@ -72,7 +73,6 @@ is($im->type, 'paletted', "check type");
 is($im->colorcount, 256, "color count");
 is($im->tags(name => 'ico_bits'), 8, "check ico_bits tag");
 is($im->tags(name => 'i_format'), 'ico', "check i_format tag");
-is($im->tags(name => 'ico_type'), 'icon', "check ico_type tag");
 SKIP:
 {
   my $comp = Imager->new;
@@ -92,7 +92,6 @@ is($im->type, 'paletted', "check type");
 is($im->colorcount, 16, "color count");
 is($im->tags(name => 'ico_bits'), 4, "check ico_bits tag");
 is($im->tags(name => 'i_format'), 'ico', "check i_format tag");
-is($im->tags(name => 'ico_type'), 'icon', "check ico_type tag");
 SKIP:
 {
   my $comp = Imager->new;
@@ -110,9 +109,8 @@ is($im->getwidth, 32, "check width");
 is($im->getwidth, 32, "check height");
 is($im->type, 'paletted', "check type");
 is($im->colorcount, 2, "color count");
-is($im->tags(name => 'ico_bits'), 1, "check ico_bits tag");
-is($im->tags(name => 'i_format'), 'ico', "check i_format tag");
-is($im->tags(name => 'ico_type'), 'cursor', "check ico_type tag");
+is($im->tags(name => 'cur_bits'), 1, "check ico_bits tag");
+is($im->tags(name => 'i_format'), 'cur', "check i_format tag");
 $im->write(file=>'testout/pal13232.ppm');
 
 # combo was created with the GIMP, which has a decent mechanism for selecting
@@ -159,3 +157,167 @@ is_deeply([ $imgs[1]->getpixel(x=>31, 'y'=>31)->rgba ], [ 17, 231, 177, 255 ],
          "check image data 1(31,31)");
 is_deeply([ $imgs[2]->getpixel(x=>15, 'y'=>15)->rgba ], [ 17, 231, 177, 255 ],
          "check image data 2(15,15)");
+
+$im = Imager->new(xsize=>32, ysize=>32);
+$im->box(filled=>1, color=>'FF0000');
+$im->box(filled=>1, color=>'0000FF', xmin => 6, ymin=>0, xmax => 21, ymax=>15);
+$im->box(filled=>1, color=>'00FF00', xmin => 10, ymin=>16, xmax => 25, ymax=>31);
+
+ok($im->write(file=>'testout/t10_32.ico', type=>'ico'),
+   "write 32-bit icon");
+
+my $im2 = Imager->new;
+ok($im2->read(file=>'testout/t10_32.ico', type=>'ico'),
+   "read it back in");
+
+is(Imager::i_img_diff($im->{IMG}, $im2->{IMG}), 0,
+   "check they're the same");
+is($im->bits, $im2->bits, "check same bits");
+
+{
+  my $im = Imager->new(xsize => 32, ysize => 32);
+  $im->box(filled=>1, color=>'#FF00FF');
+  my $data;
+  ok(Imager->write_multi({ data => \$data, type=>'ico' }, $im, $im),
+     "write multi icons");
+  ok(length $data, "and it wrote data");
+  my @im = Imager->read_multi(data => $data);
+  is(@im, 2, "got all the images back");
+  is(Imager::i_img_diff($im->{IMG}, $im[0]{IMG}), 0, "check first image");
+  is(Imager::i_img_diff($im->{IMG}, $im[1]{IMG}), 0, "check second image");
+}
+
+{ # 1 channel image
+  my $im = Imager->new(xsize => 32, ysize => 32, channels => 1);
+  $im->box(filled=>1, color => [ 128, 0, 0 ]);
+  my $data;
+  ok($im->write(data => \$data, type=>'ico'), "write 1 channel image");
+  my $im2 = Imager->new;
+  ok($im2->read(data => $data), "read it back");
+  is($im2->getchannels, 4, "check channels");
+  my $imrgb = $im->convert(preset => 'rgb')
+    ->convert(preset => 'addalpha');
+  is(Imager::i_img_diff($imrgb->{IMG}, $im2->{IMG}), 0,
+     "check image matches expected");
+}
+
+{ # 2 channel image
+  my $base = Imager->new(xsize => 32, ysize => 32, channels => 2);
+  $base->box(filled => 1, color => [ 64, 192, 0 ]);
+  my $data;
+  ok($base->write(data => \$data, type=>'ico'), "write 2 channel image");
+  my $read = Imager->new;
+  ok($read->read(data => $data), "read it back");
+  is($read->getchannels, 4, "check channels");
+  my $imrgb = $base->convert(preset => 'rgb');
+  is(Imager::i_img_diff($imrgb->{IMG}, $read->{IMG}), 0,
+     "check image matches expected");
+}
+
+{ # 4 channel image
+  my $base = Imager->new(xsize => 32, ysize => 32, channels => 4);
+  $base->box(filled=>1, ymax => 15, color => [ 255, 0, 255, 128 ]);
+  $base->box(filled=>1, ymin => 16, color => [ 0, 255, 255, 255 ]);
+  my $data;
+  ok($base->write(data => \$data, type=>'ico'), "write 4 channel image");
+  my $read = Imager->new;
+  ok($read->read(data => $data, type=>'ico'), "read it back")
+    or print "# ", $read->errstr, "\n";
+  is(Imager::i_img_diff($base->{IMG}, $read->{IMG}), 0,
+     "check image matches expected");
+}
+
+{ # mask handling
+  my $base = Imager->new(xsize => 16, ysize => 16, channels => 3);
+  $base->box(filled=>1, xmin => 5, xmax => 10, color => '#0000FF');
+  $base->box(filled=>1, ymin => 5, ymax => 10, color => '#0000FF');
+  my $mask = <<EOS; # CR in this to test it's skipped correctly
+01
+0000011111100000
+00000111111 00000xx
+00000111111000  
+00000111111000
+0000011111100000
+1111111111111111
+1111111111111111
+1111111111111111
+1111111111111111
+1111111111111111
+1111111111111111
+1010101010101010
+1010101010101010
+1010101010101010
+1010101010101010
+1010101010101010
+EOS
+  $mask =~ s/\n/\r\n/g; # to test alternate newline handling is correct
+  $base->settag(name => 'ico_mask', value => $mask);
+  my $saved_mask = $base->tags(name => 'ico_mask');
+  my $data;
+  ok($base->write(data => \$data, type => 'ico'),
+     "write with mask tag set");
+  my $read = Imager->new;
+  ok($read->read(data => $data), "read it back");
+  my $mask2 = $mask;
+  $mask2 =~ tr/01/.*/;
+  $mask2 =~ s/\n$//;
+  $mask2 =~ tr/\r x//d;
+  $mask2 =~ s/^(.{3,19})$/$1 . "." x (16 - length $1)/gem;
+  my $read_mask = $read->tags(name => 'ico_mask');
+  is($read_mask, $mask2, "check mask is correct");
+}
+
+{ # mask too short to handle
+  my $mask = "xx";
+  my $base = Imager->new(xsize => 16, ysize => 16, channels => 3);
+  $base->box(filled=>1, xmin => 5, xmax => 10, color => '#0000FF');
+  $base->box(filled=>1, ymin => 5, ymax => 10, color => '#0000FF');
+  $base->settag(name => 'ico_mask', value => $mask);
+  my $data;
+  ok($base->write(data => \$data, type=>'ico'),
+     "save icon with short mask tag");
+  my $read = Imager->new;
+  ok($read->read(data => $data), "read it back");
+  my $read_mask = $read->tags(name => 'ico_mask');
+  my $expected_mask = ".*" . ( "\n" . "." x 16 ) x 16;
+  is($read_mask, $expected_mask, "check the mask");
+
+  # mask that doesn't match what we expect
+  $base->settag(name => 'ico_mask', value => 'abcd');
+  ok($base->write(data => \$data, type => 'ico'), 
+     "write with bad format mask tag");
+  ok($read->read(data => $data), "read it back");
+  $read_mask = $read->tags(name => 'ico_mask');
+  is($read_mask, $expected_mask, "check the mask");
+
+  # mask with invalid char
+  $base->settag(name => 'ico_mask', value => ".*\n....xxx..");
+  ok($base->write(data => \$data, type => 'ico'), 
+     "write with unexpected chars in mask");
+  ok($read->read(data => $data), "read it back");
+  $read_mask = $read->tags(name => 'ico_mask');
+  is($read_mask, $expected_mask, "check the mask");
+}
+
+{ # check handling of greyscale paletted
+  my $base = Imager->new(xsize => 16, ysize => 16, channels => 1, 
+                         type => 'paletted');
+  my @grays = map Imager::Color->new($_),
+    "000000", "666666", "CCCCCC", "FFFFFF";
+  ok($base->addcolors(colors => \@grays), "add some colors");
+  $base->box(filled => 1, color => $grays[1], xmax => 7, ymax => 7);
+  $base->box(filled => 1, color => $grays[1], xmax => 7, ymin => 8);
+  $base->box(filled => 1, color => $grays[1], xmin => 8, ymax => 7);
+  $base->box(filled => 1, color => $grays[1], xmin => 8, ymax => 8);
+  my $data;
+  ok($base->write(data => \$data, type => 'ico'),
+     "write grayscale paletted");
+  my $read = Imager->new;
+  ok($read->read(data => $data), "read it back")
+    or print "# ", $read->errstr, "\n";
+  is($read->type, 'paletted', "check type");
+  is($read->getchannels, 3, "check channels");
+  my $as_rgb = $base->convert(preset => 'rgb');
+  is(Imager::i_img_diff($base->{IMG}, $read->{IMG}), 0,
+     "check the image");
+}
diff --git a/ICO/t/t30cursor.t b/ICO/t/t30cursor.t
new file mode 100644 (file)
index 0000000..d5bde95
--- /dev/null
@@ -0,0 +1,73 @@
+#!perl -w
+use strict;
+use Test::More tests => 25;
+
+BEGIN { use_ok('Imager::File::CUR'); }
+
+-d 'testout' or mkdir 'testout';
+
+my $im = Imager->new;
+
+ok($im->read(file => 'testimg/pal43232.cur', type=>'cur'),
+   "read 4 bit");
+is($im->getwidth, 32, "check width");
+is($im->getheight, 32, "check width");
+is($im->type, 'paletted', "check type");
+is($im->tags(name => 'cur_bits'), 4, "check cur_bits tag");
+is($im->tags(name => 'i_format'), 'cur', "check i_format tag");
+is($im->tags(name => 'cur_hotspotx'), 1, "check cur_hotspotx tag");
+is($im->tags(name => 'cur_hotspoty'), 18, "check cur_hotspoty tag");
+my $mask = ".*" . ("\n" . "." x 32) x 32;
+is($im->tags(name => 'cur_mask'), $mask, "check cur_mask tag");
+
+# these should get pushed back into range on saving
+$im->settag(name => 'cur_hotspotx', value => 32);
+$im->settag(name => 'cur_hotspoty', value => -1);
+ok($im->write(file=>'testout/hotspot.cur', type=>'cur'),
+   "save with oor hotspot")
+  or print "# ",$im->errstr, "\n";
+{
+  my $im2 = Imager->new;
+  ok($im2->read(file=>'testout/hotspot.cur', type=>'cur'),
+     "re-read the hotspot set cursor")
+    or print "# ", $im->errstr, "\n";
+  is($im2->tags(name => 'cur_hotspotx'), 31, "check cur_hotspotx tag");
+  is($im2->tags(name => 'cur_hotspoty'), 0, "check cur_hotspoty tag");
+}
+
+$im->settag(name => 'cur_hotspotx', value => -1);
+$im->settag(name => 'cur_hotspoty', value => 32);
+ok($im->write(file=>'testout/hotspot2.cur', type=>'cur'),
+   "save with oor hotspot")
+  or print "# ",$im->errstr, "\n";
+
+{
+  my $im2 = Imager->new;
+  ok($im2->read(file=>'testout/hotspot2.cur', type=>'cur'),
+     "re-read the hotspot set cursor")
+    or print "# ", $im->errstr, "\n";
+  is($im2->tags(name => 'cur_hotspotx'), 0, "check cur_hotspotx tag");
+  is($im2->tags(name => 'cur_hotspoty'), 31, "check cur_hotspoty tag");
+}
+
+{
+  my $data = '';
+  ok($im->write(data => \$data, type => 'cur'),
+     "write single to data");
+  print "# ", length $data, " bytes written\n";
+  my $im2 = Imager->new;
+  ok($im2->read(data => $data), "read back in");
+  is(Imager::i_img_diff($im->{IMG}, $im2->{IMG}), 0, "check image");
+}
+
+{
+  my $data = '';
+  ok(Imager->write_multi({ type => 'cur', data => \$data }, $im, $im),
+     "write multiple images");
+  print "# ", length $data, " bytes written\n";
+  my @im = Imager->read_multi(type => 'cur', data => $data)
+    or print "# ", Imager->errstr, "\n";
+  is(@im, 2, "read them back in");
+  is(Imager::i_img_diff($im->{IMG}, $im[0]{IMG}), 0, "check first image");
+  is(Imager::i_img_diff($im->{IMG}, $im[1]{IMG}), 0, "check second image");
+}
diff --git a/ICO/t/t40readcurone.t b/ICO/t/t40readcurone.t
new file mode 100644 (file)
index 0000000..65232e4
--- /dev/null
@@ -0,0 +1,10 @@
+#!perl -w
+use strict;
+use Test::More tests => 1;
+use Imager;
+
+# checks that we load the CUR handler automatically
+my $im = Imager->new;
+ok($im->read(file => 'testimg/pal43232.cur'),
+   "check that cursor reader loaded correctly for singles")
+  or print "# ", $im->errstr, "\n";
diff --git a/ICO/t/t41curmultread.t b/ICO/t/t41curmultread.t
new file mode 100644 (file)
index 0000000..a5fdab6
--- /dev/null
@@ -0,0 +1,10 @@
+#!perl -w
+use strict;
+use Test::More tests => 1;
+use Imager;
+
+# checks that we load the CUR handler automatically for multiple image reads
+my @im = Imager->read_multi(file=>'testimg/pal43232.cur');
+is(scalar(@im), 1,
+   "check that cursor reader loaded correctly for singles")
+  or print "# ", Imager->errstr, "\n";
diff --git a/ICO/t/t50readfail.t b/ICO/t/t50readfail.t
new file mode 100644 (file)
index 0000000..5d3d6e7
--- /dev/null
@@ -0,0 +1,337 @@
+#!perl -w
+use strict;
+use Imager;
+use Test::More tests => 40;
+
+sub get_data;
+
+{ # test file limits are obeyed (paletted)
+  Imager->set_file_limits(reset => 1, width => 10);
+  my $im = Imager->new;
+  ok(!$im->read(file => 'testimg/pal13232.ico'), "can't read overwide image");
+  like($im->errstr, qr/image width/, "check message");
+}
+
+{ # test file limits are obeyed (direct)
+  Imager->set_file_limits(reset => 1, width => 10);
+  my $im = Imager->new;
+  ok(!$im->read(file => 'testimg/rgba3232.ico'), "can't read overwide image");
+  like($im->errstr, qr/image width/, "check message");
+}
+
+Imager->set_file_limits(reset => 1);
+
+{ # file too short for magic
+  my $im = Imager->new;
+  ok(!$im->read(data=>"XXXX", type=>'ico'), "Can't read short image file");
+  is($im->errstr, "error opening ICO/CUR file: Short read", 
+     "check error message");
+}
+
+{ # read non-icon
+  my $im = Imager->new;
+  ok(!$im->read(file=>'t/t50readfail.t', type=>'ico'),
+     "script isn't an icon");
+  is($im->errstr, "error opening ICO/CUR file: Not an icon file", 
+     "check message");
+}
+
+{ # file with not enough icon structures
+  my $im = Imager->new;
+  my $data = pack "H*", "00000100010000";
+  ok(!$im->read(data => $data, type=>'ico'), 
+     "ico file broken at resource entries");
+  is($im->errstr, "error opening ICO/CUR file: Short read",
+     "check error message");
+}
+{
+  my $im = Imager->new;
+  my $data = pack "H*", "00000200010000";
+  ok(!$im->read(data => $data, type=>'cur'), 
+     "cursor file broken at resource entries");
+  is($im->errstr, "error opening ICO/CUR file: Short read",
+     "check error message");
+}
+
+{ # read negative index image
+  my $im = Imager->new;
+  ok(!$im->read(file=>'testimg/pal13232.ico', type=>'ico', page=>-1),
+     "read page -1");
+  is($im->errstr, "error reading ICO/CUR image: Image index out of range", 
+     "check error message");
+}
+
+{ # read too high image index
+  my $im = Imager->new;
+  ok(!$im->read(file=>'testimg/pal13232.ico', type=>'ico', page=>1),
+     "read page 1");
+  is($im->errstr, "error reading ICO/CUR image: Image index out of range",
+     "check error message");
+}
+
+{ # image offset beyond end of file
+  my $im = Imager->new;
+  my $data = get_data <<EOS;
+; header - icon with 1 image
+0000 0100 0100
+; image record 32 x 32, offset 0xFFFF
+20 20 00 00 0000 0000 00200000 FFFF0000
+EOS
+  ok(!$im->read(data => $data, type=>'ico'), 
+     "read from icon with bad offset");
+  # bad offset causes the seek to fail on an in-memory "file"
+  # it may not fail this way on a real file.
+  is($im->errstr, "error reading ICO/CUR image: I/O error", 
+     "check error message");
+}
+
+{ # short read on bmiheader
+  my $im = Imager->new;
+  my $data = get_data <<EOS;
+; header - icon with 1 image
+0000 0100 0100
+; image record 32 x 32, offset 0xFFFF
+20 20 00 00 0000 0000 00200000 16000000
+; bmiheader for the first image
+2800 0000 2000 0000 4000 0000 ; size, width, height
+; short here
+EOS
+  ok(!$im->read(data => $data, type=>'ico'), 
+     "read from icon with a short bitmap header");
+  is($im->errstr, "error reading ICO/CUR image: Short read",
+     "check error message");
+}
+
+{ # invalid bmiheader
+  my $im = Imager->new;
+  my $data = get_data <<EOS;
+; header - icon with 1 image
+0000 0100 0100
+; image record 32 x 32, offset 0xFFFF
+20 20 00 00 0000 0000 00200000 16000000
+; bmiheader for the first image
+2000 0000 2000 0000 4000 0000 ; size should be 0x28, width, height
+0100 2000 ; planes, bit count
+; data we read but ignore
+0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
+EOS
+  ok(!$im->read(data => $data, type=>'ico'), 
+     "read from icon with an invalid sub-image header");
+  is($im->errstr, "error reading ICO/CUR image: Not an icon file",
+     "check error message");
+}
+
+{ # invalid bit count for "direct" image
+  my $im = Imager->new;
+  my $data = get_data <<EOS;
+; header - icon with 1 image
+0000 0100 0100
+; image record 32 x 32, offset 0xFFFF
+20 20 00 00 0000 0000 00200000 16000000
+; bmiheader for the first image
+2800 0000 2000 0000 4000 0000 ; size, width, height
+0100 2100 ; planes, bit count
+; data we read but ignore
+0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
+EOS
+  ok(!$im->read(data => $data, type=>'ico'), 
+     "read from icon with an invalid 'direct' bits per pixel");
+  is($im->errstr, "error reading ICO/CUR image: Unknown value for bits/pixel", 
+     "check error message");
+}
+
+{ # short file reading palette
+  my $im = Imager->new;
+  my $data = get_data <<EOS;
+; header - icon with 1 image
+0000 0100 0100
+; image record 32 x 32, offset 0xFFFF
+20 20 00 00 0000 0000 00200000 16000000
+; bmiheader for the first image
+2800 0000 2000 0000 4000 0000 ; size, width, height
+0100 0100 ; planes, bit count == 1
+; data we read but ignore
+0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
+; dummy palette - one color but 2 needed
+FFFFFF00
+EOS
+  ok(!$im->read(data => $data, type=>'ico'), 
+     "read from icon with short palette");
+  is($im->errstr, "error reading ICO/CUR image: Short read",
+     "check error message");
+}
+
+{ # short file reading 1 bit image data
+  my $im = Imager->new;
+  my $data = get_data <<EOS;
+; header - icon with 1 image
+0000 0100 0100
+; image record 32 x 32, offset 0x20
+20 20 00 00 0000 0000 00200000 16000000
+; bmiheader for the first image
+2800 0000 2000 0000 4000 0000 ; size, width, height
+0100 0100 ; planes, bit count == 1
+; data we read but ignore
+0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
+; palette
+00000000
+FFFFFF00
+; image data - short
+00 ff
+EOS
+  ok(!$im->read(data => $data, type=>'ico'), 
+     "read from icon with short image data (1 bit)");
+  is($im->errstr, "error reading ICO/CUR image: Short read",
+     "check error message");
+}
+
+{ # short file reading 32 bit image data
+  my $im = Imager->new;
+  my $data = get_data <<EOS;
+; header - icon with 1 image
+0000 0100 0100
+; image record 32 x 32, offset 0x20
+20 20 00 00 0000 0000 00200000 16000000
+; bmiheader for the first image
+2800 0000 2000 0000 4000 0000 ; size, width, height
+0100 2000 ; planes, bit count == 32
+; data we read but ignore
+0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
+; nopalette
+; image data - short
+FFFFFFFF 
+EOS
+  ok(!$im->read(data => $data, type=>'ico'), 
+     "read from icon with short image data (32 bit)");
+  is($im->errstr, "error reading ICO/CUR image: Short read",
+     "check error message");
+}
+
+{ # short file reading 4 bit image data
+  my $im = Imager->new;
+  my $data = get_data <<EOS;
+; header - icon with 1 image
+0000 0100 0100
+; image record 32 x 32, offset 0x20
+20 20 00 00 0000 0000 00200000 16000000
+; bmiheader for the first image
+2800 0000 2000 0000 4000 0000 ; size, width, height
+0100 0400 ; planes, bit count == 4
+; data we read but ignore
+0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
+; 16-color palette
+00000000 FFFFFF00 00000000 FFFFFF00
+00000000 FFFFFF00 00000000 FFFFFF00
+00000000 FFFFFF00 00000000 FFFFFF00
+00000000 FFFFFF00 00000000 FFFFFF00
+; image data - short
+FFFFFFFF 
+EOS
+  ok(!$im->read(data => $data, type=>'ico'), 
+     "read from icon with short image data (4 bit)");
+  is($im->errstr, "error reading ICO/CUR image: Short read",
+     "check error message");
+}
+
+{ # short file reading 8 bit image data
+  my $im = Imager->new;
+  # base image header + palette + a little data
+  my $data = get_data <<EOS . "FFFFFFFF" x 256 . "FFFF FFFF";
+; header - icon with 1 image
+0000 0100 0100
+; image record 32 x 32, offset 0x20
+20 20 00 00 0000 0000 00200000 16000000
+; bmiheader for the first image
+2800 0000 2000 0000 4000 0000 ; size, width, height
+0100 0800 ; planes, bit count == 8
+; data we read but ignore
+0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
+EOS;
+; palette and data above
+EOS
+  ok(!$im->read(data => $data, type=>'ico'), 
+     "read from icon with short image data (8 bit)");
+  is($im->errstr, "error reading ICO/CUR image: Short read",
+     "check error message");
+}
+
+{ # short file reading mask data
+  my $im = Imager->new;
+  my $data = get_data <<EOS;
+; header - icon with 1 image
+0000 0100 0100
+; image record 16 x 16, 2 colors, reserved=0, planes=1,
+; sizeinbytes (ignored), offset 0x16
+10 10 02 00 0100 0100 00000000 16000000
+; bmiheader for the first image
+2800 0000 1000 0000 2000 0000 ; size, width, height
+0100 0100 ; planes, bit count == 1
+; data we read but ignore
+0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
+; palette
+00000000
+FFFFFF00
+; image data - 16 x 16 bits
+; note that each line needs to be aligned on a 32-bit boundary
+00ff00ff 00000000
+00ff00ff 00000000
+00ff00ff 00000000
+00ff00ff 00000000
+00ff00ff 00000000
+00ff00ff 00000000
+00ff00ff 00000000
+00ff00ff 00000000
+00ff00ff 00000000
+00ff00ff 00000000
+00ff00ff 00000000
+00ff00ff 00000000
+00ff00ff 00000000
+00ff00ff 00000000
+00ff00ff 00000000
+00ff00ff 00000000
+; mask, short
+0ff0
+EOS
+  ok(!$im->read(data => $data, type=>'ico'), 
+     "read from icon with short mask data");
+  is($im->errstr, "error reading ICO/CUR image: Short read",
+     "check error message");
+}
+
+{ # fail opening on a multi-read
+  ok(!Imager->read_multi(file=>'t/t50readfail.t', type=>'ico'),
+     "multi-read on non-icon");
+  is(Imager->errstr, "error opening ICO/CUR file: Not an icon file", 
+     "check message");
+}
+
+{ # invalid bit count for "direct" image (read_multi)
+  my $data = get_data <<EOS;
+; header - icon with 1 image
+0000 0100 0100
+; image record 32 x 32, offset 0xFFFF
+20 20 00 00 0000 0000 00200000 16000000
+; bmiheader for the first image
+2800 0000 2000 0000 4000 0000 ; size, width, height
+0100 2100 ; planes, bit count
+; data we read but ignore
+0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
+EOS
+  ok(!Imager->read_multi(data => $data, type=>'ico'), 
+     "read from icon with an invalid 'direct' bits per pixel (multi)");
+  is(Imager->errstr, 
+     "error reading ICO/CUR image: Unknown value for bits/pixel", 
+     "check error message");
+}
+
+
+# extract hex data from text
+# allows comments
+sub get_data {
+  my ($src) = @_;
+
+  $src =~ s/[\#;].*//mg;
+  $src =~ tr/0-9A-F//cd;
+
+  pack("H*", $src);
+}
diff --git a/ICO/t/t60writefail.t b/ICO/t/t60writefail.t
new file mode 100644 (file)
index 0000000..2b4edbe
--- /dev/null
@@ -0,0 +1,253 @@
+#!perl -w
+use strict;
+use Test::More tests => 69;
+use Imager ':handy';
+
+# this file tries to cover as much of the write error handling cases in 
+# msicon.c/imicon.c as possible.
+#
+# coverage checked with gcc/gcov
+
+# image too big for format tests, for each entry point
+{
+  my $im = Imager->new(xsize => 256, ysize => 255);
+  my $data;
+  ok(!$im->write(data => \$data, type=>'ico'),
+     "image too large");
+  is($im->errstr, "image too large for ico file", "check message");
+}
+
+{
+  my $im = Imager->new(xsize => 256, ysize => 255);
+  my $data;
+  ok(!Imager->write_multi({ data => \$data, type=>'ico' }, $im, $im),
+     "image too large");
+  is(Imager->errstr, "image too large for ico file", "check message");
+  Imager->_set_error('');
+}
+
+{
+  my $im = Imager->new(xsize => 256, ysize => 255);
+  my $data;
+  ok(!$im->write(data => \$data, type=>'cur'),
+     "image too large");
+  is($im->errstr, "image too large for ico file", "check message");
+}
+
+{
+  my $im = Imager->new(xsize => 256, ysize => 255);
+  my $data;
+  ok(!Imager->write_multi({ data => \$data, type=>'cur' }, $im),
+     "image too large");
+  is(Imager->errstr, "image too large for ico file", "check message");
+  Imager->_set_error('');
+}
+
+# low level write failure tests for each entry point (fail on close)
+{
+  my $im = Imager->new(xsize => 10, ysize => 10);
+  ok(!$im->write(callback => \&write_failure, type=>'ico'),
+     "low level write failure (ico)");
+  is($im->errstr, "error closing output: synthetic error", "check message");
+}
+
+{
+  my $im = Imager->new(xsize => 10, ysize => 10);
+  ok(!$im->write(callback => \&write_failure, type=>'cur'),
+     "low level write failure (cur)");
+  is($im->errstr, "error closing output: synthetic error", "check message");
+}
+
+{
+  my $im = Imager->new(xsize => 10, ysize => 10);
+  ok(!Imager->write_multi({ callback => \&write_failure, type=>'ico' }, $im, $im),
+     "low level write_multi failure (ico)");
+  is(Imager->errstr, "error closing output: synthetic error", "check message");
+  Imager->_set_error('');
+}
+
+{
+  my $im = Imager->new(xsize => 10, ysize => 10);
+  ok(!Imager->write_multi({ callback => \&write_failure, type=>'cur' }, $im, $im),
+     "low level write_multi failure (cur)");
+  is(Imager->errstr, "error closing output: synthetic error", "check message");
+  Imager->_set_error('');
+}
+
+# low level write failure tests for each entry point (fail on write)
+{
+  my $im = Imager->new(xsize => 10, ysize => 10);
+  ok(!$im->write(callback => \&write_failure, maxbuffer => 1, type=>'ico'),
+     "low level write failure (ico)");
+  is($im->errstr, "Write failure: synthetic error", "check message");
+}
+
+{
+  my $im = Imager->new(xsize => 10, ysize => 10);
+  ok(!$im->write(callback => \&write_failure, maxbuffer => 1, type=>'cur'),
+     "low level write failure (cur)");
+  is($im->errstr, "Write failure: synthetic error", "check message");
+}
+
+{
+  my $im = Imager->new(xsize => 10, ysize => 10);
+  ok(!Imager->write_multi({ callback => \&write_failure, maxbuffer => 1, type=>'ico' }, $im, $im),
+     "low level write_multi failure (ico)");
+  is(Imager->errstr, "Write failure: synthetic error", "check message");
+  Imager->_set_error('');
+}
+
+{
+  my $im = Imager->new(xsize => 10, ysize => 10);
+  ok(!Imager->write_multi({ callback => \&write_failure, maxbuffer => 1, type=>'cur' }, $im, $im),
+     "low level write_multi failure (cur)");
+  is(Imager->errstr, "Write failure: synthetic error", "check message");
+  Imager->_set_error('');
+}
+
+{
+  my $im = Imager->new(xsize => 10, ysize => 10);
+  ok(!$im->write(type => 'ico', callback => WriteLimit->new(6), maxbuffer => 1),
+     "second write (resource) should fail (ico)");
+  is($im->errstr, "Write failure: limit reached", "check message");
+  $im->_set_error('');
+
+  ok(!$im->write(type => 'cur', callback => WriteLimit->new(6), maxbuffer => 1),
+     "second (resource) write should fail (cur)");
+  is($im->errstr, "Write failure: limit reached", "check message");
+  $im->_set_error('');
+
+  ok(!$im->write(type => 'ico', callback => WriteLimit->new(22), maxbuffer => 1),
+     "third write (bmi) should fail (32-bit)");
+  is($im->errstr, "Write failure: limit reached", "check message");
+  $im->_set_error('');
+
+  ok(!$im->write(type => 'ico', callback => WriteLimit->new(62), maxbuffer => 1),
+     "fourth write (data) should fail (32-bit)");
+  is($im->errstr, "Write failure: limit reached", "check message");
+  $im->_set_error('');
+
+  ok(!$im->write(type => 'ico', callback => WriteLimit->new(462), maxbuffer => 1),
+     "mask write should fail (32-bit)");
+  is($im->errstr, "Write failure: limit reached", "check message");
+}
+
+{ # 1 bit write fails
+  my $im = Imager->new(xsize => 10, ysize => 10, type => 'paletted');
+  my $red = NC(255, 0, 0);
+  my $blue = NC(0, 0, 255);
+  $im->addcolors(colors => [ $red, $blue ]);
+  $im->box(filled => 1, color => $red, ymax => 5);
+  $im->box(filled => 1, color => $blue, ymin => 6);
+  ok(!$im->write(type => 'ico', callback => WriteLimit->new(22), maxbuffer => 1),
+     "third write (bmi) should fail (1-bit)");
+  is($im->errstr, "Write failure: limit reached", "check message");
+  
+  ok(!$im->write(type => 'ico', callback => WriteLimit->new(66), maxbuffer => 1),
+     "fourth write (palette) should fail (1-bit)");
+  is($im->errstr, "Write failure: limit reached", "check message");
+  ok(!$im->write(type => 'ico', callback => WriteLimit->new(74), maxbuffer => 1),
+     "fifth write (image) should fail (1-bit)");
+  is($im->errstr, "Write failure: limit reached", "check message");
+  my $data;
+  ok($im->write(data => \$data, type => 'ico'), "write 1 bit successfully");
+  my $read = Imager->new;
+  ok($read->read(data => $data), "read it back");
+  is($read->type, 'paletted', "check type");
+  is($read->tags(name => 'ico_bits'), 1, "check bits");
+  is(Imager::i_img_diff($read, $im), 0, "check image correct");
+}
+
+{ # 4 bit write fails
+  my $im = Imager->new(xsize => 10, ysize => 10, type => 'paletted');
+  my $red = NC(255, 0, 0);
+  my $blue = NC(0, 0, 255);
+  $im->addcolors(colors => [ ($red, $blue) x 8 ]);
+  $im->box(filled => 1, color => $red, ymax => 5);
+  $im->box(filled => 1, color => $blue, ymin => 6);
+  ok(!$im->write(type => 'ico', callback => WriteLimit->new(22), maxbuffer => 1),
+     "third write (bmi) should fail (4-bit)");
+  is($im->errstr, "Write failure: limit reached", "check message");
+  
+  ok(!$im->write(type => 'ico', callback => WriteLimit->new(66), maxbuffer => 1),
+     "fourth write (palette) should fail (4-bit)");
+  is($im->errstr, "Write failure: limit reached", "check message");
+  ok(!$im->write(type => 'ico', callback => WriteLimit->new(130), maxbuffer => 1),
+     "fifth write (image) should fail (4-bit)");
+  is($im->errstr, "Write failure: limit reached", "check message");
+  my $data;
+  ok($im->write(data => \$data, type => 'ico'), "write 4 bit successfully");
+  my $read = Imager->new;
+  ok($read->read(data => $data), "read it back");
+  is($read->type, 'paletted', "check type");
+  is($read->tags(name => 'ico_bits'), 4, "check bits");
+  is(Imager::i_img_diff($read, $im), 0, "check image correct");
+}
+
+{ # 8 bit write fails
+  my $im = Imager->new(xsize => 10, ysize => 10, type => 'paletted');
+  my $red = NC(255, 0, 0);
+  my $blue = NC(0, 0, 255);
+  $im->addcolors(colors => [ ($red, $blue) x 9 ]);
+  $im->box(filled => 1, color => $red, ymax => 5);
+  $im->box(filled => 1, color => $blue, ymin => 6);
+  ok(!$im->write(type => 'ico', callback => WriteLimit->new(22), maxbuffer => 1),
+     "third write (bmi) should fail (8-bit)");
+  is($im->errstr, "Write failure: limit reached", "check message");
+  
+  ok(!$im->write(type => 'ico', callback => WriteLimit->new(62), maxbuffer => 1),
+     "fourth write (palette) should fail (8-bit)");
+  is($im->errstr, "Write failure: limit reached", "check message");
+  ok(!$im->write(type => 'ico', callback => WriteLimit->new(62 + 1024), maxbuffer => 1),
+     "fifth write (image) should fail (8-bit)");
+  is($im->errstr, "Write failure: limit reached", "check message");
+  ok(!$im->write(type => 'ico', callback => WriteLimit->new(62 + 1024 + 10), maxbuffer => 1),
+     "sixth write (zeroes) should fail (8-bit)");
+  is($im->errstr, "Write failure: limit reached", "check message");
+  my $data;
+  ok($im->write(data => \$data, type => 'ico'), "write 8 bit successfully");
+  my $read = Imager->new;
+  ok($read->read(data => $data), "read it back");
+  is($read->type, 'paletted', "check type");
+  is($read->tags(name => 'ico_bits'), 8, "check bits");
+  is(Imager::i_img_diff($read, $im), 0, "check image correct");
+}
+
+# write callback that fails
+sub write_failure {
+  print "# synthesized write failure\n";
+  Imager::i_push_error(0, "synthetic error");
+  return;
+}
+
+package WriteLimit;
+use overload 
+  '&{}' => \&limited_write,
+  'bool' => sub { 1 };
+
+sub new {
+  my ($class, $limit) = @_;
+
+  bless 
+    { 
+     do_write =>
+     sub {
+       my ($data) = @_;
+       $limit -= length $data;
+       if ($limit >= 0) {
+         print "# write of ", length $data, " bytes successful ($limit left)\n";
+         return 1;
+       }
+       else {
+         print "# write of ", length $data, " bytes failed\n";
+         Imager::i_push_error(0, "limit reached");
+         return;
+       }
+     }
+    },$class;
+}
+
+sub limited_write {
+  my ($self) = @_;
+  return $self->{do_write};
+}
diff --git a/ICO/t/t70icosing.t b/ICO/t/t70icosing.t
new file mode 100644 (file)
index 0000000..6a42ec8
--- /dev/null
@@ -0,0 +1,11 @@
+#!perl -w
+use strict;
+use Test::More tests => 1;
+use Imager;
+require '../t/testtools.pl';
+
+# checks that we load the ICO write handler automatically
+my $img = test_oo_img();
+ok($img->write(file => 'testout/icosing.ico'),
+   "write ico with autoload")
+  or print "# ",$img->errstr,"\n";
diff --git a/ICO/t/t71icomult.t b/ICO/t/t71icomult.t
new file mode 100644 (file)
index 0000000..fbb2f9e
--- /dev/null
@@ -0,0 +1,11 @@
+#!perl -w
+use strict;
+use Test::More tests => 1;
+use Imager;
+require '../t/testtools.pl';
+
+# checks that we load the ICO write handler automatically
+my $img = test_oo_img();
+ok(Imager->write_multi({ file => 'testout/icomult.ico' }, $img, $img),
+   "write_multi ico with autoload")
+  or print "# ",Imager->errstr,"\n";
diff --git a/ICO/t/t72cursing.t b/ICO/t/t72cursing.t
new file mode 100644 (file)
index 0000000..d8632ab
--- /dev/null
@@ -0,0 +1,11 @@
+#!perl -w
+use strict;
+use Test::More tests => 1;
+use Imager;
+require '../t/testtools.pl';
+
+# checks that we load the CUR write handler automatically
+my $img = test_oo_img();
+ok($img->write(file => 'testout/cursing.cur'),
+   "write cur with autoload")
+  or print "# ",$img->errstr,"\n";
diff --git a/ICO/t/t73curmult.t b/ICO/t/t73curmult.t
new file mode 100644 (file)
index 0000000..b2ba49e
--- /dev/null
@@ -0,0 +1,11 @@
+#!perl -w
+use strict;
+use Test::More tests => 1;
+use Imager;
+require '../t/testtools.pl';
+
+# checks that we load the CUR write handler automatically
+my $img = test_oo_img();
+ok(Imager->write_multi({ file => 'testout/icomult.cur' }, $img, $img),
+   "write_multi cur with autoload")
+  or print "# ",Imager->errstr,"\n";
diff --git a/ICO/testimg/pal43232.cur b/ICO/testimg/pal43232.cur
new file mode 100755 (executable)
index 0000000..4326b17
Binary files /dev/null and b/ICO/testimg/pal43232.cur differ
index 178b302..5c77569 100644 (file)
--- a/Imager.pm
+++ b/Imager.pm
@@ -146,6 +146,9 @@ use Imager::Font;
 # registered file readers
 my %readers;
 
+# registered file writers
+my %writers;
+
 # modules we attempted to autoload
 my %attempted_to_load;
 
@@ -1365,6 +1368,28 @@ sub register_reader {
   return 1;
 }
 
+sub register_writer {
+  my ($class, %opts) = @_;
+
+  defined $opts{type}
+    or die "register_writer called with no type parameter\n";
+
+  my $type = $opts{type};
+
+  defined $opts{single} || defined $opts{multiple}
+    or die "register_writer called with no single or multiple parameter\n";
+
+  $writers{$type} = {  };
+  if ($opts{single}) {
+    $writers{$type}{single} = $opts{single};
+  }
+  if ($opts{multiple}) {
+    $writers{$type}{multiple} = $opts{multiple};
+  }
+
+  return 1;
+}
+
 # probes for an Imager::File::whatever module
 sub _reader_autoload {
   my $type = shift;
@@ -1380,6 +1405,44 @@ sub _reader_autoload {
       ++$attempted_to_load{$file};
       require $file;
     };
+    if ($@) {
+      # try to get a reader specific module
+      my $file = "Imager/File/\U$type\EReader.pm";
+      unless ($attempted_to_load{$file}) {
+       eval {
+         ++$attempted_to_load{$file};
+         require $file;
+       };
+      }
+    }
+  }
+}
+
+# probes for an Imager::File::whatever module
+sub _writer_autoload {
+  my $type = shift;
+
+  return if $formats{$type} || $readers{$type};
+
+  return unless $type =~ /^\w+$/;
+
+  my $file = "Imager/File/\U$type\E.pm";
+
+  unless ($attempted_to_load{$file}) {
+    eval {
+      ++$attempted_to_load{$file};
+      require $file;
+    };
+    if ($@) {
+      # try to get a writer specific module
+      my $file = "Imager/File/\U$type\EWriter.pm";
+      unless ($attempted_to_load{$file}) {
+       eval {
+         ++$attempted_to_load{$file};
+         require $file;
+       };
+      }
+    }
   }
 }
 
@@ -1497,96 +1560,111 @@ sub write {
     return undef;
   }
 
-  if (!$formats{$input{'type'}}) { $self->{ERRSTR}='format not supported'; return undef; }
+  _writer_autoload($input{type});
 
-  my ($IO, $fh) = $self->_get_writer_io(\%input, $input{'type'})
-    or return undef;
-
-  if ($input{'type'} eq 'tiff') {
-    $self->_set_opts(\%input, "tiff_", $self)
-      or return undef;
-    $self->_set_opts(\%input, "exif_", $self)
+  my ($IO, $fh);
+  if ($writers{$input{type}} && $writers{$input{type}}{single}) {
+    ($IO, $fh) = $self->_get_writer_io(\%input, $input{'type'})
       or return undef;
 
-    if (defined $input{class} && $input{class} eq 'fax') {
-      if (!i_writetiff_wiol_faxable($self->{IMG}, $IO, $input{fax_fine})) {
-       $self->{ERRSTR} = $self->_error_as_msg();
-       return undef;
-      }
-    } else {
-      if (!i_writetiff_wiol($self->{IMG}, $IO)) {
-       $self->{ERRSTR} = $self->_error_as_msg();
-       return undef;
-      }
-    }
-  } elsif ( $input{'type'} eq 'pnm' ) {
-    $self->_set_opts(\%input, "pnm_", $self)
+    $writers{$input{type}}{single}->($self, $IO, %input)
       or return undef;
-    if ( ! i_writeppm_wiol($self->{IMG},$IO) ) {
-      $self->{ERRSTR} = $self->_error_as_msg();
-      return undef;
-    }
-    $self->{DEBUG} && print "writing a pnm file\n";
-  } elsif ( $input{'type'} eq 'raw' ) {
-    $self->_set_opts(\%input, "raw_", $self)
-      or return undef;
-    if ( !i_writeraw_wiol($self->{IMG},$IO) ) {
-      $self->{ERRSTR} = $self->_error_as_msg();
-      return undef;
-    }
-    $self->{DEBUG} && print "writing a raw file\n";
-  } elsif ( $input{'type'} eq 'png' ) {
-    $self->_set_opts(\%input, "png_", $self)
-      or return undef;
-    if ( !i_writepng_wiol($self->{IMG}, $IO) ) {
-      $self->{ERRSTR}='unable to write png image';
-      return undef;
-    }
-    $self->{DEBUG} && print "writing a png file\n";
-  } elsif ( $input{'type'} eq 'jpeg' ) {
-    $self->_set_opts(\%input, "jpeg_", $self)
-      or return undef;
-    $self->_set_opts(\%input, "exif_", $self)
-      or return undef;
-    if ( !i_writejpeg_wiol($self->{IMG}, $IO, $input{jpegquality})) {
-      $self->{ERRSTR} = $self->_error_as_msg();
-      return undef;
-    }
-    $self->{DEBUG} && print "writing a jpeg file\n";
-  } elsif ( $input{'type'} eq 'bmp' ) {
-    $self->_set_opts(\%input, "bmp_", $self)
-      or return undef;
-    if ( !i_writebmp_wiol($self->{IMG}, $IO) ) {
-      $self->{ERRSTR}='unable to write bmp image';
-      return undef;
-    }
-    $self->{DEBUG} && print "writing a bmp file\n";
-  } elsif ( $input{'type'} eq 'tga' ) {
-    $self->_set_opts(\%input, "tga_", $self)
-      or return undef;
-
-    if ( !i_writetga_wiol($self->{IMG}, $IO, $input{wierdpack}, $input{compress}, $input{idstring}) ) {
-      $self->{ERRSTR}=$self->_error_as_msg();
+  }
+  else {
+    if (!$formats{$input{'type'}}) { 
+      $self->{ERRSTR}='format not supported'; 
       return undef;
     }
-    $self->{DEBUG} && print "writing a tga file\n";
-  } elsif ( $input{'type'} eq 'gif' ) {
-    $self->_set_opts(\%input, "gif_", $self)
+    
+    ($IO, $fh) = $self->_get_writer_io(\%input, $input{'type'})
       or return undef;
-    # compatibility with the old interfaces
-    if ($input{gifquant} eq 'lm') {
-      $input{make_colors} = 'addi';
-      $input{translate} = 'perturb';
-      $input{perturb} = $input{lmdither};
-    } elsif ($input{gifquant} eq 'gen') {
-      # just pass options through
-    } else {
-      $input{make_colors} = 'webmap'; # ignored
-      $input{translate} = 'giflib';
-    }
-    if (!i_writegif_wiol($IO, \%input, $self->{IMG})) {
-      $self->{ERRSTR} = $self->_error_as_msg;
-      return;
+    
+    if ($input{'type'} eq 'tiff') {
+      $self->_set_opts(\%input, "tiff_", $self)
+        or return undef;
+      $self->_set_opts(\%input, "exif_", $self)
+        or return undef;
+      
+      if (defined $input{class} && $input{class} eq 'fax') {
+        if (!i_writetiff_wiol_faxable($self->{IMG}, $IO, $input{fax_fine})) {
+          $self->{ERRSTR} = $self->_error_as_msg();
+          return undef;
+        }
+      } else {
+        if (!i_writetiff_wiol($self->{IMG}, $IO)) {
+          $self->{ERRSTR} = $self->_error_as_msg();
+          return undef;
+        }
+      }
+    } elsif ( $input{'type'} eq 'pnm' ) {
+      $self->_set_opts(\%input, "pnm_", $self)
+        or return undef;
+      if ( ! i_writeppm_wiol($self->{IMG},$IO) ) {
+        $self->{ERRSTR} = $self->_error_as_msg();
+        return undef;
+      }
+      $self->{DEBUG} && print "writing a pnm file\n";
+    } elsif ( $input{'type'} eq 'raw' ) {
+      $self->_set_opts(\%input, "raw_", $self)
+        or return undef;
+      if ( !i_writeraw_wiol($self->{IMG},$IO) ) {
+        $self->{ERRSTR} = $self->_error_as_msg();
+        return undef;
+      }
+      $self->{DEBUG} && print "writing a raw file\n";
+    } elsif ( $input{'type'} eq 'png' ) {
+      $self->_set_opts(\%input, "png_", $self)
+        or return undef;
+      if ( !i_writepng_wiol($self->{IMG}, $IO) ) {
+        $self->{ERRSTR}='unable to write png image';
+        return undef;
+      }
+      $self->{DEBUG} && print "writing a png file\n";
+    } elsif ( $input{'type'} eq 'jpeg' ) {
+      $self->_set_opts(\%input, "jpeg_", $self)
+        or return undef;
+      $self->_set_opts(\%input, "exif_", $self)
+        or return undef;
+      if ( !i_writejpeg_wiol($self->{IMG}, $IO, $input{jpegquality})) {
+        $self->{ERRSTR} = $self->_error_as_msg();
+        return undef;
+      }
+      $self->{DEBUG} && print "writing a jpeg file\n";
+    } elsif ( $input{'type'} eq 'bmp' ) {
+      $self->_set_opts(\%input, "bmp_", $self)
+        or return undef;
+      if ( !i_writebmp_wiol($self->{IMG}, $IO) ) {
+        $self->{ERRSTR}='unable to write bmp image';
+        return undef;
+      }
+      $self->{DEBUG} && print "writing a bmp file\n";
+    } elsif ( $input{'type'} eq 'tga' ) {
+      $self->_set_opts(\%input, "tga_", $self)
+        or return undef;
+      
+      if ( !i_writetga_wiol($self->{IMG}, $IO, $input{wierdpack}, $input{compress}, $input{idstring}) ) {
+        $self->{ERRSTR}=$self->_error_as_msg();
+        return undef;
+      }
+      $self->{DEBUG} && print "writing a tga file\n";
+    } elsif ( $input{'type'} eq 'gif' ) {
+      $self->_set_opts(\%input, "gif_", $self)
+        or return undef;
+      # compatibility with the old interfaces
+      if ($input{gifquant} eq 'lm') {
+        $input{make_colors} = 'addi';
+        $input{translate} = 'perturb';
+        $input{perturb} = $input{lmdither};
+      } elsif ($input{gifquant} eq 'gen') {
+        # just pass options through
+      } else {
+        $input{make_colors} = 'webmap'; # ignored
+        $input{translate} = 'giflib';
+      }
+      if (!i_writegif_wiol($IO, \%input, $self->{IMG})) {
+        $self->{ERRSTR} = $self->_error_as_msg;
+        return;
+      }
     }
   }
 
@@ -1604,10 +1682,12 @@ sub write {
 sub write_multi {
   my ($class, $opts, @images) = @_;
 
-  if (!$opts->{'type'} && $opts->{'file'}) {
-    $opts->{'type'} = $FORMATGUESS->($opts->{'file'});
+  my $type = $opts->{type};
+
+  if (!$type && $opts->{'file'}) {
+    $type = $FORMATGUESS->($opts->{'file'});
   }
-  unless ($opts->{'type'}) {
+  unless ($type) {
     $class->_set_error('type parameter missing and not possible to guess from extension');
     return;
   }
@@ -1619,41 +1699,73 @@ sub write_multi {
   $class->_set_opts($opts, "i_", @images)
     or return;
   my @work = map $_->{IMG}, @images;
-  my ($IO, $file) = $class->_get_writer_io($opts, $opts->{'type'})
-    or return undef;
-  if ($opts->{'type'} eq 'gif') {
-    $class->_set_opts($opts, "gif_", @images)
-      or return;
-    my $gif_delays = $opts->{gif_delays};
-    local $opts->{gif_delays} = $gif_delays;
-    if ($opts->{gif_delays} && !ref $opts->{gif_delays}) {
-      # assume the caller wants the same delay for each frame
-      $opts->{gif_delays} = [ ($gif_delays) x @images ];
-    }
-    my $res = i_writegif_wiol($IO, $opts, @work);
-    $res or $class->_set_error($class->_error_as_msg());
-    return $res;
-  }
-  elsif ($opts->{'type'} eq 'tiff') {
-    $class->_set_opts($opts, "tiff_", @images)
-      or return;
-    $class->_set_opts($opts, "exif_", @images)
-      or return;
-    my $res;
-    $opts->{fax_fine} = 1 unless exists $opts->{fax_fine};
-    if ($opts->{'class'} && $opts->{'class'} eq 'fax') {
-      $res = i_writetiff_multi_wiol_faxable($IO, $opts->{fax_fine}, @work);
+
+  _writer_autoload($type);
+
+  my ($IO, $file);
+  if ($writers{$type} && $writers{$type}{multiple}) {
+    ($IO, $file) = $class->_get_writer_io($opts, $type)
+      or return undef;
+
+    $writers{$type}{multiple}->($class, $IO, $opts, @images)
+      or return undef;
+  }
+  else {
+    if (!$formats{$type}) { 
+      $class->_set_error("format $type not supported"); 
+      return undef;
+    }
+    
+    ($IO, $file) = $class->_get_writer_io($opts, $type)
+      or return undef;
+    
+    if ($type eq 'gif') {
+      $class->_set_opts($opts, "gif_", @images)
+        or return;
+      my $gif_delays = $opts->{gif_delays};
+      local $opts->{gif_delays} = $gif_delays;
+      if ($opts->{gif_delays} && !ref $opts->{gif_delays}) {
+        # assume the caller wants the same delay for each frame
+        $opts->{gif_delays} = [ ($gif_delays) x @images ];
+      }
+      unless (i_writegif_wiol($IO, $opts, @work)) {
+        $class->_set_error($class->_error_as_msg());
+        return undef;
+      }
+    }
+    elsif ($type eq 'tiff') {
+      $class->_set_opts($opts, "tiff_", @images)
+        or return;
+      $class->_set_opts($opts, "exif_", @images)
+        or return;
+      my $res;
+      $opts->{fax_fine} = 1 unless exists $opts->{fax_fine};
+      if ($opts->{'class'} && $opts->{'class'} eq 'fax') {
+        $res = i_writetiff_multi_wiol_faxable($IO, $opts->{fax_fine}, @work);
+      }
+      else {
+        $res = i_writetiff_multi_wiol($IO, @work);
+      }
+      unless ($res) {
+        $class->_set_error($class->_error_as_msg());
+        return undef;
+      }
     }
     else {
-      $res = i_writetiff_multi_wiol($IO, @work);
+      $ERRSTR = "Sorry, write_multi doesn't support $type yet";
+      return 0;
     }
-    $res or $class->_set_error($class->_error_as_msg());
-    return $res;
   }
-  else {
-    $ERRSTR = "Sorry, write_multi doesn't support $opts->{'type'} yet";
-    return 0;
+
+  if (exists $opts->{'data'}) {
+    my $data = io_slurp($IO);
+    if (!$data) {
+      Imager->_set_error('Could not slurp from buffer');
+      return undef;
+    }
+    ${$opts->{data}} = $data;
   }
+  return 1;
 }
 
 # read multiple images from a file
@@ -1710,7 +1822,7 @@ sub read_multi {
     }
   }
 
-  $ERRSTR = "Cannot read multiple images from $opts{'type'} files";
+  $ERRSTR = "Cannot read multiple images from $type files";
   return;
 }
 
@@ -3118,6 +3230,7 @@ sub def_guess_type {
   return 'rgb'  if ($ext eq "rgb");
   return 'gif'  if ($ext eq "gif");
   return 'raw'  if ($ext eq "raw");
+  return lc $ext; # best guess
   return ();
 }
 
index 4777a7c..ce82ff3 100644 (file)
--- a/Imager.xs
+++ b/Imager.xs
@@ -270,7 +270,7 @@ static ssize_t call_writer(struct cbdata *cbd, void const *buf, size_t size) {
   FREETMPS;
   LEAVE;
 
-  return success ? size : 0;
+  return success ? size : -1;
 }
 
 static ssize_t call_reader(struct cbdata *cbd, void *buf, size_t size, 
@@ -375,7 +375,7 @@ static off_t io_seeker(void *p, off_t offset, int whence) {
 static ssize_t io_writer(void *p, void const *data, size_t size) {
   struct cbdata *cbd = p;
 
-  /*printf("io_writer(%p, %p, %u)\n", p, data, size);*/
+  /* printf("io_writer(%p, %p, %u)\n", p, data, size); */
   if (!cbd->writing) {
     if (cbd->reading && cbd->where < cbd->used) {
       /* we read past the place where the caller expected us to be
@@ -457,11 +457,12 @@ static ssize_t io_reader(void *p, void *data, size_t size) {
   return total;
 }
 
-static void io_closer(void *p) {
+static int io_closer(void *p) {
   struct cbdata *cbd = p;
 
   if (cbd->writing && cbd->used > 0) {
-    write_flush(cbd);
+    if (write_flush(cbd) < 0)
+      return -1;
     cbd->writing = 0;
   }
 
@@ -480,6 +481,8 @@ static void io_closer(void *p) {
     FREETMPS;
     LEAVE;
   }
+
+  return 0;
 }
 
 static void io_destroyer(void *p) {
@@ -1172,7 +1175,6 @@ void
 i_io_DESTROY(ig)
         Imager::IO     ig
 
-
 MODULE = Imager                PACKAGE = Imager
 
 PROTOTYPES: ENABLE
@@ -3266,6 +3268,14 @@ i_errors()
          ++i;
        }
 
+void
+i_clear_error()
+
+void
+i_push_error(code, msg)
+       int code
+       const char *msg
+
 undef_int
 i_nearest_color(im, ...)
     Imager::ImgRaw     im
index 96fd422..e890a46 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -18,13 +18,24 @@ ICO/ICO.xs
 ICO/Makefile.PL
 ICO/imicon.c
 ICO/imicon.h
+ICO/lib/Imager/File/CUR.pm
 ICO/msicon.c
 ICO/msicon.h
 ICO/t/t10icon.t
 ICO/t/t20readone.t
 ICO/t/t21readmult.t
+ICO/t/t30cursor.t
+ICO/t/t40readcurone.t
+ICO/t/t41curmultread.t
+ICO/t/t50readfail.t
+ICO/t/t60writefail.t
+ICO/t/t70icosing.t
+ICO/t/t71icomult.t
+ICO/t/t72cursing.t
+ICO/t/t73curmult.t
 ICO/testimg/combo.ico
 ICO/testimg/pal13232.ico
+ICO/testimg/pal43232.cur
 ICO/testimg/pal43232.ico
 ICO/testimg/pal43232.ppm
 ICO/testimg/pal83232.ico
index 2f31f94..eb2e76c 100644 (file)
@@ -44,3 +44,7 @@ Makefile\.old
 ^ICO/ICO\.c$
 ^ICO/testout
 
+# trash from profiling
+\.gcno$
+\.gcda$
+\.gcov$
index d0973f5..ad83c2e 100644 (file)
@@ -871,7 +871,7 @@ YAML
 # this is intended to only be running on the development
 # machines
 sub distcheck {
-  if (-e '.svn') {
+  if (-e '.svn' && -e 'Changes') {
     # update Changes if needed
     my $write_changes;
     # get the last revision from Changes
diff --git a/image.c b/image.c
index 9c70941..3d8880b 100644 (file)
--- a/image.c
+++ b/image.c
@@ -2118,8 +2118,6 @@ struct magic_entry {
 
 static int
 test_magic(unsigned char *buffer, size_t length, struct magic_entry const *magic) {
-  int c;
-
   if (length < magic->magic_size)
     return 0;
   if (magic->mask) {
index e72d0c6..654bbe6 100644 (file)
--- a/imager.h
+++ b/imager.h
@@ -529,6 +529,8 @@ extern int
 i_set_image_file_limits(int width, int height, int bytes);
 extern int
 i_get_image_file_limits(int *width, int *height, int *bytes);
+extern int
+i_int_check_image_file_limits(int width, int height, int channels, int sample_size);
 
 /* memory allocation */
 void* mymalloc(int size);
index 82c3f8d..98cfa2c 100644 (file)
--- a/imageri.h
+++ b/imageri.h
@@ -42,9 +42,6 @@ extern int i_setcolors_forward(i_img *im, int index, const i_color *colors,
 
 extern void i_get_combine(int combine, i_fill_combine_f *, i_fill_combinef_f *);
 
-extern int
-i_int_check_image_file_limits(int width, int height, int channels, int sample_size);
-
 #define im_min(a, b) ((a) < (b) ? (a) : (b))
 #define im_max(a, b) ((a) > (b) ? (a) : (b))
 
diff --git a/imext.c b/imext.c
index 52e8d26..cc4cd0c 100644 (file)
--- a/imext.c
+++ b/imext.c
@@ -96,6 +96,11 @@ im_ext_funcs imager_function_table =
     i_copyto_trans,
     i_copy,
     i_rubthru,
+
+    /* IMAGER_API_LEVEL 2 functions */
+    i_set_image_file_limits,
+    i_get_image_file_limits,
+    i_int_check_image_file_limits,
   };
 
 /* in general these functions aren't called by Imager internally, but
diff --git a/imext.h b/imext.h
index b54dbb8..cb43c45 100644 (file)
--- a/imext.h
+++ b/imext.h
@@ -182,4 +182,11 @@ extern im_ext_funcs *imager_function_ext_table;
 #define i_rubthru(im, src, tx, ty, src_minx, src_miny, src_maxx, src_maxy) \
   ((im_extt->f_i_rubthru)((im), (src), (tx), (ty), (src_minx), (src_miny), (src_maxx), (src_maxy)))
 
+#define i_set_image_file_limits(max_width, max_height, max_bytes) \
+  ((im_extt->f_i_set_image_file_limits)((max_width), (max_height), (max_bytes)))
+#define i_get_image_file_limits(max_width, max_height, max_bytes) \
+  ((im_extt->f_i_get_image_file_limits)((pmax_width), (pmax_height), (pmax_bytes)))
+#define i_int_check_image_file_limits(width, height, channels, sample_size) \
+  ((im_extt->f_i_int_check_image_file_limits)((width), (height), (channels), (sample_size)))
+
 #endif
index a7ff20c..6e8fbad 100644 (file)
@@ -18,7 +18,7 @@
  will result in an increment of IMAGER_API_LEVEL.
 */
 
-#define IMAGER_API_LEVEL 1
+#define IMAGER_API_LEVEL 2
 
 typedef struct {
   int version;
@@ -135,7 +135,12 @@ typedef struct {
   i_img *(*f_i_copy)(i_img *im);
   int (*f_i_rubthru)(i_img *im, i_img *src, int tx, int ty, int src_minx, int src_miny, int src_maxx, int src_maxy);
 
-  /* IMAGER_API_LEVEL 2 functions will be added here */
+  /* IMAGER_API_LEVEL 2 functions */
+  int (*f_i_set_image_file_limits)(int width, int height, int bytes);
+  int (*f_i_get_image_file_limits)(int *width, int *height, int *bytes);
+  int (*f_i_int_check_image_file_limits)(int width, int height, int channels, int sample_size);
+
+  /* IMAGER_API_LEVEL 3 functions will be added here */
 } im_ext_funcs;
 
 #define PERL_FUNCTION_TABLE_NAME "Imager::__ext_func_table"
index 078f9b9..25f2ad6 100644 (file)
--- a/iolayer.c
+++ b/iolayer.c
@@ -113,7 +113,7 @@ Some of these functions are internal.
 static ssize_t fd_read(io_glue *ig, void *buf, size_t count);
 static ssize_t fd_write(io_glue *ig, const void *buf, size_t count);
 static off_t fd_seek(io_glue *ig, off_t offset, int whence);
-static void fd_close(io_glue *ig);
+static int fd_close(io_glue *ig);
 static ssize_t fd_size(io_glue *ig);
 static const char *my_strerror(int err);
 
@@ -157,8 +157,8 @@ realseek_read(io_glue *ig, void *buf, size_t count) {
   size_t        bc = 0;
   char       *cbuf = buf;
 
-  IOL_DEB( printf("realseek_read: fd = %d, ier->cpos = %ld, buf = %p, "
-                  "count = %d\n", fd, (long) ier->cpos, buf, count) );
+  IOL_DEB( printf("realseek_read:  buf = %p, count = %d\n", 
+                 buf, count) );
   /* Is this a good idea? Would it be better to handle differently?
      skip handling? */
   while( count!=bc && (rc = ig->source.cb.readcb(p,cbuf+bc,count-bc))>0 ) {
@@ -218,11 +218,13 @@ actual close or not.  Does nothing for now.  Should be fixed.
 =cut */
 
 static
-void
+int
 realseek_close(io_glue *ig) {
   mm_log((1, "realseek_close(ig %p)\n", ig));
   if (ig->source.cb.closecb)
-    ig->source.cb.closecb(ig->source.cb.p);
+    return ig->source.cb.closecb(ig->source.cb.p);
+  else
+    return 0;
 }
 
 
@@ -284,7 +286,7 @@ ssize_t
 buffer_read(io_glue *ig, void *buf, size_t count) {
   io_ex_buffer *ieb = ig->exdata;
 
-  IOL_DEB( printf("buffer_read: fd = %d, ier->cpos = %ld, buf = %p, count = %d\n", fd, (long) ier->cpos, buf, count) );
+  IOL_DEB( printf("buffer_read: ieb->cpos = %ld, buf = %p, count = %d\n", (long) ieb->cpos, buf, count) );
 
   if ( ieb->cpos+count > ig->source.buffer.len ) {
     mm_log((1,"buffer_read: short read: cpos=%d, len=%d, count=%d\n", ieb->cpos, ig->source.buffer.len));
@@ -293,7 +295,7 @@ buffer_read(io_glue *ig, void *buf, size_t count) {
   
   memcpy(buf, ig->source.buffer.data+ieb->cpos, count);
   ieb->cpos += count;
-  IOL_DEB( printf("buffer_read: rc = %d, count = %d\n", rc, count) );
+  IOL_DEB( printf("buffer_read: count = %d\n", count) );
   return count;
 }
 
@@ -330,10 +332,11 @@ or not.  Does nothing for now.  Should be fixed.
 */
 
 static
-void
+int
 buffer_close(io_glue *ig) {
   mm_log((1, "buffer_close(ig %p)\n", ig));
-  /* FIXME: Do stuff here */
+
+  return 0;
 }
 
 
@@ -684,12 +687,12 @@ or not.  Does nothing for now.  Should be fixed.
 */
 
 static
-void
+int
 bufchain_close(io_glue *ig) {
   mm_log((1, "bufchain_close(ig %p)\n",ig));
   IOL_DEB( printf("bufchain_close(ig %p)\n", ig) );
-  /* FIXME: Commit a seek point here */
-  
+
+  return 0;  
 }
 
 
@@ -1115,8 +1118,9 @@ static off_t fd_seek(io_glue *ig, off_t offset, int whence) {
   return result;
 }
 
-static void fd_close(io_glue *ig) {
+static int fd_close(io_glue *ig) {
   /* no, we don't close it */
+  return 0;
 }
 
 static ssize_t fd_size(io_glue *ig) {
index d56de92..73616c1 100644 (file)
@@ -22,7 +22,7 @@ typedef i_io_glue_t io_glue;
 typedef ssize_t(*i_io_readp_t) (io_glue *ig, void *buf, size_t count);
 typedef ssize_t(*i_io_writep_t)(io_glue *ig, const void *buf, size_t count);
 typedef off_t  (*i_io_seekp_t) (io_glue *ig, off_t offset, int whence);
-typedef void   (*i_io_closep_t)(io_glue *ig);
+typedef int    (*i_io_closep_t)(io_glue *ig);
 typedef ssize_t(*i_io_sizep_t) (io_glue *ig);
 
 typedef void   (*i_io_closebufp_t)(void *p);
@@ -34,7 +34,7 @@ typedef void (*i_io_destroyp_t)(i_io_glue_t *ig);
 typedef ssize_t(*i_io_readl_t) (void *p, void *buf, size_t count);
 typedef ssize_t(*i_io_writel_t)(void *p, const void *buf, size_t count);
 typedef off_t  (*i_io_seekl_t) (void *p, off_t offset, int whence);
-typedef void   (*i_io_closel_t)(void *p);
+typedef int    (*i_io_closel_t)(void *p);
 typedef void   (*i_io_destroyl_t)(void *p);
 typedef ssize_t(*i_io_sizel_t) (void *p);
 
index 927680c..bf64192 100644 (file)
@@ -778,6 +778,120 @@ values for the first line and so on, you can use the interleave option:
 
 There are no PNG specific tags.
 
+=head2 ICO (Microsoft Windows Icon) and CUR (Microsoft Windows Cursor)
+
+Icon and Cursor files are very similar, the only differences being a
+number in the header and the storage of the cursor hotspot.  I've
+treated them separately so that you're not messing with tags to
+distinguish between them.
+
+The following tags are set when reading an icon image and are used
+when writing it:
+
+=over
+
+=item ico_mask
+
+This is the AND mask of the icon.  When used as an icon in Windows 1
+bits in the mask correspond to pixels that are modified by the source
+image rather than simply replaced by the source image.
+
+Rather than requiring a binary bitmap this is accepted in a specific format:
+
+=over
+
+=item *
+
+first line consisting of the 0 placeholder, the 1 placeholder and a
+newline.
+
+=item *
+
+following lines which contain 0 and 1 placeholders for each scanline
+of the image, starting from the top of the image.
+
+=back
+
+When reading an image, '.' is used as the 0 placeholder and '*' as the
+1 placeholder.  An example:
+
+  .*
+  ..........................******
+  ..........................******
+  ..........................******
+  ..........................******
+  ...........................*****
+  ............................****
+  ............................****
+  .............................***
+  .............................***
+  .............................***
+  .............................***
+  ..............................**
+  ..............................**
+  ...............................*
+  ...............................*
+  ................................
+  ................................
+  ................................
+  ................................
+  ................................
+  ................................
+  *...............................
+  **..............................
+  **..............................
+  ***.............................
+  ***.............................
+  ****............................
+  ****............................
+  *****...........................
+  *****...........................
+  *****...........................
+  *****...........................
+
+=back
+
+The following tags are set when reading an icon:
+
+=over
+
+=item ico_bits
+
+The number of bits per pixel used to store the image.
+
+=back
+
+For cursor files the following tags are set and read when reading and
+writing:
+
+=over
+
+=item cur_mask
+
+This is the same as the ico_mask above.
+
+=item cur_hotspotx
+
+=item cur_hotspoty
+
+The "hot" spot of the cursor image.  This is the spot on the cursor
+that you click with.  If you set these to out of range values they are
+clipped to the size of the image when written to the file.
+
+=back
+
+C<cur_bits> is set when reading a cursor.
+
+Examples:
+
+  my $img = Imager->new(xsize => 32, ysize => 32, channels => 4);
+  $im->box(color => 'FF0000');
+  $im->write(file => 'box.ico');
+
+  $im->settag(name => 'cur_hotspotx', value => 16);
+  $im->settag(name => 'cur_hotspoty', value => 16);
+  $im->write(file => 'box.cur');
+
 =head1 ADDING NEW FORMATS
 
 To support a new format for reading, call the register_reader() class
@@ -875,18 +989,88 @@ Example:
      },
     );
 
+=item register_writer
+
+Registers single or multiple image write functions.
+
+Parameters:
+
+=over
+
+=item *
+
+type - the identifier of the file format.  This is typically the
+extension in lowercase.
+
+This parameter is required.
+
+=item *
+
+single - a code ref to write a single image to a file.  This is
+supplied:
+
+=over
+
+=item *
+
+the object that write() was called on,
+
+=item *
+
+an Imager::IO object that should be used to write the file, and
+
+=item *
+
+all the parameters supplied to the write() method.
+
+=back
+
+The single parameter is required.
+
+=item *
+
+multiple - a code ref which is called to write multiple images to a
+file. This is supplied:
+
+=over
+
+=item *
+
+the class name write_multi() was called on, this is typically
+C<Imager>.
+
+=item *
+
+an Imager::IO object that should be used to write the file, and
+
+=item *
+
+all the parameters supplied to the read_multi() method.
+
+=back
+
+=back
+
 =back
 
 If you name the reader module C<Imager::File::>I<your-format-name>
 where I<your-format-name> is a fully upper case version of the type
-value you would pass to read() or read_multi() then Imager will
-attempt to load that module if it has no other way to read that
-format.
+value you would pass to read(), read_multi(), write() or write_multi()
+then Imager will attempt to load that module if it has no other way to
+read or write that format.
 
 For example, if you create a module Imager::File::GIF and the user has
 built Imager without it's normal GIF support then an attempt to read a
 GIF image will attempt to load Imager::File::GIF.
 
+If your module can only handle reading then you can name your module
+C<Imager::File::>I<your-format-name>C<Reader> and Imager will attempt
+to autoload it.
+
+If your module can only handle writing then you can name your module 
+C<Imager::File::>I<your-format-name>C<Writer> and Imager will attempt
+to autoload it.
+
 =head1 EXAMPLES
 
 =head2 Producing an image from a CGI script
index 75c2ff7..b9cd83f 100644 (file)
@@ -1,7 +1,7 @@
 #!perl -w
 use strict;
 use lib 't';
-use Test::More tests => 101;
+use Test::More tests => 106;
 use Imager qw(:all);
 $^W=1; # warnings during command-line tests
 $|=1;  # give us some progress in the test harness
@@ -32,7 +32,7 @@ SKIP:
     $im = Imager->new(xsize=>2, ysize=>2);
     ok(!$im->write(file=>"testout/notiff.tif"), "should fail to write tiff");
     is($im->errstr, 'format not supported', "check no tiff message");
-    skip("no tiff support", 97);
+    skip("no tiff support", 102);
   }
 
   Imager::i_tags_add($img, "i_xres", 0, "300", 0);
@@ -409,4 +409,19 @@ SKIP:
           "Error opening file: Not a TIFF (?:or MDI )?file, bad magic number (8483 \\(0x2123\\)|8993 \\(0x2321\\))", 
        "check error message");
   }
+
+  { # write_multi to data
+    my $data;
+    my $im = Imager->new(xsize => 50, ysize => 50);
+    ok(Imager->write_multi({ data => \$data, type=>'tiff' }, $im, $im),
+       "write multi to in memory");
+    ok(length $data, "make sure something written");
+    my @im = Imager->read_multi(data => $data);
+    is(@im, 2, "make sure we can read it back");
+    is(Imager::i_img_diff($im[0]{IMG}, $im->{IMG}), 0,
+       "check first image");
+    is(Imager::i_img_diff($im[1]{IMG}, $im->{IMG}), 0,
+       "check second image");
+  }
 }
+