--- chunky-v0.3.pl	2012-09-20 20:14:33.363000000 +0200
+++ chunky.pl	2012-09-21 00:02:27.654000000 +0200
@@ -40,14 +40,16 @@
 ### NO NEED TO CHANGE ANYTHING BELOW THIS LINE ###
 ##################################################
 
-# FIXME: handle all public chunk types.
-#        Might get inspired by https://github.com/jsummers/tweakpng/blob/133f27b452c04490a65adf66458e2c5595003536/chunk.cpp ,
-#        Chunk::describe_* methods starting at line 908.
-# FIXME: handle more private chunk types.
-# FIXME: find out about more private chunk types.
-# FIXME: complain about more errors, e.g. color depth etc. not defined by the spec
-# FIXME: test, test, test. Many of the rarer chunks, espc. PNGEXT and
-#        private, have not been tested at all.
+# FIXME: chunky will most likely break on EBCDIC.
+
+# TODO: handle all public chunk types.
+#       Might get inspired by https://github.com/jsummers/tweakpng/blob/133f27b452c04490a65adf66458e2c5595003536/chunk.cpp ,
+#       Chunk::describe_* methods starting at line 908.
+# TODO: handle more private chunk types.
+# TODO: find out about more private chunk types.
+# TODO: complain about more errors, e.g. color depth etc. not defined by the spec
+# TODO: test, test, test. Many of the rarer chunks, espc. PNGEXT and
+#       private, have not been tested at all.
 
 # IDEA: handle MNG and JNG files?
 # IDEA: support editing? Should this be surgical modification of an existing
@@ -61,39 +63,52 @@
 use Compress::Zlib;	# for deflate support
 no strict 'refs';	# allow indirect calls of chunk handlers
 
-our $version = "0.3";
+our $version = "0.4";
+
+#########################################
+### DEFAULTS FOR COMMAND-LINE OPTIONS ###
+#########################################
+my $help_requested	= 0;	# --help
+my $version_requested	= 0;	# --version
+my $license_requested	= 0;	# --license
+my $ignore_private	= 0;	# --ignore-private
+my $ignore_unknown	= 0;	# --ignore-unknown
+my %ignore_chunks	= ();	# --ignore-chunks
+my %only_chunks		= ();	# --only-chunks
+my $only_chunks		= 0;
+my %collapse_chunks	= ();	# --collapse-chunks
+my %collapse_default	= (	
+  "IDAT"	=> 1,
+  "fdAT"	=> 1,
+  "mkBT"	=> 1,
+);
+my $collapse_chunks	= 0;
+our $decimal_places	= 1;	# --round
 
 ########################
 ### GLOBAL VARIABLES ###
 ########################
 
-# misc.
-our %seen 	= ();	# seen chunk types
-our $seen_any 	= 0;	# seen any chunk?
-our $lastchunk 	= "";	# previous chunk
-our $idat_len	= 0;	# for IDAT chunk collapsing
-our $idat_count	= 0;	# dto.
-
 # image information that needs to be global
 our $width	= undef;	# needed for sTER
 our $bitdepth	= undef;	# needed for sBIT
 our $colortype	= undef;	# needed for bKGD
 our @palette	= ();		# needed for bKGD etc.
 
+# misc.
+our %seen 		= ();	# seen chunk types
+our $seen_any 		= 0;	# seen any chunk?
+our $lastchunk 		= "";	# previous chunk
+our $nextchunk		= "";	# next (expected) chunk
+
 #######################
 ### LOCAL VARIABLES ###
 #######################
 
-# defaults for command-line options
-my $collapse_idat	= 1;	# --collapse-idat
-my $help_requested	= 0;	# --help
-my $version_requested	= 0;	# --version
-my $license_requested	= 0;	# --license
-my $ignore_private	= 0;	# --ignore-private
-my $ignore_unknown	= 0;	# --ignore-unknown
-my %ignore_chunks	= ();	# --ignore-chunks
-my %only_chunks		= ();	# --only-chunks
-my $only_chunks		= 0;	# dto.
+# chunk collapsing
+my $collapsed_len	= 0;	# amount of chunk data collapsed
+my $collapsed_count	= 0;	# number of chunks collapsed
+my $collapsing		= "";	# chunk type currently being collapsed
 
 ######################################
 ### KVETCHING AND USER INTERACTION ###
@@ -120,19 +135,23 @@
 
 Usage: chunky.pl [OPTION]... FILE
 
