The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!perl -w
use strict;
use Test::More tests => 244;
use Imager qw(:all :handy);
use Imager::Test qw(is_color3 is_fcolor3);

-d "testout" or mkdir "testout";

Imager->open_log(log => "testout/t020masked.log");

my $base_rgb = Imager::ImgRaw::new(100, 100, 3);
# put something in there
my $black = NC(0, 0, 0);
my $red = NC(255, 0, 0);
my $green = NC(0, 255, 0);
my $blue = NC(0, 0, 255);
my $white = NC(255, 255, 255);
my $grey = NC(128, 128, 128);
use Imager::Color::Float;
my $redf = Imager::Color::Float->new(1, 0, 0);
my $greenf = Imager::Color::Float->new(0, 1, 0);
my $bluef = Imager::Color::Float->new(0, 0, 1);
my $greyf = Imager::Color::Float->new(0.5, 0.5, 0.5);
my @cols = ($red, $green, $blue);
for my $y (0..99) {
  Imager::i_plin($base_rgb, 0, $y, ($cols[$y % 3] ) x 100);
}

# first a simple subset image
my $s_rgb = Imager::i_img_masked_new($base_rgb, undef, 25, 25, 50, 50);

is(Imager::i_img_getchannels($s_rgb), 3,
   "1 channel image channel count match");
ok(Imager::i_img_getmask($s_rgb) & 1,
   "1 channel image mask");
ok(Imager::i_img_virtual($s_rgb),
   "1 channel image thinks it isn't virtual");
is(Imager::i_img_bits($s_rgb), 8,
   "1 channel image has bits == 8");
is(Imager::i_img_type($s_rgb), 0, # direct
   "1 channel image is direct");

my @ginfo = i_img_info($s_rgb);
is($ginfo[0], 50, "check width");
is($ginfo[1], 50, "check height");

# sample some pixels through the subset
my $c = Imager::i_get_pixel($s_rgb, 0, 0);
is_color3($c, 0, 255, 0, "check (0,0)");
$c = Imager::i_get_pixel($s_rgb, 49, 49);
# (25+49)%3 = 2
is_color3($c, 0, 0, 255, "check (49,49)");

# try writing to it
for my $y (0..49) {
  Imager::i_plin($s_rgb, 0, $y, ($cols[$y % 3]) x 50);
}
pass("managed to write to it");
# and checking the target image
$c = Imager::i_get_pixel($base_rgb, 25, 25);
is_color3($c, 255, 0, 0, "check (25,25)");
$c = Imager::i_get_pixel($base_rgb, 29, 29);
is_color3($c, 0, 255, 0, "check (29,29)");

undef $s_rgb;

# a basic background
for my $y (0..99) {
  Imager::i_plin($base_rgb, 0, $y, ($red ) x 100);
}
my $mask = Imager::ImgRaw::new(50, 50, 1);
# some venetian blinds
for my $y (4..20) {
  Imager::i_plin($mask, 5, $y*2, ($white) x 40);
}
# with a strip down the middle
for my $y (0..49) {
  Imager::i_plin($mask, 20, $y, ($white) x 8);
}
my $m_rgb = Imager::i_img_masked_new($base_rgb, $mask, 25, 25, 50, 50);
ok($m_rgb, "make masked with mask");
for my $y (0..49) {
  Imager::i_plin($m_rgb, 0, $y, ($green) x 50);
}
my @color_tests =
  (
   [ 25+0,  25+0,  $red ],
   [ 25+19, 25+0,  $red ],
   [ 25+20, 25+0,  $green ],
   [ 25+27, 25+0,  $green ],
   [ 25+28, 25+0,  $red ],
   [ 25+49, 25+0,  $red ],
   [ 25+19, 25+7,  $red ],
   [ 25+19, 25+8,  $green ],
   [ 25+19, 25+9,  $red ],
   [ 25+0,  25+8,  $red ],
   [ 25+4,  25+8,  $red ],
   [ 25+5,  25+8,  $green ],
   [ 25+44, 25+8,  $green ],
   [ 25+45, 25+8,  $red ],
   [ 25+49, 25+49, $red ],
  );
