File: t10jpeg.t

package info (click to toggle)
libimager-perl 1.027%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 6,820 kB
  • sloc: perl: 32,971; ansic: 28,092; makefile: 52; cpp: 4
file content (742 lines) | stat: -rw-r--r-- 23,889 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
#!perl -w
use strict;
use Imager qw(:all);
use Test::More;
use Imager::Test qw(is_color_close3 test_image_raw test_image is_image);

-d "testout" or mkdir "testout";

init_log("testout/t101jpeg.log",1);

$Imager::formats{"jpeg"}
  or plan skip_all => "no jpeg support";

diag "libjpeg version: ", Imager::File::JPEG->libjpeg_version(), "\n";

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=test_image_raw();
my $cmpimg=Imager::ImgRaw::new(150,150,3);

open(FH,">testout/t101.jpg")
  || die "cannot open testout/t101.jpg for writing\n";
binmode(FH);
my $IO = Imager::io_new_fd(fileno(FH));
ok(Imager::File::JPEG::i_writejpeg_wiol($img,$IO,30), "write jpeg low level");
close(FH);

open(FH, "testout/t101.jpg") || die "cannot open testout/t101.jpg\n";
binmode(FH);
$IO = Imager::io_new_fd(fileno(FH));
($cmpimg,undef) = Imager::File::JPEG::i_readjpeg_wiol($IO);
close(FH);

my $diff = sqrt(i_img_diff($img,$cmpimg))/150*150;
print "# jpeg average mean square pixel difference: ",$diff,"\n";
ok($cmpimg, "read jpeg low level");

ok($diff < 10000, "difference between original and jpeg within bounds");

Imager::i_log_entry("Starting 4\n", 1);
my $imoo = Imager->new;
ok($imoo->read(file=>'testout/t101.jpg'), "read jpeg OO");

ok($imoo->write(file=>'testout/t101_oo.jpg'), "write jpeg OO");
Imager::i_log_entry("Starting 5\n", 1);
my $oocmp = Imager->new;
ok($oocmp->read(file=>'testout/t101_oo.jpg'), "read jpeg OO for comparison");

$diff = sqrt(i_img_diff($imoo->{IMG},$oocmp->{IMG}))/150*150;
print "# OO image difference $diff\n";
ok($diff < 10000, "difference between original and jpeg within bounds");

# write failure test
open FH, "< testout/t101.jpg" or die "Cannot open testout/t101.jpg: $!";
binmode FH;
my $io = Imager::io_new_fd(fileno(FH));
$io->set_buffered(0);
ok(!$imoo->write(io => $io, type=>'jpeg'), 'failure handling');
close FH;
print "# ",$imoo->errstr,"\n";

# check that the i_format tag is set
my @fmt = $imoo->tags(name=>'i_format');
is($fmt[0], 'jpeg', 'i_format tag');

{ # check file limits are checked
  my $limit_file = "testout/t101.jpg";
  ok(Imager->set_file_limits(reset=>1, width=>149), "set width limit 149");
  my $im = Imager->new;
  ok(!$im->read(file=>$limit_file),
     "should fail read due to size limits");
  print "# ",$im->errstr,"\n";
  like($im->errstr, qr/image width/, "check message");
  
  ok(Imager->set_file_limits(reset=>1, height=>149), "set height limit 149");
  ok(!$im->read(file=>$limit_file),
     "should fail read due to size limits");
  print "# ",$im->errstr,"\n";
  like($im->errstr, qr/image height/, "check message");
  
  ok(Imager->set_file_limits(reset=>1, width=>150), "set width limit 150");
  ok($im->read(file=>$limit_file),
     "should succeed - just inside width limit");
  ok(Imager->set_file_limits(reset=>1, height=>150), "set height limit 150");
  ok($im->read(file=>$limit_file),
     "should succeed - just inside height limit");
  
  # 150 x 150 x 3 channel image uses 67500 bytes
  ok(Imager->set_file_limits(reset=>1, bytes=>67499),
     "set bytes limit 67499");
  ok(!$im->read(file=>$limit_file),
       "should fail - too many bytes");
  print "# ",$im->errstr,"\n";
  like($im->errstr, qr/storage size/, "check error message");
  ok(Imager->set_file_limits(reset=>1, bytes=>67500),
     "set bytes limit 67500");
  ok($im->read(file=>$limit_file),
     "should succeed - just inside bytes limit");
  Imager->set_file_limits(reset=>1);
}