-Display control options:
+Chunk display control options:
+
+    --collapse-chunks=<...>   Collapse specified chunk(s) (default: IDAT, fdAT, mkBT)
+    --ignore-chunks=<...>     Ignore the specified chunk(s)
+    --ignore-private          Ignore private chunks
+    --ignore-unknown          Ignore unknown chunks
+    --only-chunks=<...>       Only report the specified chunk(s)
+    
+Other display control options:
 
-    --ignore-chunks=<...>   Don't report the specified chunk(s)
-    --ignore-private        Ignore private chunks
-    --ignore-unknown        Ignore unknown chunks
-    --no-collapse-idat      Don't collapse multiple consecutive IDAT chunks
-    --only-chunks=<...>     Only report the specified chunk(s)
+    --round=#                 Round to # decimal places (gAMA, pHYs; default: 1)
 
 Miscellaneous options:
 
-    --help                  Print this help screen
-    --license               Print license text
-    --version               Print version
+    --help                    Print this help screen
+    --license                 Print license text
+    --version                 Print version
 
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
@@ -924,8 +943,20 @@
 sub handle_dSIG($) {
   my ($length) = @_;
 
-  # FIXME: constraints on dSIG chunk ordering; must appear in pairs
-  # FIXME: also must appear immediately after IHDR and before IEND
+  given($seen{'dSIG'}) {
+    when (0) {
+      kvetch "first dSIG chunk must follow IHDR" unless $lastchunk eq "IHDR";
+    }
+    
+    when (1) {
+      $nextchunk = "IEND";
+    }
+    
+    default {
+      kvetch("too many dSIG chunks");
+    }
+  }
+
   seek PNG, $length, SEEK_CUR;
   
   kvetch("not implemented!")
@@ -953,7 +984,7 @@
   read PNG, my $chunkdata, $length;
   my $gamma = unpack("N", $chunkdata) / 100_000;
       
-  say "  Gamma: $gamma (~1/" . sprintf("%.1f", 1 / $gamma) . ")";
+  say "  Gamma: $gamma (~1/" . sprintf("%.${decimal_places}f", 1 / $gamma) . ")";
 }
 
 # PNGEXT 1.2.0: GIF graphic control extension
@@ -1026,7 +1057,7 @@
   my $entries = int($length / 2);
 
   say "  " . my $entries . " entries";
-  kvetch "number of entries ($entries) does not match number of palette entries" . scalar @palette if($entries != scalar @palette);
+  kvetch "number of entries ($entries) does not match number of palette entries (" . scalar @palette . ")" if($entries != scalar @palette);
 
 }
     
@@ -1051,9 +1082,9 @@
   kvetch "multiple IDAT chunks must be consecutive" if($seen{'IDAT'} && ($lastchunk ne "IDAT"));
   seek PNG, $length, SEEK_CUR;
 
-#  kvetch("not implemented!")
+  # kvetch("not implemented!")
   # FIXME: implement?
-  # Note that this only gets called in the first place if IDAT chunk collapsing is disabled!
+  # Note that this does not get called at all if IDAT chunks are collapsed (the default).
 }
     
 # PNG 1.2: image trailer
@@ -1076,7 +1107,7 @@
     
   read PNG, my $chunkdata, $length;
   my ($width, $height, $bitdepth, $colortype, $compressionmethod, $filtermethod, $interlacemethod) = unpack("NNCCCCC", $chunkdata);
-  $::colortype	= $colortype; # there's GOT to be a better way of doing this.
+  $::colortype	= $colortype; # FIXME: there's GOT to be a better way of doing this.
   $::bitdepth	= $bitdepth;
   $::width	= $width;
 
@@ -1119,7 +1150,7 @@
 
   read PNG, my $chunkdata, $length;
   
-  my ($keyword, $compressionflag, $compressionmethod, $languagetag, $translkeyword, $text) = unpack("Z*CCZ*Z*Z*", $chunkdata);
+  my ($keyword, $compressionflag, $compressionmethod, $languagetag, $translkeyword, $text) = unpack("Z*CCZ*Z*A*", $chunkdata);
   
   print "  Compression: ";
   given($compressionflag) {
@@ -1129,9 +1160,12 @@
       print "UNRECOGNIZED ($compressionflag), method $compressionmethod";
     }
   }
+
+  say ", language: ", $languagetag || "none specified";
   
-  say ", language: '$languagetag'";
-  say "  '$keyword ($translkeyword): " . decompress($compressionflag, $compressionmethod, $text) . "'";
+  print "  '$keyword";
+  print " ($translkeyword)" if($translkeyword ne "");
+  say ": " . decompress($compressionflag, $compressionmethod, $text) . "'";
   
 }
     
@@ -1171,7 +1205,7 @@
       
   seek PNG, $length, SEEK_CUR;
       