my $test_num = 15;
for my $test (@color_tests) {
  my ($x, $y, $testc) = @$test;
  my ($r, $g, $b) = $testc->rgba;
  my $c = Imager::i_get_pixel($base_rgb, $x, $y);
  is_color3($c, $r, $g, $b, "at ($x, $y)");
}

{
  # tests for the OO versions, fairly simple, since the basic functionality
  # is covered by the low-level interface tests
   
  my $base = Imager->new(xsize=>100, ysize=>100);
  ok($base, "make base OO image");
  $base->box(color=>$blue, filled=>1); # fill it all
  my $mask = Imager->new(xsize=>80, ysize=>80, channels=>1);
  $mask->box(color=>$white, filled=>1, xmin=>5, xmax=>75, ymin=>5, ymax=>75);
  my $m_img = $base->masked(mask=>$mask, left=>5, top=>5);
  ok($m_img, "make masked OO image");
  is($m_img->getwidth, 80, "check width");
  $m_img->box(color=>$green, filled=>1);
  my $c = $m_img->getpixel(x=>0, y=>0);
  is_color3($c, 0, 0, 255, "check (0,0)");
  $c = $m_img->getpixel(x => 5, y => 5);
  is_color3($c, 0, 255, 0, "check (5,5)");

  # older versions destroyed the Imager::ImgRaw object manually in 
  # Imager::DESTROY rather than letting Imager::ImgRaw::DESTROY 
  # destroy the object
  # so we test here by destroying the base and mask objects and trying 
  # to draw to the masked wrapper
  # you may need to test with ElectricFence to trigger the problem
  undef $mask;
  undef $base;
  $m_img->box(color=>$blue, filled=>1);
  pass("didn't crash unreffing base or mask for masked image");
}

# 35.7% cover on maskimg.c up to here

{ # error handling:
  my $base = Imager->new(xsize => 100, ysize => 100);
  ok($base, "make base");
  { #  make masked image subset outside of the base image
    my $masked = $base->masked(left => 100);
    ok (!$masked, "fail to make empty masked");
    is($base->errstr, "subset outside of target image", "check message");
  }
}

{ # size limiting
  my $base = Imager->new(xsize => 10, ysize => 10);
  ok($base, "make base for size limit tests");
  {
    my $masked = $base->masked(left => 5, right => 15);
    ok($masked, "make masked");
    is($masked->getwidth, 5, "check width truncated");
  }
  {
    my $masked = $base->masked(top => 5, bottom => 15);
    ok($masked, "make masked");
    is($masked->getheight, 5, "check height truncated");
  }
}
# 36.7% up to here

$mask = Imager->new(xsize => 80, ysize => 80, channels => 1);
$mask->box(filled => 1, color => $white, xmax => 39, ymax => 39);
$mask->box(fill => { hatch => "check1x1" }, ymin => 40, xmax => 39);