SKIP:
{
  # we don't test them all
  my %expected_tags =
    (
     exif_date_time_original => "2005:11:25 00:00:00",
     exif_flash => 0,
     exif_image_description => "Imager Development Notes",
     exif_make => "Canon",
     exif_model => "CanoScan LiDE 35",
     exif_resolution_unit => 2,
     exif_resolution_unit_name => "inches",
     exif_user_comment => "        Part of notes from reworking i_arc() and friends.",
     exif_white_balance => 0,
     exif_white_balance_name => "Auto white balance",
    );
  
  my $im = Imager->new;
  $im->read(file=>"testimg/exiftest.jpg")
    or skip("Could not read test image:".$im->errstr, scalar keys %expected_tags);
  
  for my $key (keys %expected_tags) {
    is($expected_tags{$key}, $im->tags(name => $key),
       "test value of exif tag $key");
  }
}

{
  # tests that the density values are set and read correctly
  # tests jpeg_comment too
  my @density_tests =
    (
     [ 't101cm100.jpg', 
       { 
	jpeg_density_unit => 2, 
	i_xres => 254, 
	i_yres => 254
       },
       { 
	jpeg_density_unit => 2, 
	i_xres => 254, 
	i_yres => 254,
	i_aspect_only => undef,
       },
     ],
     [
      't101xonly.jpg',
      {
       i_xres => 100,
      },
      {
       i_xres => 100,
       i_yres => 100,
       jpeg_density_unit => 1,
       i_aspect_only => undef,
      },
     ],
     [
      't101yonly.jpg',
      {
       i_yres => 100,
      },
      {
       i_xres => 100,
       i_yres => 100,
       jpeg_density_unit => 1,
       i_aspect_only => undef,
      },
     ],
     [
      't101asponly.jpg',
      {
       i_xres => 50,
       i_yres => 100,
       i_aspect_only => 1,
      },
      {
       i_xres => 50,
       i_yres => 100,
       i_aspect_only => 1,
       jpeg_density_unit => 0,
      },
     ],
     [
      't101com.jpg',
      {
       jpeg_comment => 'test comment'
      },
     ],
    );
  
  print "# test density tags\n";
  # I don't care about the content
  my $base_im = Imager->new(xsize => 10, ysize => 10);
  for my $test (@density_tests) {
    my ($filename, $out_tags, $expect_tags) = @$test;
    $expect_tags ||= $out_tags;
    
    my $work = $base_im->copy;
    for my $key (keys %$out_tags) {
      $work->addtag(name => $key, value => $out_tags->{$key});
    }
    
    ok($work->write(file=>"testout/$filename", type=>'jpeg'),
       "save $filename");
    
    my $check = Imager->new;
    ok($check->read(file=> "testout/$filename"),
       "read $filename");
    
    my %tags;
    for my $key (keys %$expect_tags) {
      $tags{$key} = $check->tags(name=>$key);
    }
    is_deeply($expect_tags, \%tags, "check tags for $filename");
  }
}

{ # Issue # 17981
  # the test image has a zero-length user_comment field
  # the code would originally attempt to convert '\0' to ' '
  # for the first 8 bytes, even if the string was less than 
  # 8 bytes long
  my $im = Imager->new;
  ok($im->read(file => 'testimg/209_yonge.jpg', type=>'jpeg'),
     "test read of image with invalid exif_user_comment");
  is($im->tags(name=>'exif_user_comment'), '',
     "check exif_user_comment set correctly");
}

{ # test parseiptc handling no IPTC data correctly
  my $saw_warn;
  local $SIG{__WARN__} = 
    sub {
      ++$saw_warn;
      print "# @_\n";
    };
  my $im = Imager->new;
  ok($im->read(file => 'testout/t101.jpg', type=>'jpeg'),
     "read jpeg with no IPTC data");
  ok(!defined $im->{IPTCRAW}, "no iptc data");
  my %iptc = $im->parseiptc;
  ok(!$saw_warn, "should be no warnings");
}

