]> git.imager.perl.org - imager.git/blobdiff - t/t107bmp.t
expand the matrix multiply overload to allow mult by array ref or number
[imager.git] / t / t107bmp.t
index bf18539309aa81404d795be70306a02da2e6df79..e8db5da07b463ef45707921d7211661866cdac9e 100644 (file)
@@ -1,9 +1,11 @@
 #!perl -w
 use strict;
-use Test::More tests => 89;
+use Test::More tests => 213;
 use Imager qw(:all);
+use Imager::Test qw(test_image_raw is_image is_color3 test_image);
 init_log("testout/t107bmp.log",1);
-#BEGIN { require 't/testtools.pl'; } # BEGIN to apply prototypes
+
+my $debug_writes = 0;
 
 my $base_diff = 0;
 # if you change this make sure you generate new compressed versions
@@ -11,12 +13,7 @@ my $green=i_color_new(0,255,0,255);
 my $blue=i_color_new(0,0,255,255);
 my $red=i_color_new(255,0,0,255);
 
-my $img=Imager::ImgRaw::new(150,150,3);
-
-i_box_filled($img,70,25,130,125,$green);
-i_box_filled($img,20,25,80,125,$blue);
-i_arc($img,75,75,30,0,361,$red);
-i_conv($img,[0.1, 0.2, 0.4, 0.2, 0.1]);
+my $img = test_image_raw();
 
 Imager::i_tags_add($img, 'i_xres', 0, '300', 0);
 Imager::i_tags_add($img, 'i_yres', 0, undef, 300);