{
  my $base = Imager->new(xsize => 100, ysize => 100, bits => "double");
  ok($base, "base for single pixel tests");
  is($base->type, "direct", "check type");
  my $masked = $base->masked(mask => $mask, left => 1, top => 2);
  my $limited = $base->masked(left => 1, top => 2);

  is($masked->type, "direct", "check masked is same type as base");
  is($limited->type, "direct", "check limited is same type as base");

  {
    # single pixel writes, masked
    {
      ok($masked->setpixel(x => 1, y => 3, color => $green),
	 "set (1,3) in masked (2, 5) in based");
      my $c = $base->getpixel(x => 2, y => 5);
      is_color3($c, 0, 255, 0, "check it wrote through");
      ok($masked->setpixel(x => 45, y => 2, color => $red),
	 "set (45,2) in masked (46,4) in base (no mask)");
    $c = $base->getpixel(x => 46, y => 4);
      is_color3($c, 0, 0, 0, "shouldn't have written through");
    }
    {
      ok($masked->setpixel(x => 2, y => 3, color => $redf),
	 "write float red to (2,3) base(3,5)");
      my $c = $base->getpixel(x => 3, y => 5);
      is_color3($c, 255, 0, 0, "check it wrote through");
      ok($masked->setpixel(x => 45, y => 3, color => $greenf),
	 "set float (45,3) in masked (46,5) in base (no mask)");
      $c = $base->getpixel(x => 46, y => 5);
      is_color3($c, 0, 0, 0, "check it didn't write");
    }
    {
      # write out of range should fail
      is($masked->setpixel(x => 80, y => 0, color => $green), "0 but true",
	 "write 8-bit color out of range");
      is($masked->setpixel(x => 0, y => 80, color => $greenf), "0 but true",
	 "write float color out of range");
    }
  }

  # 46.9

  {
    print "# plin coverage\n";
    {
      $base->box(filled => 1, color => $black);
      # plin masked
      # simple path
      is($masked->setscanline(x => 76, y => 1, pixels => [ ($red, $green) x 3 ]),
	 4, "try to write 6 pixels, but only write 4");
      is_deeply([ $base->getsamples(x => 77, y => 3, width => 4) ],
		[ ( 0 ) x 12 ],
		"check not written through");
      # !simple path
      is($masked->setscanline(x => 4, y => 2, pixels => [ ($red, $green, $blue, $grey) x (72/4) ]),
	 72, "write many pixels (masked)");
      is_deeply([ $base->getsamples(x => 5, y => 4, width => 72) ],
		[ ( (255, 0, 0), (0, 255, 0), (0, 0, 255), (128, 128, 128)) x 9,
		  ( 0, 0, 0 ) x 36 ],
		"check written through to base");
      
      # simple path, due to number of transitions
      is($masked->setscanline(x => 0, y => 40, pixels => [ ($red, $green, $blue, $grey) x 5 ]),
	 20, "try to write 20 pixels, with alternating write through");
      is_deeply([ $base->getsamples(x => 1, y => 42, width => 20) ],
		[ ( (0, 0, 0), (0,255,0), (0,0,0), (128,128,128) ) x 5 ],
		"check correct pixels written through");
    }
    
    {
      $base->box(filled => 1, color => $black);
      # plin, non-masked path
      is($limited->setscanline(x => 4, y => 2, pixels => [ ($red, $green, $blue, $grey) x (72/4) ]),
	 72, "write many pixels (limited)");
      is_deeply([ $base->getsamples(x => 5, y => 4, width => 72) ],
		[ ( (255, 0, 0), (0, 255, 0), (0, 0, 255), (128, 128, 128)) x 18 ],
		"check written through to based");
    }
    
    {
      # draw outside fails
      is($masked->setscanline(x => 80, y => 2, pixels => [ $red, $green ]),
	 0, "check writing no pixels");
    }
  }

  {
    print "# plinf coverage\n";
    {
      $base->box(filled => 1, color => $black);
      # plinf masked
      # simple path
      is($masked->setscanline(x => 76, y => 1, pixels => [ ($redf, $greenf) x 3 ]),
	 4, "try to write 6 pixels, but only write 4");
      is_deeply([ $base->getsamples(x => 77, y => 3, width => 4, type => "float") ],
		[ ( 0 ) x 12 ],
		"check not written through");
      # !simple path
      is($masked->setscanline(x => 4, y => 2, pixels => [ ($redf, $greenf, $bluef, $greyf) x (72/4) ]),
	 72, "write many pixels (masked)");
      is_deeply([ $base->getsamples(x => 5, y => 4, width => 72, type => "float") ],
		[ ( (1, 0, 0), (0, 1, 0), (0, 0, 1), (0.5, 0.5, 0.5)) x 9,
		  ( 0, 0, 0 ) x 36 ],
		"check written through to base");
      
      # simple path, due to number of transitions
      is($masked->setscanline(x => 0, y => 40, pixels => [ ($redf, $greenf, $bluef, $greyf) x 5 ]),
	 20, "try to write 20 pixels, with alternating write through");
      is_deeply([ $base->getsamples(x => 1, y => 42, width => 20, type => "float") ],
		[ ( (0, 0, 0), (0,1,0), (0,0,0), (0.5,0.5,0.5) ) x 5 ],
		"check correct pixels written through");
    }
    
    {
      $base->box(filled => 1, color => $black);
      # plinf, non-masked path
      is($limited->setscanline(x => 4, y => 2, pixels => [ ($redf, $greenf, $bluef, $greyf) x (72/4) ]),
	 72, "write many pixels (limited)");
      is_deeply([ $base->getsamples(x => 5, y => 4, width => 72, type => "float") ],
		[ ( (1, 0, 0), (0, 1, 0), (0, 0, 1), (0.5, 0.5, 0.5)) x 18 ],
		"check written through to based");
    }
    
    {
      # draw outside fails
      is($masked->setscanline(x => 80, y => 2, pixels => [ $redf, $greenf ]),
	 0, "check writing no pixels");
    }
  }
  # 71.4%
  {
    {
      print "# gpix\n";
      # gpix
      $base->box(filled => 1, color => $black);
      ok($base->setpixel(x => 4, y => 10, color => $red),
	 "set base(4,10) to red");
      is_fcolor3($masked->getpixel(x => 3, y => 8),
		 255, 0, 0, "check pixel written");

      # out of range
      is($masked->getpixel(x => -1, y => 1),
	 undef, "check failure to left");
      is($masked->getpixel(x => 0, y => -1),
	 undef, "check failure to top");
      is($masked->getpixel(x => 80, y => 1),
	 undef, "check failure to right");
      is($masked->getpixel(x => 0, y => 80),
	 undef, "check failure to bottom");
    }
    {
      print "# gpixf\n";
      # gpixf
      $base->box(filled => 1, color => $black);
      ok($base->setpixel(x => 4, y => 10, color => $redf),
	 "set base(4,10) to red");
      is_fcolor3($masked->getpixel(x => 3, y => 8, type => "float"),
		 1.0, 0, 0, 0, "check pixel written");

      # out of range
      is($masked->getpixel(x => -1, y => 1, type => "float"),
	 undef, "check failure to left");
      is($masked->getpixel(x => 0, y => -1, type => "float"),
	 undef, "check failure to top");
      is($masked->getpixel(x => 80, y => 1, type => "float"),
	 undef, "check failure to right");
      is($masked->getpixel(x => 0, y => 80, type => "float"),
	 undef, "check failure to bottom");
    }
  }
  # 74.5
  {
    {
      print "# glin\n";
      $base->box(filled => 1, color => $black);
      is($base->setscanline(x => 31, y => 3, 
			    pixels => [ ( $red, $green) x 10 ]),
	 20, "write 20 pixels to base image");
      my @colors = $masked->
	getscanline(x => 30, y => 1, width => 20);
      is(@colors, 20, "check we got right number of colors");
      is_color3($colors[0], 255, 0, 0, "check first pixel");
      is_color3($colors[19], 0, 255, 0, "check last pixel");

      @colors = $masked->getscanline(x => 76, y => 2, width => 10);
      is(@colors, 4, "read line from right edge");
      is_color3($colors[0], 0, 0, 0, "check pixel");

      is_deeply([ $masked->getscanline(x => -1, y => 0, width => 1) ],
	 [], "fail read left of image");
      is_deeply([ $masked->getscanline(x => 0, y => -1, width => 1) ],
	 [], "fail read top of image");
      is_deeply([$masked->getscanline(x => 80, y => 0, width => 1)],
	 [], "fail read right of image");
      is_deeply([$masked->getscanline(x => 0, y => 80, width => 1)],
	 [], "fail read bottom of image");
    }
    {
      print "# glinf\n";
      $base->box(filled => 1, color => $black);
      is($base->setscanline(x => 31, y => 3, 
			    pixels => [ ( $redf, $greenf) x 10 ]),
	 20, "write 20 pixels to base image");
      my @colors = $masked->
	getscanline(x => 30, y => 1, width => 20, type => "float");
      is(@colors, 20, "check we got right number of colors");
      is_fcolor3($colors[0], 1.0, 0, 0, 0, "check first pixel");
      is_fcolor3($colors[19], 0, 1.0, 0, 0, "check last pixel");

      @colors = $masked->
	getscanline(x => 76, y => 2, width => 10, type => "float");
      is(@colors, 4, "read line from right edge");
      is_fcolor3($colors[0], 0, 0, 0, 0, "check pixel");

      is_deeply([ $masked->getscanline(x => -1, y => 0, width => 1, type => "float") ],
	 [], "fail read left of image");
      is_deeply([ $masked->getscanline(x => 0, y => -1, width => 1, type => "float") ],
	 [], "fail read top of image");
      is_deeply([$masked->getscanline(x => 80, y => 0, width => 1, type => "float")],
	 [], "fail read right of image");
      is_deeply([$masked->getscanline(x => 0, y => 80, width => 1, type => "float")],
	 [], "fail read bottom of image");
    }
  }
  # 81.6%
  {
    {
      print "# gsamp\n";
      $base->box(filled => 1, color => $black);
      is($base->setscanline(x => 31, y => 3, 
			    pixels => [ ( $red, $green) x 10 ]),
	 20, "write 20 pixels to base image");
      my @samps = $masked->
	getsamples(x => 30, y => 1, width => 20);
      is(@samps, 60, "check we got right number of samples");
      is_deeply(\@samps,
		[ (255, 0, 0, 0, 255, 0) x 10 ],
		"check it");

      @samps = $masked->
	getsamples(x => 76, y => 2, width => 10);
      is(@samps, 12, "read line from right edge");
      is_deeply(\@samps, [ (0, 0, 0) x 4], "check result");

      is_deeply([ $masked->getsamples(x => -1, y => 0, width => 1) ],
	 [], "fail read left of image");
      is_deeply([ $masked->getsamples(x => 0, y => -1, width => 1) ],
	 [], "fail read top of image");
      is_deeply([$masked->getsamples(x => 80, y => 0, width => 1)],
	 [], "fail read right of image");
      is_deeply([$masked->getsamples(x => 0, y => 80, width => 1)],
	 [], "fail read bottom of image");
    }
    {
      print "# gsampf\n";
      $base->box(filled => 1, color => $black);
      is($base->setscanline(x => 31, y => 3, 
			    pixels => [ ( $redf, $greenf) x 10 ]),
	 20, "write 20 pixels to base image");
      my @samps = $masked->
	getsamples(x => 30, y => 1, width => 20, type => "float");
      is(@samps, 60, "check we got right number of samples");
      is_deeply(\@samps,
		[ (1.0, 0, 0, 0, 1.0, 0) x 10 ],
		"check it");

      @samps = $masked->
	getsamples(x => 76, y => 2, width => 10, type => "float");
      is(@samps, 12, "read line from right edge");
      is_deeply(\@samps, [ (0, 0, 0) x 4], "check result");

      is_deeply([ $masked->getsamples(x => -1, y => 0, width => 1, type => "float") ],
	 [], "fail read left of image");
      is_deeply([ $masked->getsamples(x => 0, y => -1, width => 1, type => "float") ],
	 [], "fail read top of image");
      is_deeply([$masked->getsamples(x => 80, y => 0, width => 1, type => "float")],
	 [], "fail read right of image");
      is_deeply([$masked->getsamples(x => 0, y => 80, width => 1, type => "float")],
	 [], "fail read bottom of image");
    }
  }
  # 86.2%
}