{ # Issue # 18397
  # attempting to write a 4 channel image to a bufchain would
  # cause a seg fault.
  # it should fail still
  # overridden by # 29876
  # give 4/2 channel images a background color when saving to JPEG
  my $im = Imager->new(xsize => 16, ysize => 16, channels => 4);
  $im->box(filled => 1, xmin => 8, color => '#FFE0C0');
  my $data;
  ok($im->write(data => \$data, type => 'jpeg'),
     "should write with a black background");
  my $imread = Imager->new;
  ok($imread->read(data => $data, type => 'jpeg'), 'read it back');
  is_color_close3($imread->getpixel('x' => 0, 'y' => 0), 0, 0, 0, 4,
		  "check it's black");
  is_color_close3($imread->getpixel('x' => 15, 'y' => 9), 255, 224, 192, 4,
		  "check filled area filled");
  
  # write with a red background
  $data = '';
  ok($im->write(data => \$data, type => 'jpeg', i_background => '#FF0000'),
     "write with red background");
  ok($imread->read(data => $data, type => 'jpeg'), "read it back");
  is_color_close3($imread->getpixel('x' => 0, 'y' => 0), 255, 0, 0, 4,
		  "check it's red");
  is_color_close3($imread->getpixel('x' => 15, 'y' => 9), 255, 224, 192, 4,
		  "check filled area filled");
}
SKIP:
{ # Issue # 18496
  # If a jpeg with EXIF data containing an (invalid) IFD entry with a 
  # type of zero is read then Imager crashes with a Floating point 
  # exception
  # testimg/zerojpeg.jpg was manually modified from exiftest.jpg to
  # reproduce the problem.
  my $im = Imager->new;
  ok($im->read(file=>'testimg/zerotype.jpg'), "shouldn't crash");
}

SKIP:
{ # code coverage - make sure wiol_skip_input_data is called
  open BASEDATA, "< testimg/exiftest.jpg"
    or skip "can't open base data", 1;
  binmode BASEDATA;
  my $data = do { local $/; <BASEDATA> };
  close BASEDATA;
  
  substr($data, 3, 1) eq "\xE1"
    or skip "base data isn't as expected", 1;
  # inserting a lot of marker data here means we take the branch in 
  # wiol_skip_input_data that refills the buffer
  my $marker = "\xFF\xE9"; # APP9 marker
  $marker .= pack("n", 8192) . "x" x 8190;
  $marker x= 10; # make it take up a lot of space
  substr($data, 2, 0) = $marker;
  my $im = Imager->new;
  ok($im->read(data => $data), "read with a skip of data");
}

SKIP:
{ # code coverage - take the branch that provides a fake EOI
  open BASEDATA, "< testimg/exiftest.jpg"
    or skip "can't open base data", 1;
  binmode BASEDATA;
  my $data = do { local $/; <BASEDATA> };
  close BASEDATA;
  substr($data, -1000) = '';
  
  my $im = Imager->new;
  ok($im->read(data => $data), "read with image data truncated");
}

{ # code coverage - make sure wiol_empty_output_buffer is called
  my $im = Imager->new(xsize => 1000, ysize => 1000);
  for my $x (0 .. 999) {
    $im->line(x1 => $x, y1 => 0, x2 => $x, y2 => 999,
	      color => Imager::Color->new(rand 256, rand 256, rand 256));
  }
  my $data;
  ok($im->write(data => \$data, type=>'jpeg', jpegquality => 100), 
     "write big file to ensure wiol_empty_output_buffer is called")
    or print "# ", $im->errstr, "\n";
  
  # code coverage - write failure path in wiol_empty_output_buffer
  ok(!$im->write(callback => sub { return },
		 type => 'jpeg', jpegquality => 100),
     "fail to write")
    and print "# ", $im->errstr, "\n";
}