-#  kvetch("not implemented!")
+  kvetch("not implemented!")
   # FIXME: implement
 }
     
@@ -1189,8 +1223,9 @@
   if($unit == 0) {
     say "  Pixel aspect ratio (X:Y): $pixelsperunit_x:$pixelsperunit_y";
   } elsif($unit == 1) {
-    say "  Pixels per meter^2 (XxY): ${pixelsperunit_x}x${pixelsperunit_y} (" . $pixelsperunit_x / 100 * 2.54 . "x" . $pixelsperunit_y / 100 * 2.54 . " dpi)";
+    say "  Pixels per meter^2 (XxY): ${pixelsperunit_x}x${pixelsperunit_y} (" . sprintf("%.${decimal_places}f", $pixelsperunit_x / 100 * 2.54) . "x" . sprintf("%.${decimal_places}f", $pixelsperunit_y / 100 * 2.54) . " dpi)";
   } else {
+    say "  Pixels: ${pixelsperunit_x}x${pixelsperunit_y}";
     kvetch "Unknown unit: $unit";
   }
 }
@@ -1203,7 +1238,8 @@
   kvetch "multiple PLTE chunks not allowed" if $seen{'PLTE'};
   kvetch "tRNS chunk already seen before PLTE chunk" if $seen{'tRNS'};
 
-  say (($length % 3) ? "  !!! Corrupt PLTE chunk data" : ("  " . ($length / 3) . " colors in palette"));
+  say "  " . int($length / 3) . " color" . (($length < 6) ? "" : "s") . " in palette";
+  kvetch "trailing palette data (" . ($length % 3) . " bytes)" if($length % 3);
   
   read PNG, my $chunkdata, $length;
   @_ = unpack("(CCC)*", $chunkdata); # @_ for easier shifting
@@ -1274,10 +1310,18 @@
   kvetch "sCAL chunk must precede IDAT chunks" if $seen{'IDAT'};
   kvetch "multiple sCAL chunks not allowed" if $seen{'sCAL'};
       
-  seek PNG, $length, SEEK_CUR;
-      
-#  kvetch("not implemented!")
-  # FIXME: implement
+  read PNG, my $chunkdata, $length;
+  my ($unit, $pixelwidth, $pixelheight) = unpack("CZ*A*", $chunkdata);
+  
+  print "  Physical size of each pixel: ${pixelwidth}x{$pixelheight}";
+  given ($unit) {
+    say " meters" when 1;
+    say " radians" when 2;
+    default {
+      say ", UNKNOWN UNIT: $unit";
+    }
+  }
+
 }
     
 # PNG 1.2: suggested palette
@@ -1304,7 +1348,7 @@
     
     default {
       say;
-      kvetch("unrecognized sample depth");
+      kvetch("unrecognized sample depth: $sampledepth");
     }
   }
     
@@ -1352,8 +1396,7 @@
     say "cross-fuse layout"	when 0;
     say "diverging-fuse layout"	when 1;
     default {
-      say;
-      kvetch "unrecognized stereo mode: $stereomode";
+      say "UNRECOGNIZED: $stereomode";
     }
   }
   
@@ -1369,6 +1412,7 @@
   read PNG, my $chunkdata, $length;
   my ($keyword, $textstring) = unpack("Z*A*", $chunkdata);
   say "  '$keyword: $textstring'";
+  
 }
     
 # PNG 1.2: image last-modification time
@@ -1405,7 +1449,8 @@
       say "  Transparent color: ($red, $blue, $green)";
     }
     when (3) {
-      # FIXME: ...      
+      say "  $length transparency entries";
+      # FIXME: store data for possible later use, akin to PLTE?
     }
   }
 }
@@ -1416,7 +1461,7 @@
 
   read PNG, my $chunkdata, $length;
   
-  my ($keyword, $compressionmethod, $text) = unpack("Z*CZ*", $chunkdata);
+  my ($keyword, $compressionmethod, $text) = unpack("Z*CA*", $chunkdata);
   
   print "  Compression: ";
   given($compressionmethod) {
@@ -1443,8 +1488,11 @@
 # ZDoom: dataless chunk, indicates grayscale image should be treated as standalone alpha channel
 sub handle_alPh($) {
   my ($length) = @_;
+  kvetch_length($length, 0);
   
   kvetch "alPh chunk must precede IDAT chunks" if $seen{'IDAT'};
+  
+  say "  Grayscale image should be treated as standalone alpha channel";  
   seek PNG, $length, SEEK_CUR;
 }
 
@@ -1464,6 +1512,7 @@
   my ($length) = @_;
 
   seek PNG, $length, SEEK_CUR;
+  # Note that this does not get called at all if IDAT chunks are collapsed (the default).
 }
     
 # ZDoom: center position of image