{
  my $base = Imager->new(xsize => 100, ysize => 100, type => "paletted");
  ok($base, "make paletted base");
  is($base->type, "paletted", "check we got paletted");
  is($base->addcolors(colors => [ $black, $red, $green, $blue ]),
     "0 but true",
     "add some colors to base");
  my $masked = $base->masked(mask => $mask, left => 1, top => 2);
  my $limited = $base->masked(left => 1, top => 2);

  is($masked->type, "paletted", "check masked is same type as base");
  is($limited->type, "paletted", "check limited is same type as base");

  {
    # make sure addcolors forwarded
    is($masked->addcolors(colors => [ $grey ]), 4,
       "test addcolors forwarded");
    my @colors = $masked->getcolors();
    is(@colors, 5, "check getcolors forwarded");
    is_color3($colors[1], 255, 0, 0, "check color from palette");
  }

  my ($blacki, $redi, $greeni, $bluei, $greyi) = 0 .. 4;

  { # gpal
    print "# gpal\n";
    $base->box(filled => 1, color => $black);
    is($base->setscanline(x => 0, y => 5, type => "index",
			  pixels => [ ( $redi, $greeni, $bluei, $greyi) x 25 ]),
       100, "write some pixels to base");
    my @indexes = $masked->getscanline(y => 3, type => "index", width => "81");
    is(@indexes, 80, "got 80 indexes");
    is_deeply(\@indexes,
	      [ ( $greeni, $bluei, $greyi, $redi) x 20 ],
	      "check values");

    is_deeply([ $masked->getscanline(x => -1, y => 3, type => "index") ],
	      [], "fail read left of image");
  }
  # 89.8%

  { # ppal, unmasked
    print "# ppal\n";
    $base->box(filled => 1, color => $black);
    is($limited->setscanline(x => 1, y => 1, type => "index",
			     pixels => [ ( $redi, $greeni, $bluei) x 3 ]),
       9, "ppal limited");
    is_deeply([ $base->getscanline(x => 2, y => 3, type => "index", 
				   width => 9) ],
	      [ ( $redi, $greeni, $bluei) x 3 ],
	      "check set in base");
  }
  { # ppal, masked
    $base->box(filled => 1, color => $black);
    is($masked->setscanline(x => 1, y => 2, type => "index",
			    pixels => [ ( $redi, $greeni, $bluei, $greyi) x 12 ]),
       48, "ppal masked");
    is_deeply([ $base->getscanline(x => 0, y => 4, type => "index") ],
	      [ 0, 0,
		( $redi, $greeni, $bluei, $greyi ) x 9,
		$redi, $greeni, $bluei, ( 0 ) x 59 ],
	      "check written");
  }
  {
    # ppal, errors
    is($masked->setscanline(x => -1, y => 0, type => "index",
			    pixels => [ $redi, $bluei ]),
       0, "fail to write ppal");

    is($masked->setscanline(x => 78, y => 0, type => "index",
			   pixels => [ $redi, $bluei, $greeni, $greyi ]),
       2, "write over right side");
  }
}