{ # code coverage - virtual image branch in i_writejpeg_wiol()
  my $im = $imoo->copy;
  my $immask = $im->masked;
  ok($immask, "made a virtual image (via masked)");
  ok($immask->virtual, "check it's virtual");
  my $mask_data;
  ok($immask->write(data => \$mask_data, type => 'jpeg'),
     "write masked version");
  my $base_data;
  ok($im->write(data => \$base_data, type=>'jpeg'),
     "write normal version");
  is($base_data, $mask_data, "check the data written matches");
}

SKIP:
{ # code coverage - IPTC data
  # this is dummy data
  my $iptc = "\x04\x04" .
    "\034\002x   My Caption"
      . "\034\002P   Tony Cook"
	. "\034\002i   Dummy Headline!"
	  . "\034\002n   No Credit Given";
  
  my $app13 = "\xFF\xED" . pack("n", 2 + length $iptc) . $iptc;
  
  open BASEDATA, "< testimg/exiftest.jpg"
    or skip "can't open base data", 1;
  binmode BASEDATA;
  my $data = do { local $/; <BASEDATA> };
  close BASEDATA;
  substr($data, 2, 0) = $app13;
  
  my $im = Imager->new;
  ok($im->read(data => $data), "read with app13 data");
  my %iptc = $im->parseiptc;
  is($iptc{caption}, 'My Caption', 'check iptc caption');
  is($iptc{photogr}, 'Tony Cook', 'check iptc photogr');
  is($iptc{headln}, 'Dummy Headline!', 'check iptc headln');
  is($iptc{credit}, 'No Credit Given', 'check iptc credit');
}

{ # handling of CMYK jpeg
  # http://rt.cpan.org/Ticket/Display.html?id=20416
  my $im = Imager->new;
  ok($im->read(file => 'testimg/scmyk.jpg'), 'read a CMYK jpeg');
  is($im->getchannels, 3, "check channel count");
  my $col = $im->getpixel(x => 0, 'y' => 0);
  ok($col, "got the 'black' pixel");
  # this is jpeg, so we can't compare colors exactly
  # older versions returned this pixel at a light color, but
  # it's black in the image
  my ($r, $g, $b) = $col->rgba;
  cmp_ok($r, '<', 10, 'black - red low');
  cmp_ok($g, '<', 10, 'black - green low');
  cmp_ok($b, '<', 10, 'black - blue low');
  $col = $im->getpixel(x => 15, 'y' => 0);
  ok($col, "got the dark blue");
  ($r, $g, $b) = $col->rgba;
  cmp_ok($r, '<', 10, 'dark blue - red low');
  cmp_ok($g, '<', 10, 'dark blue - green low');
  cmp_ok($b, '>', 110, 'dark blue - blue middle (bottom)');
  cmp_ok($b, '<', 130, 'dark blue - blue middle (top)');
  $col = $im->getpixel(x => 0, 'y' => 15);
  ok($col, "got the red");
  ($r, $g, $b) = $col->rgba;
  cmp_ok($r, '>', 245, 'red - red high');
  cmp_ok($g, '<', 10, 'red - green low');
  cmp_ok($b, '<', 10, 'red - blue low');
}

{
  ok(grep($_ eq 'jpeg', Imager->read_types), "check jpeg in read types");
  ok(grep($_ eq 'jpeg', Imager->write_types), "check jpeg in write types");
}

{ # progressive JPEG
  # https://rt.cpan.org/Ticket/Display.html?id=68691
  my $im = test_image();
  my $progim = $im->copy;

  ok($progim->write(file => "testout/t10prog.jpg", type => "jpeg",
		    jpeg_progressive => 1),
     "write progressive jpeg");

  my $rdprog = Imager->new(file => "testout/t10prog.jpg");
  ok($rdprog, "read progressive jpeg");
  my @prog = $rdprog->tags(name => "jpeg_progressive");
  is($prog[0], 1, "check progressive flag set on read");

  my $data;
  ok($im->write(data => \$data, type => "jpeg"), 
     "save as non-progressive to compare");
  my $norm = Imager->new(data => $data);
  ok($norm, "read non-progressive file");
  my @nonprog = $norm->tags(name => "jpeg_progressive");
  is($nonprog[0], 0, "check progressive flag 0 for non prog file");

  is_image($rdprog, $norm, "prog vs norm should be the same image");
}