@@ -1497,6 +1546,7 @@
   my ($length) = @_;
 
   seek PNG, $length, SEEK_CUR;
+  # Note that this does not get called at all if IDAT chunks are collapsed (the default).
 }
     
 sub handle_mkTS($) {
@@ -1558,7 +1608,7 @@
   
   print "  Virtual image size: ${virtualimagewidth}x${virtualimageheight}";
   if($virtualpageunits == 0) {
-    say " pixels";
+    say " pixel(s)";
   } else {
     say;
     kvetch "Unrecognized virtual page units: $virtualpageunits";
@@ -1588,7 +1638,6 @@
   "help"		=> \$help_requested,
   "version"		=> \$version_requested,
   "license"		=> \$license_requested,
-  "collapse-idat!"	=> \$collapse_idat,
   "ignore-private!"	=> \$ignore_private,
   "ignore-unknown!"	=> \$ignore_unknown,
   "ignore-chunks=s"	=> sub {
@@ -1600,11 +1649,24 @@
                              $only_chunks{$_}++ foreach(split /,/, $chunklist);
                              $only_chunks++;
                            },
+  "collapse-chunks:s"	=> sub {
+                             my (undef, $chunklist) = @_;
+                             $collapse_chunks{$_}++ foreach(split /,/, $chunklist);
+                             $collapse_chunks++;
+                           },
+  "round=i"		=> \$decimal_places,
 );
 
+help	if $help_requested;
+version	if $version_requested;
+license	if $license_requested;
+
 # if at most one is specified, at least one multiplicand is 0, and therefore the product.
 die "--ignore-chunks and --only-chunks cannot both be specified" if(scalar(%ignore_chunks) * scalar(%only_chunks));
 
+# can't set the default before option processing, since then you couldn't un-collapse the defaults.
+%collapse_chunks = %collapse_default unless $collapse_chunks;
+
 # start processing file
 help if(scalar @ARGV == 0);
 my $file = $ARGV[0];
@@ -1621,16 +1683,23 @@
 while(read(PNG, my $chunkhead, 8)) {
 
   my ($length, $chunktype) = unpack("NA4", $chunkhead);
-
-  # Flush IDAT chunk info, if necessary. This needs to happen first so it
-  # won't be affected by ignored chunks. Relying on $idat_count rather than
-  # $lastchunk ensures that this still works when IDAT chunks are ignored
-  # for some reason.
-  if($collapse_idat and $chunktype ne "IDAT" and $idat_count) {
-    say "IDAT ($idat_count chunks) ($idat_len bytes)";
-    $idat_count = $idat_len = 0;
+  
+  # Flush collapsed chunk info, if necessary. This needs to happen first so
+  # it'll still happen if the new chunk happens to be ignored.
+  if($chunktype ne $collapsing and $collapsed_count) {
+    print "$collapsing";
+    print " ($collapsed_count chunks)" if($collapsed_count > 1);
+    say   " ($collapsed_len bytes)";
+    
+    $collapsed_count = $collapsed_len = $collapsing = 0;
   }
-     
+  
+  # If we are expecting a specific chunk, check if it actually occurs.
+  if($nextchunk and $chunktype ne $nextchunk) {
+    kvetch("expected $nextchunk after $lastchunk, found $chunktype");
+  }  
+  $nextchunk = "";
+
   # determine whether we should ignore this chunk.
   $chunktype =~ m/^.(.)/;
   unless(
@@ -1639,16 +1708,17 @@
     (!$only_chunks{$chunktype} and $only_chunks)	# this chunk's not on our whitelist (and we have a whitelist)
     ) { 
 
-    # handle IDAT chunk collapsing, if enabled.
-    if($collapse_idat and $chunktype eq "IDAT") {
-      $idat_len += $length;
-      $idat_count++;
+    # handle chunk collapsing.
+    if($collapse_chunks{$chunktype}) {
+      $collapsing = $chunktype;
+      $collapsed_len += $length;
+      $collapsed_count++;
       seek PNG, $length, SEEK_CUR;
         
     } else {
     
       say "$chunktype ($length bytes)";
-      kvetch "Invalid chunk name: $chunktype" unless $chunktype =~ /[A-Za-z]{4}/; # FIXME: probably breaks on EBDCIC
+      kvetch "Invalid chunk name: $chunktype" unless $chunktype =~ /[A-Za-z]{4}/;
       kvetch "No chunks allowed after IEND chunk" if $seen{'IEND'};
 
       # find and invoke handler routine      