my $full_mask = Imager->new(xsize => 10, ysize => 10, channels => 1);
$full_mask->box(filled => 1, color => NC(255, 0, 0));

# no mask and mask with full coverage should behave the same
my $psamp_outside_error = "Image position outside of image";
for my $masked (0, 1){ # psamp
  print "# psamp masked: $masked\n";
  my $imback = Imager::ImgRaw::new(20, 20, 3);
  my $mask;
  if ($masked) {
    $mask = $full_mask->{IMG};
  }
  my $imraw = Imager::i_img_masked_new($imback, $mask, 3, 4, 10, 10);
  {
    is(Imager::i_psamp($imraw, 0, 2, undef, [ 255, 128, 64 ]), 3,
       "i_psamp def channels, 3 samples");
    is_color3(Imager::i_get_pixel($imraw, 0, 2), 255, 128, 64,
	      "check color written");
    Imager::i_img_setmask($imraw, 5);
    is(Imager::i_psamp($imraw, 1, 3, undef, [ 64, 128, 192 ]), 3,
       "i_psamp def channels, 3 samples, masked");
    is_color3(Imager::i_get_pixel($imraw, 1, 3), 64, 0, 192,
	      "check color written");
    is(Imager::i_psamp($imraw, 1, 7, [ 0, 1, 2 ], [ 64, 128, 192 ]), 3,
       "i_psamp channels listed, 3 samples, masked");
    is_color3(Imager::i_get_pixel($imraw, 1, 7), 64, 0, 192,
	      "check color written");
    Imager::i_img_setmask($imraw, ~0);
    is(Imager::i_psamp($imraw, 2, 4, [ 0, 1 ], [ 255, 128, 64, 32 ]), 4,
       "i_psamp channels [0, 1], 4 samples");
    is_color3(Imager::i_get_pixel($imraw, 2, 4), 255, 128, 0,
	      "check first color written");
    is_color3(Imager::i_get_pixel($imraw, 3, 4), 64, 32, 0,
	      "check second color written");
    is(Imager::i_psamp($imraw, 0, 5, [ 0, 1, 2 ], [ (128, 63, 32) x 10 ]), 30,
       "write a full row");
    is_deeply([ Imager::i_gsamp($imraw, 0, 10, 5, [ 0, 1, 2 ]) ],
	      [ (128, 63, 32) x 10 ],
	      "check full row");
    is(Imager::i_psamp($imraw, 8, 8, [ 0, 1, 2 ],
		       [ 255, 128, 32, 64, 32, 16, 32, 16, 8 ]),
       6, "i_psamp channels [0, 1, 2], 9 samples, but room for 6");
  }
  { # errors we catch
    is(Imager::i_psamp($imraw, 6, 8, [ 0, 1, 3 ], [ 255, 128, 32 ]),
       undef, "i_psamp channels [0, 1, 3], 3 samples (invalid channel number)");
    is(_get_error(), "No channel 3 in this image",
       "check error message");
    is(Imager::i_psamp($imraw, 6, 8, [ 0, 1, -1 ], [ 255, 128, 32 ]),
       undef, "i_psamp channels [0, 1, -1], 3 samples (invalid channel number)");
    is(_get_error(), "No channel -1 in this image",
       "check error message");
    is(Imager::i_psamp($imraw, 0, -1, undef, [ 0, 0, 0 ]), undef,
       "negative y");
    is(_get_error(), $psamp_outside_error, "check error message");
    is(Imager::i_psamp($imraw, 0, 10, undef, [ 0, 0, 0 ]), undef,
       "y overflow");
    is(_get_error(), $psamp_outside_error, "check error message");
    is(Imager::i_psamp($imraw, -1, 0, undef, [ 0, 0, 0 ]), undef,
       "negative x");
    is(_get_error(), $psamp_outside_error, "check error message");
    is(Imager::i_psamp($imraw, 10, 0, undef, [ 0, 0, 0 ]), undef,
       "x overflow");
    is(_get_error(), $psamp_outside_error, "check error message");
  }
  print "# end psamp tests\n";
}