SKIP:
{ # optimize coding
  my $im = test_image();
  my $base;
  ok($im->write(data => \$base, type => "jpeg"), "save without optimize");
  my $opt;
  ok($im->write(data => \$opt, type => "jpeg", jpeg_optimize => 1),
     "save with optimize");
  cmp_ok(length $opt, '<', length $base, "check optimized is smaller");
  my $im_base = Imager->new(data => $base, filetype => "jpeg");
  ok($im_base, "read unoptimized back");
  my $im_opt = Imager->new(data => $opt, filetype => "jpeg");
  ok($im_opt, "read optimized back");
  $im_base && $im_opt
    or skip "couldn't read one back", 1;
  is_image($im_opt, $im_base,
	   "optimization should only change huffman compression, not quality");
}

SKIP:
{
  my $im = test_image();
  # unknown jpeg_compress_profile always fails, doesn't matter whether we have mozjpeg
  my $data;
  ok(!$im->write(data => \$data, type => "jpeg", jpeg_compress_profile => "invalid"),
     "fail to write with unknown compression profile");
  like($im->errstr, qr/jpeg_compress_profile=invalid unknown/,
       "check the error message");

  ok($im->write(data => \$data, type => "jpeg", jpeg_compress_profile => "fastest"),
     "jpeg_compress_profile=fastest is always available");
  Imager::File::JPEG->is_mozjpeg
      and skip "this is mozjpeg", 1;
  ok(!$im->write(data => \$data, type => "jpeg", jpeg_compress_profile => "max"),
     "fail to write with max compression profile without mozjpeg");
  like($im->errstr, qr/jpeg_compress_profile=max requires mozjpeg/,
       "check the error message");
}

SKIP:
{
  Imager::File::JPEG->is_mozjpeg
      or skip "this isn't mozjpeg", 1;
  my $im = test_image();
  my $base_data;
  ok($im->write(data => \$base_data, type => "jpeg", jpeg_compress_profile => "fastest"),
     "write using defaults");
  my $opt_data;
  ok($im->write(data => \$opt_data,  type => "jpeg", jpeg_compress_profile => "max"),
     "write using max compression profile");
  cmp_ok(length($opt_data), '<', length($base_data),
         "max compression should be smaller");
}

SKIP:
{
  Imager::File::JPEG->has_arith_coding
      or skip "arithmetic coding not available", 1;
  my $im = test_image;
  my $data;
  ok($im->write(data => \$data, type => "jpeg", jpeg_arithmetic => 1),
     "write with arithmetic coding");
  my $im2 = Imager->new(data => $data);
  ok($im2, "read back arithmetic coded");
  ok($im2->tags(name => "jpeg_read_arithmetic"),
     "and read tag set");
}

{
  # default RGB (YCbCr) image has JFIF header
  my $im = test_image;
  my $data;
  ok($im->write(data => \$data, type => "jpeg"),
     "write default RGB");
  my $im2 = Imager->new;
  ok($im2->read(data => $data), "read it back");
  is($im2->tags(name => "jpeg_read_jfif"), 1, "JFIF detected");
  ok($im->write(data => \$data, type => "jpeg", jpeg_jfif => 0),
     "disable the JFIF header");
  ok($im2->read(data => $data), "read back file without JFIF");
  is($im2->tags(name => "jpeg_read_jfif"), 0, "no JFIF detected");
}