@@ -182,7 +179,7 @@ for my $comp (@comp) {
 }
 
 { # check file limits are checked
-  my $limit_file = "testout/t104.ppm";
+  my $limit_file = "testout/t107_24bit.bmp";
   ok(Imager->set_file_limits(reset=>1, width=>149), "set width limit 149");
   my $im = Imager->new;
   ok(!$im->read(file=>$limit_file),
@@ -216,7 +213,448 @@ for my $comp (@comp) {
      "should succeed - just inside bytes limit");
   Imager->set_file_limits(reset=>1);
 }
-                              
+
+{ # various short read failure tests, each entry has:
+  # source filename, size, expected error
+  # these have been selected based on code coverage, to check each
+  # failure path is checked, where practical
+  my @tests =
+    (
+     [ 
+      "file truncated inside header",
+      "winrgb2.bmp", 
+      20, "file too short to be a BMP file"
+     ],
+     [
+      "1-bit, truncated inside palette",
+      "winrgb2.bmp", 
+      56, "reading BMP palette" 
+     ],
+     [ 
+      "1-bit, truncated in offset region",
+      "winrgb2off.bmp", 64, "failed skipping to image data offset" 
+     ],
+     [ 
+      "1-bit, truncated in image data",
+      "winrgb2.bmp", 96, "failed reading 1-bit bmp data"
+     ],
+     [
+      "4-bit, truncated inside palette",
+      "winrgb4.bmp",
+      56, "reading BMP palette"
+     ],
+     [
+      "4-bit, truncated in offset region",
+      "winrgb4off.bmp", 120, "failed skipping to image data offset",
+     ],
+     [
+      "4-bit, truncate in image data",
+      "winrgb4.bmp", 120, "failed reading 4-bit bmp data"
+     ],
+     [
+      "4-bit RLE, truncate in uncompressed data",
+      "comp4.bmp", 0x229, "missing data during decompression"
+     ],
+     [
+      "8-bit, truncated in palette",
+      "winrgb8.bmp", 1060, "reading BMP palette"
+      ],
+     [
+      "8-bit, truncated in offset region",
+      "winrgb8off.bmp", 1080, "failed skipping to image data offset"
+     ],
+     [
+      "8-bit, truncated in image data",
+      "winrgb8.bmp", 1080, "failed reading 8-bit bmp data"
+     ],
+     [
+      "8-bit RLE, truncate in uncompressed data",
+      "comp8.bmp", 0x68C, "missing data during decompression"
+     ],
+     [
+      "24-bit, truncate in offset region",
+      "winrgb24off.bmp", 56, "failed skipping to image data offset",
+     ],
+     [
+      "24-bit, truncate in image data",
+      "winrgb24.bmp", 100, "failed reading image data",
+     ],
+    );
+
+  my $test_index = 0;
+  for my $test (@tests) {
+    my ($desc, $srcfile, $size, $error) = @$test;
+    my $im = Imager->new;
+    open IMDATA, "< testimg/$srcfile"
+      or die "$test_index - $desc: Cannot open testimg/$srcfile: $!";
+    binmode IMDATA;
+    my $data;
+    read(IMDATA, $data, $size) == $size
+      or die "$test_index - $desc: Could not read $size data from $srcfile";
+    close IMDATA;
+    ok(!$im->read(data => $data, type =>'bmp'),
+       "$test_index - $desc: Should fail to read");
+    is($im->errstr, $error, "$test_index - $desc: check message");
+    ++$test_index;
+  }
+}
+
+{ # various short read success tests, each entry has:
+  # source filename, size, expected tags
+  print "# allow_incomplete tests\n";
+  my @tests =
+    (
+     [ 
+      "1-bit",
+      "winrgb2.bmp", 96,
+      {
+       bmp_compression_name => 'BI_RGB',
+       bmp_compression => 0,
+       bmp_used_colors => 2,
+       i_lines_read => 8,
+      },
+     ],
+     [
+      "4-bit",
+      "winrgb4.bmp", 250,
+      {
+       bmp_compression_name => 'BI_RGB',
+       bmp_compression => 0,
+       bmp_used_colors => 16,
+       i_lines_read => 11,
+      },
+     ],
+     [
+      "4-bit RLE - uncompressed seq",
+      "comp4.bmp", 0x229, 
+      {
+       bmp_compression_name => 'BI_RLE4',
+       bmp_compression => 2,
+       bmp_used_colors => 16,
+       i_lines_read => 44,
+      },
+     ],
+     [
+      "4-bit RLE - start seq",
+      "comp4.bmp", 0x97, 
+      {
+       bmp_compression_name => 'BI_RLE4',
+       bmp_compression => 2,
+       bmp_used_colors => 16,
+       i_lines_read => 8,
+      },
+     ],
+     [
+      "8-bit",
+      "winrgb8.bmp", 1250,
+      {
+       bmp_compression_name => 'BI_RGB',
+       bmp_compression => 0,
+       bmp_used_colors => 256,
+       i_lines_read => 8,
+      },
+     ],
+     [
+      "8-bit RLE - uncompressed seq",
+      "comp8.bmp", 0x68C, 
+      {
+       bmp_compression_name => 'BI_RLE8',
+       bmp_compression => 1,
+       bmp_used_colors => 256,
+       i_lines_read => 27,
+      },
+     ],
+     [
+      "8-bit RLE - initial seq",
+      "comp8.bmp", 0x487, 
+      {
+       bmp_compression_name => 'BI_RLE8',
+       bmp_compression => 1,
+       bmp_used_colors => 256,
+       i_lines_read => 20,
+      },
+     ],
+     [
+      "24-bit",
+      "winrgb24.bmp", 800,
+      {
+       bmp_compression_name => 'BI_RGB',
+       bmp_compression => 0,
+       bmp_used_colors => 0,
+       i_lines_read => 12,
+      },
+     ],
+    );
+
+  my $test_index = 0;
+  for my $test (@tests) {
+    my ($desc, $srcfile, $size, $tags) = @$test;
+    my $im = Imager->new;
+    open IMDATA, "< testimg/$srcfile"
+      or die "$test_index - $desc: Cannot open testimg/$srcfile: $!";
+    binmode IMDATA;
+    my $data;
+    read(IMDATA, $data, $size) == $size
+      or die "$test_index - $desc: Could not read $size data from $srcfile";
+    close IMDATA;
+    ok($im->read(data => $data, type =>'bmp', allow_incomplete => 1),
+       "$test_index - $desc: Should read successfully");
+    # check standard tags are set
+    is($im->tags(name => 'i_format'), 'bmp',
+       "$test_index - $desc: i_format set");
+    is($im->tags(name => 'i_incomplete'), 1, 
+       "$test_index - $desc: i_incomplete set");
+    my %check_tags;
+    for my $key (keys %$tags) {
+      $check_tags{$key} = $im->tags(name => $key);
+    }
+    is_deeply(\%check_tags, $tags, "$test_index - $desc: check tags");
+    ++$test_index;
+  }
+}
+
+{ # check handling of reading images with negative height
+  # each entry is:
+  # source file, description
+  print "# check handling of negative height values\n";
+  my @tests =
+    (
+     [ "winrgb2.bmp", "1-bit, uncompressed" ],
+     [ "winrgb4.bmp", "4-bit, uncompressed" ],
+     [ "winrgb8.bmp", "8-bit, uncompressed" ],
+     [ "winrgb24.bmp", "24-bit, uncompressed" ],
+     [ "comp4.bmp", "4-bit, RLE" ],
+     [ "comp8.bmp", "8-bit, RLE" ],
+    );
+  my $test_index = 0;
+  for my $test (@tests) {
+    my ($file, $desc) = @$test;
+    open IMDATA, "< testimg/$file"
+      or die "$test_index - Cannot open $file: $!";
+    binmode IMDATA;
+    my $data = do { local $/; <IMDATA> };
+    close IMDATA;
+    my $im_orig = Imager->new;
+    $im_orig->read(data => $data)
+      or die "Cannot load original $file: ", $im_orig->errstr;
+    
+    # now negate the height
+    my $orig_height = unpack("V", substr($data, 0x16, 4));
+    my $neg_height = 0xFFFFFFFF & ~($orig_height - 1);
+    substr($data, 0x16, 4) = pack("V", $neg_height);
+
+    # and read the modified image
+    my $im = Imager->new;
+    ok($im->read(data => $data),
+       "$test_index - $desc: read negated height image")
+      or print "# ", $im->errstr, "\n";
+
+    # flip orig to match what we should get
+    $im_orig->flip(dir => 'v');
+
+    # check it out
+    is_image($im, $im_orig, "$test_index - $desc: check image");
+
+    ++$test_index;
+  }
+}
+
+{ print "# patched data read failure tests\n";
+  # like the "various invalid format" tests, these generate fail
+  # images from other images included with Imager without providing a
+  # full bmp source, saving on dist size and focusing on the changes needed
+  # to cause the failure
+  # each entry is: source file, patches, expected error, description
+  
+  my @tests =
+    (
+     # low image data offsets
+     [ 
+      "winrgb2.bmp", 
+      { 10 => "3d 00 00 00" }, 
+      "image data offset too small (61)",
+      "1-bit, small image offset"
+     ],
+     [ 
+      "winrgb4.bmp", 
+      { 10 => "75 00 00 00" }, 
+      "image data offset too small (117)",
+      "4-bit, small image offset"
+     ],
+     [ 
+      "winrgb8.bmp", 
+      { 10 => "35 04 00 00" }, 
+      "image data offset too small (1077)",
+      "8-bit, small image offset"
+     ],
+     [ 
+      "winrgb24.bmp", 
+      { 10 => "35 00 00 00" }, 
+      "image data offset too small (53)",
+      "24-bit, small image offset"
+     ],
+     # compression issues
+     [
+      "comp8.bmp",
+      { 0x436 => "97" },
+      "invalid data during decompression",
+      "8bit, RLE run beyond edge of image"
+     ],
+     [
+      # caused glibc malloc or valgrind to complain
+      "comp8.bmp",
+      { 0x436 => "94 00 00 03" },
+      "invalid data during decompression",
+      "8bit, literal run beyond edge of image"
+     ],
+     [
+      "comp4.bmp",
+      { 0x76 => "FF bb FF BB" },
+      "invalid data during decompression",
+      "4bit - RLE run beyond edge of image"
+     ],
+     [
+      "comp4.bmp",
+      { 0x76 => "94 bb 00 FF" },
+      "invalid data during decompression",
+      "4bit - literal run beyond edge of image"
+     ],
+    );
+  my $test_index = 0;
+  for my $test (@tests) {
+    my ($filename, $patches, $error, $desc) = @$test;
+
+    my $data = load_patched_file("testimg/$filename", $patches);
+    my $im = Imager->new;
+    ok(!$im->read(data => $data, type=>'bmp'),
+       "$test_index - $desc:should fail to read");
+    is($im->errstr, $error, "$test_index - $desc:check message");
+    ++$test_index;
+  }
+}
+
+{ # various write failure tests
+  # each entry is:
+  # source, limit, expected error, description
+  my @tests =
+    (
+     [ 
+      "winrgb2.bmp", 1, 
+      "cannot write bmp header: limit reached",
+      "1-bit, writing header" 
+     ],
+     [ 
+      "winrgb4.bmp", 1, 
+      "cannot write bmp header: limit reached",
+      "4-bit, writing header" 
+     ],
+     [ 
+      "winrgb8.bmp", 1, 
+      "cannot write bmp header: limit reached",
+      "8-bit, writing header" 
+     ],
+     [ 
+      "winrgb24.bmp", 1, 
+      "cannot write bmp header: limit reached",
+      "24-bit, writing header" 
+     ],
+     [ 
+      "winrgb2.bmp", 0x38, 
+      "cannot write palette entry: limit reached",
+      "1-bit, writing palette" 
+     ],
+     [ 
+      "winrgb4.bmp", 0x38, 
+      "cannot write palette entry: limit reached",
+      "4-bit, writing palette" 
+     ],
+     [ 
+      "winrgb8.bmp", 0x38, 
+      "cannot write palette entry: limit reached",
+      "8-bit, writing palette" 
+     ],
+     [ 
+      "winrgb2.bmp", 0x40, 
+      "writing 1 bit/pixel packed data: limit reached",
+      "1-bit, writing image data" 
+     ],
+     [ 
+      "winrgb4.bmp", 0x80, 
+      "writing 4 bit/pixel packed data: limit reached",
+      "4-bit, writing image data" 
+     ],
+     [ 
+      "winrgb8.bmp", 0x440, 
+      "writing 8 bit/pixel packed data: limit reached",
+      "8-bit, writing image data" 
+     ],
+     [ 
+      "winrgb24.bmp", 0x39, 
+      "writing image data: limit reached",
+      "24-bit, writing image data" 
+     ],
+    );
+  print "# write failure tests\n";
+  my $test_index = 0;
+  for my $test (@tests) {
+    my ($file, $limit, $error, $desc) = @$test;
+
+    my $im = Imager->new;
+    $im->read(file => "testimg/$file")
+      or die "Cannot read $file: ", $im->errstr;
+
+    ok(!$im->write(type => 'bmp', callback => limited_write($limit),
+                  maxbuffer => 1),
+       "$test_index - $desc: write should fail");
+    is($im->errstr, $error, "$test_index - $desc: check error message");
+
+    ++$test_index;
+  }
+}
+
+{
+  ok(grep($_ eq 'bmp', Imager->read_types), "check bmp in read types");
+  ok(grep($_ eq 'bmp', Imager->write_types), "check bmp in write types");
+}
+
+{
+  # RT #30075
+  # give 4/2 channel images a background color when saving to BMP
+  my $im = Imager->new(xsize=>16, ysize=>16, channels=>4);
+  $im->box(filled => 1, xmin => 8, color => '#FFE0C0');
+  $im->box(filled => 1, color => NC(0, 192, 192, 128),
+          ymin => 8, xmax => 7);
+  ok($im->write(file=>"testout/t107_alpha.bmp", type=>'bmp'),
+     "should succeed writing 4 channel image");
+  my $imread = Imager->new;
+  ok($imread->read(file => 'testout/t107_alpha.bmp'), "read it back");
+  is_color3($imread->getpixel('x' => 0, 'y' => 0), 0, 0, 0, 
+           "check transparent became black");
+  is_color3($imread->getpixel('x' => 8, 'y' => 0), 255, 224, 192,
+           "check color came through");
+  is_color3($imread->getpixel('x' => 0, 'y' => 15), 0, 96, 96,
+           "check translucent came through");
+  my $data;
+  ok($im->write(data => \$data, type => 'bmp', i_background => '#FF0000'),
+     "write with red background");
+  ok($imread->read(data => $data, type => 'bmp'),
+     "read it back");
+  is_color3($imread->getpixel('x' => 0, 'y' => 0), 255, 0, 0, 
+           "check transparent became red");
+  is_color3($imread->getpixel('x' => 8, 'y' => 0), 255, 224, 192,
+           "check color came through");
+  is_color3($imread->getpixel('x' => 0, 'y' => 15), 127, 96, 96,
+           "check translucent came through");
+}
+
+{ # RT 41406
+  my $data;
+  my $im = test_image();
+  ok($im->write(data => \$data, type => 'bmp'), "write using OO");
+  my $size = unpack("V", substr($data, 34, 4));
+  is($size, 67800, "check data size");
+}
+
 sub write_test {
   my ($im, $filename) = @_;
   local *FH;
@@ -291,3 +729,37 @@ sub read_test {
   }
 }
 
+sub limited_write {
+  my ($limit) = @_;
+
+  return
+     sub {
+       my ($data) = @_;
+       $limit -= length $data;
+       if ($limit >= 0) {
+         print "# write of ", length $data, " bytes successful ($limit left)\n" if $debug_writes;
+         return 1;
+       }
+       else {
+         print "# write of ", length $data, " bytes failed\n";
+         Imager::i_push_error(0, "limit reached");
+         return;
+       }
+     };
+}
+
+sub load_patched_file {
+  my ($filename, $patches) = @_;
+
+  open IMDATA, "< $filename"
+    or die "Cannot open $filename: $!";
+  binmode IMDATA;
+  my $data = do { local $/; <IMDATA> };
+  for my $offset (keys %$patches) {
+    (my $hdata = $patches->{$offset}) =~ tr/ //d;
+    my $pdata = pack("H*", $hdata);
+    substr($data, $offset, length $pdata) = $pdata;
+  }
+
+  return $data;
+}