for my $masked (0, 1) { # psampf
  print "# psampf\n";
  my $imback = Imager::ImgRaw::new(20, 20, 3);
  my $mask;
  if ($masked) {
    $mask = $full_mask->{IMG};
  }
  my $imraw = Imager::i_img_masked_new($imback, $mask, 3, 4, 10, 10);
  {
    is(Imager::i_psampf($imraw, 0, 2, undef, [ 1, 0.5, 0.25 ]), 3,
       "i_psampf def channels, 3 samples");
    is_color3(Imager::i_get_pixel($imraw, 0, 2), 255, 128, 64,
	      "check color written");
    Imager::i_img_setmask($imraw, 5);
    is(Imager::i_psampf($imraw, 1, 3, undef, [ 0.25, 0.5, 0.75 ]), 3,
       "i_psampf def channels, 3 samples, masked");
    is_color3(Imager::i_get_pixel($imraw, 1, 3), 64, 0, 191,
	      "check color written");
    is(Imager::i_psampf($imraw, 1, 7, [ 0, 1, 2 ], [ 0.25, 0.5, 0.75 ]), 3,
       "i_psampf channels listed, 3 samples, masked");
    is_color3(Imager::i_get_pixel($imraw, 1, 7), 64, 0, 191,
	      "check color written");
    Imager::i_img_setmask($imraw, ~0);
    is(Imager::i_psampf($imraw, 2, 4, [ 0, 1 ], [ 1, 0.5, 0.25, 0.125 ]), 4,
       "i_psampf channels [0, 1], 4 samples");
    is_color3(Imager::i_get_pixel($imraw, 2, 4), 255, 128, 0,
	      "check first color written");
    is_color3(Imager::i_get_pixel($imraw, 3, 4), 64, 32, 0,
	      "check second color written");
    is(Imager::i_psampf($imraw, 0, 5, [ 0, 1, 2 ], [ (0.5, 0.25, 0.125) x 10 ]), 30,
       "write a full row");
    is_deeply([ Imager::i_gsamp($imraw, 0, 10, 5, [ 0, 1, 2 ]) ],
	      [ (128, 64, 32) x 10 ],
	      "check full row");
    is(Imager::i_psampf($imraw, 8, 8, [ 0, 1, 2 ],
			[ 1.0, 0.5, 0.125, 0.25, 0.125, 0.0625, 0.125, 0, 1 ]),
       6, "i_psampf channels [0, 1, 2], 9 samples, but room for 6");
  }
  { # errors we catch
    is(Imager::i_psampf($imraw, 6, 8, [ 0, 1, 3 ], [ 1, 0.5, 0.125 ]),
       undef, "i_psampf channels [0, 1, 3], 3 samples (invalid channel number)");
    is(_get_error(), "No channel 3 in this image",
       "check error message");
    is(Imager::i_psampf($imraw, 6, 8, [ 0, 1, -1 ], [ 1, 0.5, 0.125 ]),
       undef, "i_psampf channels [0, 1, -1], 3 samples (invalid channel number)");
    is(_get_error(), "No channel -1 in this image",
       "check error message");
    is(Imager::i_psampf($imraw, 0, -1, undef, [ 0, 0, 0 ]), undef,
       "negative y");
    is(_get_error(), $psamp_outside_error, "check error message");
    is(Imager::i_psampf($imraw, 0, 10, undef, [ 0, 0, 0 ]), undef,
       "y overflow");
    is(_get_error(), $psamp_outside_error, "check error message");
    is(Imager::i_psampf($imraw, -1, 0, undef, [ 0, 0, 0 ]), undef,
       "negative x");
    is(_get_error(), $psamp_outside_error, "check error message");
    is(Imager::i_psampf($imraw, 10, 0, undef, [ 0, 0, 0 ]), undef,
       "x overflow");
    is(_get_error(), $psamp_outside_error, "check error message");
  }
  print "# end psampf tests\n";
}