{
  my $im = Imager->new(xsize => 150, ysize => 150);
  $im->box(filled => 1, color => "#F00", box => [ 15, 15, 84, 84 ]);
  $im->box(filled => 1, color => "#0F0", box => [ 30, 30, 69, 69 ]);
  # guesstimate
  my $smoothed = $im->copy->filter(type => 'gaussian', stddev => 1);
  my $data_base;
  ok($im->write(data => \$data_base, type => "jpeg"), "write base image (control)");
  my $data_smoothed;
  ok($im->write(data => \$data_smoothed, type => "jpeg", jpeg_smooth => 50),
     "write smoothed image");
  my $smim = Imager->new;
  ok($smim->read(data => $data_smoothed), "read smoothed back");
  my $diffWvsS = Imager::i_img_diff($smim->{IMG}, $smoothed->{IMG});
  my $diffWvsB = Imager::i_img_diff($smim->{IMG}, $im->{IMG});
  cmp_ok($diffWvsS, '<', $diffWvsB,
	 "written image should be closer to blurred image than base image");

  my $data;
  ok(!$im->write(data => \$data, type => "jpeg", jpeg_smooth => 101),
     "invalid smoothing value");
  like($im->errstr, qr/jpeg_smooth must be an integer from 0 to 100/, "check error message");
}

{
  # jpeg_restart
  my $im = test_image;
  my $data;
  ok($im->write(data => \$data,        type => "jpeg"),
     "write with default restarts");
  my $data_8_rows;
  ok($im->write(data => \$data_8_rows, type => "jpeg", jpeg_restart => 8),
     "write with restart every 8 rows");
  my $data_8_mcus;
  ok($im->write(data => \$data_8_mcus, type => "jpeg", jpeg_restart => "8b"),
     "write with restart every 8 mcus");
  cmp_ok(length($data), '<', length($data_8_rows),
         "no restarts smaller than restart every 8 rows");
  cmp_ok(length($data_8_rows), '<', length($data_8_mcus),
         "restarts every 8 rows smaller than restart every 8 mcus");

  # error handling
  ok(!$im->write(data => \$data, type => "jpeg", jpeg_restart => "8c"),
     "bad units value");
  like($im->errstr, qr/jpeg_restart must be an integer from 0 to 65535 followed by an optional b/,
       "check error message");
  $im->_set_error("");
  ok(!$im->write(data => \$data, type => "jpeg", jpeg_restart => "65536"),
     "out of range value");
  like($im->errstr, qr/jpeg_restart must be an integer from 0 to 65535 followed by an optional b/,
       "check error message");
}

SKIP:
{
  # jpeg_sample
  my $im = test_image;
  my $data_base;
  ok($im->write(data => \$data_base, type => "jpeg"),
     "write default");
  my $data_1x1;
  ok($im->write(data => \$data_1x1,  type => "jpeg", jpeg_sample => "1x1"),
     "write with 1x1")
    or diag $im->errstr;
  cmp_ok(length($data_base), '<', length($data_1x1),
         "1x1 sampled file is larger");
  my $baseim = Imager->new(data => $data_base)
    or skip "couldn't read back base image", 1;
  my $im1x1 = Imager->new(data => $data_1x1)
    or skip "couldn't read back 1x1 image", 1;
  my $diff_base = Imager::i_img_diff($im->{IMG}, $baseim->{IMG});
  my $diff_1x1 = Imager::i_img_diff($im->{IMG}, $im1x1->{IMG});
  cmp_ok($diff_1x1, '<', $diff_base,
         "1x1 sampled image should be closer to source image");

  # varieties
  my $data_deftag;
  ok($im->write(data => \$data_deftag, type => "jpeg", jpeg_sample => "2x2,1x1,1x1"),
     "specify default by tag");
  is(length($data_base), length($data_deftag),
     "default set by tag matches default");

  my $data;
  my $errq = quotemeta "jpeg_sample: must match /^[1-4]x[1-4](,[1-4]x[1-4]){0,9}\$/aai";
  my $errqr = qr/$errq/;
  # failures
  ok(!$im->write(data => \$data, type => "jpeg", jpeg_sample => "5x1"),
     "H out of range");
  like($im->errstr, $errqr, "check error message");
  $im->_set_error("");
  ok(!$im->write(data => \$data, type => "jpeg", jpeg_sample => "1x5"),
     "V out of range");
  like($im->errstr, $errqr, "check error message");
  $im->_set_error("");
  ok(!$im->write(data => \$data, type => "jpeg", jpeg_sample => "1x5."),
     "non-comma/eof after V");
  like($im->errstr, $errqr, "check error message");
  $im->_set_error("");
  ok(!$im->write(data => \$data, type => "jpeg", jpeg_sample => "1y1"),
     "non X between H and V");
  like($im->errstr, $errqr, "check error message");
  $im->_set_error("");
  ok(!$im->write(data => \$data, type => "jpeg", jpeg_sample => "1x1,"),
     "orphan comma at end");
  like($im->errstr, $errqr, "check error message");
  $im->_set_error("");
}