{
  my $sub_mask = $full_mask->copy;
  $sub_mask->box(filled => 1, color => NC(0,0,0), xmin => 4, xmax => 6);
  my $base = Imager::ImgRaw::new(20, 20, 3);
  my $masked = Imager::i_img_masked_new($base, $sub_mask->{IMG}, 3, 4, 10, 10);

  is(Imager::i_psamp($masked, 0, 2, undef, [ ( 0, 127, 255) x 10 ]), 30,
     "psamp() to masked image");
  is_deeply([ Imager::i_gsamp($base, 0, 20, 6, undef) ],
	    [ ( 0, 0, 0 ) x 3, # left of mask
	      ( 0, 127, 255 ) x 4, # masked area
	      ( 0, 0, 0 ) x 3, # unmasked area
	      ( 0, 127, 255 ) x 3, # masked area
	      ( 0, 0, 0 ) x 7 ], # right of mask
	    "check values written");
  is(Imager::i_psampf($masked, 0, 2, undef, [ ( 0, 0.5, 1.0) x 10 ]), 30,
     "psampf() to masked image");
  is_deeply([ Imager::i_gsamp($base, 0, 20, 6, undef) ],
	    [ ( 0, 0, 0 ) x 3, # left of mask
	      ( 0, 128, 255 ) x 4, # masked area
	      ( 0, 0, 0 ) x 3, # unmasked area
	      ( 0, 128, 255 ) x 3, # masked area
	      ( 0, 0, 0 ) x 7 ], # right of mask
	    "check values written");
}

{
  my $empty = Imager->new;
  ok(!$empty->masked, "fail to make a masked image from an empty");
  is($empty->errstr, "masked: empty input image",
    "check error message");
}

Imager->close_log();

unless ($ENV{IMAGER_KEEP_FILES}) {
  unlink "testout/t020masked.log";
}

sub _get_error {
  my @errors = Imager::i_errors();
  return join(": ", map $_->[0], @errors);
}