my @bool_tags =
  qw(jpeg_optimize_scans jpeg_trellis_quant jpeg_trellis_quant_dc
     jpeg_tresllis_eob_opt jpeg_use_lambda_weight_tbl jpeg_use_scans_in_trellis
     jpeg_overshoot_deringing);

my @float_tags =
  qw(jpeg_lambda_log_scale1 jpeg_lambda_log_scale2 jpeg_trellis_delta_dc_weight);

my @int_tags =
  qw(jpeg_trellis_freq_split jpeg_trellis_num_loops jpeg_base_quant_tbl_idx
     jpeg_dc_scan_opt_mode);
SKIP:
{
  Imager::File::JPEG->is_mozjpeg
      or skip "These tags are mozjpeg only", 1;

  # boolean tags
  for my $tag (@bool_tags) {
    my $im = test_image();
    my $data;
    ok($im->write(data => \$data, type => "jpeg", $tag => 0),
       "write with boolean tag $tag");
  }
  # float tags
  for my $tag (@float_tags) {
    my $im = test_image();
    my $data;
    ok($im->write(data => \$data, type => "jpeg", $tag => 8.1),
       "write with float tag $tag");
  }
  # int tags
  for my $tag (@int_tags) {
    my $im = test_image();
    my $data;
    ok($im->write(data => \$data, type => "jpeg", $tag => 2),
       "write with int tag $tag");
  }
}

SKIP:
{
  Imager::File::JPEG->is_mozjpeg
      and skip "Testing absence of mozjpeg", 1;

  # none of these tags should be settable
  # boolean tags
  for my $tag (@bool_tags) {
    my $im = test_image();
    my $data;
    ok(!$im->write(data => \$data, type => "jpeg", $tag => 0),
       "fail write with boolean tag $tag");
  }
  # float tags
  for my $tag (@float_tags) {
    my $im = test_image();
    my $data;
    ok(!$im->write(data => \$data, type => "jpeg", $tag => 8.1),
       "fail write with float tag $tag");
  }
  # int tags
  for my $tag (@int_tags) {
    my $im = test_image();
    my $data;
    ok(!$im->write(data => \$data, type => "jpeg", $tag => 2),
       "fail write with int tag $tag");
  }
}

SKIP:
{
  Imager::File::JPEG->is_mozjpeg
      or skip "jpeg_tune needs mozjpeg", 1;
  for my $tune (qw(psnr ssim ms-ssim hvs-psnr)) {
    my $data;
    my $im = test_image;
    ok($im->write(data => \$data, type => "jpeg", jpeg_tune => $tune),
       "jpeg_tune=$tune");
    note "size ".length $data;
  }
  {
    my $data;
    my $im = test_image;
    ok(!$im->write(data => \$data, type => "jpeg", jpeg_tune => "rubbish"),
       "fail jpeg_tune=rubbish");
  }
}

SKIP:
{
  Imager::File::JPEG->is_mozjpeg
      and skip "testing absence of mozjpeg", 1;
  for my $tune (qw(psnr ssim ms-ssim hvs-psnr)) {
    my $data;
    my $im = test_image;
    ok(!$im->write(data => \$data, type => "jpeg", jpeg_tune => $tune),
       "fail jpeg_tune=$tune");
  }
}

{ # check close failures are handled correctly
  my $im = test_image();
  my $fail_close = sub {
    Imager::i_push_error(0, "synthetic close failure");
    return 0;
  };
  ok(!$im->write(type => "jpeg", callback => sub { 1 },
		 closecb => $fail_close),
     "check failing close fails");
    like($im->errstr, qr/synthetic close failure/,
	 "check error message");
}

done_testing();