| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package Image::PNG::Rewriter; | 
| 2 |  |  |  |  |  |  |  | 
| 3 | 3 |  |  | 3 |  | 81435 | use 5.010000; | 
|  | 3 |  |  |  |  | 12 |  | 
|  | 3 |  |  |  |  | 132 |  | 
| 4 | 3 |  |  | 3 |  | 17 | use strict; | 
|  | 3 |  |  |  |  | 6 |  | 
|  | 3 |  |  |  |  | 110 |  | 
| 5 | 3 |  |  | 3 |  | 14 | use warnings; | 
|  | 3 |  |  |  |  | 9 |  | 
|  | 3 |  |  |  |  | 90 |  | 
| 6 | 3 |  |  | 3 |  | 10101 | use Compress::Zlib qw(); | 
|  | 3 |  |  |  |  | 333052 |  | 
|  | 3 |  |  |  |  | 71 |  | 
| 7 | 3 |  |  | 3 |  | 27 | use Carp; | 
|  | 3 |  |  |  |  | 5 |  | 
|  | 3 |  |  |  |  | 205 |  | 
| 8 | 3 |  |  | 3 |  | 4191 | use POSIX qw/ceil/; | 
|  | 3 |  |  |  |  | 29901 |  | 
|  | 3 |  |  |  |  | 20 |  | 
| 9 |  |  |  |  |  |  |  | 
| 10 |  |  |  |  |  |  | our $VERSION = '0.9'; | 
| 11 |  |  |  |  |  |  | our $PNG_MAGIC = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"; | 
| 12 |  |  |  |  |  |  |  | 
| 13 |  |  |  |  |  |  | require XSLoader; | 
| 14 |  |  |  |  |  |  | XSLoader::load('Image::PNG::Rewriter', $VERSION); | 
| 15 |  |  |  |  |  |  |  | 
| 16 | 3 |  |  |  |  | 4146 | use constant CHANNELS => { | 
| 17 |  |  |  |  |  |  | 0 => 1, | 
| 18 |  |  |  |  |  |  | 2 => 3, | 
| 19 |  |  |  |  |  |  | 3 => 1, | 
| 20 |  |  |  |  |  |  | 4 => 2, | 
| 21 |  |  |  |  |  |  | 6 => 4, | 
| 22 | 3 |  |  | 3 |  | 3758 | }; | 
|  | 3 |  |  |  |  | 7 |  | 
| 23 |  |  |  |  |  |  |  | 
| 24 |  |  |  |  |  |  | sub new { | 
| 25 | 60 |  |  | 60 | 1 | 69245 | my $class = shift; | 
| 26 | 60 |  |  |  |  | 321 | my $self = bless {}, $class; | 
| 27 | 60 |  |  |  |  | 303 | my %o = @_; | 
| 28 | 60 |  |  |  |  | 141 | my $h = $o{handle}; | 
| 29 |  |  |  |  |  |  |  | 
| 30 | 60 | 50 |  |  |  | 199 | die "No 'handle' specified" unless $h; | 
| 31 |  |  |  |  |  |  |  | 
| 32 |  |  |  |  |  |  | $o{zlib} //= sub { | 
| 33 | 120 |  |  | 120 |  | 641 | my $data = shift; | 
| 34 | 120 |  |  |  |  | 540 | my ($d, $status0) = Compress::Zlib::deflateInit(); | 
| 35 | 120 | 50 |  |  |  | 48652 | die unless $status0 == Compress::Zlib::Z_OK; | 
| 36 | 120 |  |  |  |  | 984 | my ($out1, $status1) = $d->deflate($data); | 
| 37 | 120 | 50 |  |  |  | 215270 | die unless $status1 == Compress::Zlib::Z_OK; | 
| 38 | 120 |  |  |  |  | 1083 | my ($out2, $status2) = $d->flush(); | 
| 39 | 120 | 50 |  |  |  | 5624 | die unless $status2 == Compress::Zlib::Z_OK; | 
| 40 | 120 |  |  |  |  | 1971 | return $out1 . $out2; | 
| 41 | 60 |  | 50 |  |  | 794 | }; | 
| 42 |  |  |  |  |  |  |  | 
| 43 | 60 |  |  |  |  | 199 | $self->{_zlib} = $o{zlib}; | 
| 44 |  |  |  |  |  |  |  | 
| 45 | 60 | 50 |  |  |  | 619 | read($h, my $magic, 8) == 8 or die; | 
| 46 |  |  |  |  |  |  |  | 
| 47 | 60 | 50 |  |  |  | 202 | die "Not a PNG image" unless $magic eq $PNG_MAGIC; | 
| 48 |  |  |  |  |  |  |  | 
| 49 | 60 |  |  |  |  | 144 | $self->{_chunks} = []; | 
| 50 |  |  |  |  |  |  |  | 
| 51 | 60 |  |  |  |  | 209 | while (!eof($h)) { | 
| 52 |  |  |  |  |  |  | # [size] [type] [data] [checksum] | 
| 53 | 436 | 50 |  |  |  | 1320 | read($h, my $raw, 8) == 8 or die; | 
| 54 | 436 |  |  |  |  | 1681 | my ($length, $type) = unpack 'Na4', $raw; | 
| 55 | 436 | 50 |  |  |  | 1323 | read($h, my $data, $length) == $length or die; | 
| 56 | 436 | 50 |  |  |  | 12058 | read($h, my $crc_raw, 4) == 4 or die; | 
| 57 | 436 |  |  |  |  | 1108 | my $crc = unpack 'N', $crc_raw; | 
| 58 | 436 |  |  |  |  | 502 | push @{ $self->{_chunks} }, { | 
|  | 436 |  |  |  |  | 2818 |  | 
| 59 |  |  |  |  |  |  | type => $type, | 
| 60 |  |  |  |  |  |  | size => $length, | 
| 61 |  |  |  |  |  |  | data => $data, | 
| 62 |  |  |  |  |  |  | crc32 => $crc, | 
| 63 |  |  |  |  |  |  | }; | 
| 64 |  |  |  |  |  |  | } | 
| 65 |  |  |  |  |  |  |  | 
| 66 |  |  |  |  |  |  | # get the first IHDR chunk; only one is allowed | 
| 67 | 60 |  |  |  |  | 106 | my ($ihdr) = grep { $_->{type} eq 'IHDR' } @{ $self->{_chunks} }; | 
|  | 436 |  |  |  |  | 882 |  | 
|  | 60 |  |  |  |  | 185 |  | 
| 68 | 60 | 50 |  |  |  | 1088 | die unless $ihdr; | 
| 69 |  |  |  |  |  |  |  | 
| 70 | 60 |  |  |  |  | 279 | my @ihdr_values = unpack 'NNccccc', $ihdr->{data}; | 
| 71 | 60 | 50 |  |  |  | 176 | die unless @ihdr_values == 7; | 
| 72 |  |  |  |  |  |  |  | 
| 73 | 60 |  |  |  |  | 465 | ($self->{_width}, | 
| 74 |  |  |  |  |  |  | $self->{_height}, | 
| 75 |  |  |  |  |  |  | $self->{_depth}, | 
| 76 |  |  |  |  |  |  | $self->{_color}, | 
| 77 |  |  |  |  |  |  | $self->{_comp}, | 
| 78 |  |  |  |  |  |  | $self->{_filter}, | 
| 79 |  |  |  |  |  |  | $self->{_interlace}) = @ihdr_values; | 
| 80 |  |  |  |  |  |  |  | 
| 81 | 60 | 50 |  |  |  | 231 | die unless $self->{_width}; | 
| 82 | 60 | 50 |  |  |  | 143 | die unless $self->{_height}; | 
| 83 |  |  |  |  |  |  |  | 
| 84 |  |  |  |  |  |  | # TODO: validate depth/type restrictions? | 
| 85 |  |  |  |  |  |  |  | 
| 86 | 60 | 50 |  |  |  | 156 | die unless $self->{_comp} == 0; | 
| 87 | 60 | 50 |  |  |  | 141 | die unless $self->{_filter} == 0; | 
| 88 |  |  |  |  |  |  |  | 
| 89 | 60 | 50 |  |  |  | 158 | confess "Interlaced images are not supported" | 
| 90 |  |  |  |  |  |  | if $self->{_interlace}; | 
| 91 |  |  |  |  |  |  |  | 
| 92 | 60 |  |  |  |  | 253 | $self->{_channels} = (CHANNELS)->{ $self->{_color} }; | 
| 93 |  |  |  |  |  |  |  | 
| 94 | 60 | 50 |  |  |  | 192 | die unless defined $self->{_channels}; | 
| 95 |  |  |  |  |  |  |  | 
| 96 |  |  |  |  |  |  | # PNGs can have many IDAT chunks | 
| 97 | 60 |  |  |  |  | 229 | my $coalesced = join '', map { $_->{data} } | 
|  | 436 |  |  |  |  | 918 |  | 
| 98 | 60 |  |  |  |  | 163 | grep { $_->{type} eq 'IDAT' } @{ $self->{_chunks} }; | 
|  | 60 |  |  |  |  | 169 |  | 
| 99 |  |  |  |  |  |  |  | 
| 100 |  |  |  |  |  |  | # One IEND chunk is required | 
| 101 | 436 |  |  |  |  | 1245 | die unless 1 == grep { $_->{type} eq 'IEND' } | 
|  | 60 |  |  |  |  | 119 |  | 
| 102 | 60 | 50 |  |  |  | 101 | @{ $self->{_chunks} }; | 
| 103 |  |  |  |  |  |  |  | 
| 104 | 60 |  |  |  |  | 254 | my ($i, $status0) = Compress::Zlib::inflateInit; | 
| 105 | 60 | 50 |  |  |  | 7877 | die unless $status0 == Compress::Zlib::Z_OK; | 
| 106 | 60 |  |  |  |  | 532 | my ($inflated, $status1) = $i->inflate("$coalesced"); | 
| 107 | 60 | 50 |  |  |  | 48629 | die $status1 unless $status1 == Compress::Zlib::Z_STREAM_END; | 
| 108 |  |  |  |  |  |  |  | 
| 109 | 60 |  |  |  |  | 438 | $self->{_inflated} = $inflated; | 
| 110 | 60 |  |  |  |  | 148 | $self->{_deflated} = $coalesced; | 
| 111 |  |  |  |  |  |  |  | 
| 112 | 60 |  |  |  |  | 219 | $self->{_new_deflated} = "$coalesced"; | 
| 113 | 60 |  |  |  |  | 3713 | $self->{_new_inflated} = "$inflated"; | 
| 114 |  |  |  |  |  |  |  | 
| 115 | 60 |  |  |  |  | 559 | my $expected_bytes = $self->{_height} * | 
| 116 |  |  |  |  |  |  | ceil(($self->{_width} * $self->{_channels} * $self->{_depth} + 8) / 8); | 
| 117 |  |  |  |  |  |  |  | 
| 118 | 60 |  |  |  |  | 149 | my $actual_bytes = length $self->{_inflated}; | 
| 119 | 60 | 50 |  |  |  | 155 | die unless $expected_bytes == $actual_bytes; | 
| 120 |  |  |  |  |  |  |  | 
| 121 | 60 |  |  |  |  | 167 | $self->{_scanline_width} = $expected_bytes / $self->{_height}; | 
| 122 | 60 |  |  |  |  | 332 | $self->{_scanline_delta} = $self->{_channels} * ceil($self->{_depth} / 8); | 
| 123 |  |  |  |  |  |  |  | 
| 124 |  |  |  |  |  |  | # Destructive operation needs a copy | 
| 125 | 60 |  |  |  |  | 12488 | $self->{_unfiltered} = "$inflated"; | 
| 126 | 60 |  |  |  |  | 29463 | _unfilter($self->{_unfiltered}, $self->{_height}, $self->{_scanline_delta}, $self->{_scanline_width}); | 
| 127 |  |  |  |  |  |  |  | 
| 128 | 60 |  |  |  |  | 621 | $self; | 
| 129 |  |  |  |  |  |  | } | 
| 130 |  |  |  |  |  |  |  | 
| 131 |  |  |  |  |  |  | sub refilter { | 
| 132 | 120 |  |  | 120 | 1 | 275 | my $self = shift; | 
| 133 | 120 |  |  |  |  | 1477 | my @filters = @_; | 
| 134 | 120 | 50 |  |  |  | 355 | die unless @filters == $self->height; | 
| 135 |  |  |  |  |  |  |  | 
| 136 | 120 |  |  |  |  | 17032 | $self->{_new_inflated} = $self->{_unfiltered} . ""; | 
| 137 | 120 |  |  |  |  | 8443 | my $filter = join '', map chr, @filters; | 
| 138 |  |  |  |  |  |  |  | 
| 139 | 120 |  |  |  |  | 36996 | _filter($self->{_unfiltered}, $self->{_new_inflated}, | 
| 140 |  |  |  |  |  |  | $filter, $self->{_height}, $self->{_scanline_delta}, | 
| 141 |  |  |  |  |  |  | $self->{_scanline_width}); | 
| 142 |  |  |  |  |  |  |  | 
| 143 | 120 |  |  |  |  | 379 | $self->{_new_filters} = \@filters; | 
| 144 | 120 |  |  |  |  | 963 | $self->{_new_deflated} = $self->{_zlib}->($self->{_new_inflated}); | 
| 145 | 120 |  |  |  |  | 579 | return $self->{_new_deflated}, $self->{_new_inflated}; | 
| 146 |  |  |  |  |  |  | } | 
| 147 |  |  |  |  |  |  |  | 
| 148 |  |  |  |  |  |  | sub as_png { | 
| 149 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 150 | 0 |  |  |  |  | 0 | my @other_chunks = | 
| 151 | 0 |  |  |  |  | 0 | grep { $_->{type} ne 'IDAT' } $self->original_chunks; | 
| 152 | 0 |  |  |  |  | 0 | my $data = $self->{_new_deflated}; | 
| 153 |  |  |  |  |  |  |  | 
| 154 | 0 |  |  |  |  | 0 | my $idat = { type => 'IDAT', data => $data, | 
| 155 |  |  |  |  |  |  | crc32 => Compress::Zlib::crc32("IDAT$data") }; | 
| 156 |  |  |  |  |  |  |  | 
| 157 | 0 |  |  |  |  | 0 | my @chunks = map { | 
| 158 | 0 | 0 |  |  |  | 0 | pack('Na4', length $_->{data}, $_->{type}) | 
| 159 |  |  |  |  |  |  | . $_->{data} . pack('N', $_->{crc32}) | 
| 160 |  |  |  |  |  |  | } map { | 
| 161 | 0 |  |  |  |  | 0 | $_->{type} eq 'IEND' ? ($idat, $_) : $_ | 
| 162 |  |  |  |  |  |  | } @other_chunks; | 
| 163 |  |  |  |  |  |  |  | 
| 164 | 0 |  |  |  |  | 0 | return $PNG_MAGIC . join '', @chunks; | 
| 165 |  |  |  |  |  |  | } | 
| 166 |  |  |  |  |  |  |  | 
| 167 |  |  |  |  |  |  | sub original_filters { | 
| 168 | 120 |  |  | 120 | 1 | 203 | my $self = shift; | 
| 169 | 120 |  |  |  |  | 1395 | map { ord(substr $self->{_inflated}, | 
|  | 15420 |  |  |  |  | 39841 |  | 
| 170 |  |  |  |  |  |  | $_ * $self->{_scanline_width}, 1) } | 
| 171 |  |  |  |  |  |  | 0 .. $self->{_height} - 1; | 
| 172 |  |  |  |  |  |  | } | 
| 173 |  |  |  |  |  |  |  | 
| 174 | 0 |  |  | 0 | 1 | 0 | sub original_chunks { @{ $_[0]->{_chunks} } } | 
|  | 0 |  |  |  |  | 0 |  | 
| 175 | 60 |  |  | 60 | 1 | 1178 | sub original_inflated { $_[0]->{_inflated} } | 
| 176 | 0 |  |  | 0 | 1 | 0 | sub original_deflated { $_[0]->{_deflated} } | 
| 177 | 60 |  |  | 60 | 1 | 672 | sub width { $_[0]->{_width} } | 
| 178 | 240 |  |  | 240 | 1 | 23703 | sub height { $_[0]->{_height} } | 
| 179 | 60 |  |  | 60 | 1 | 336 | sub color_mode { $_[0]->{_color} } | 
| 180 | 60 |  |  | 60 | 1 | 257 | sub depth { $_[0]->{_depth} } | 
| 181 |  |  |  |  |  |  |  | 
| 182 | 60 |  |  | 60 | 1 | 266 | sub scanline_width { $_[0]->{_scanline_width} } | 
| 183 | 60 |  |  | 60 | 1 | 247 | sub scanline_delta { $_[0]->{_scanline_delta} } | 
| 184 |  |  |  |  |  |  |  | 
| 185 |  |  |  |  |  |  | 1; | 
| 186 |  |  |  |  |  |  |  | 
| 187 |  |  |  |  |  |  | __END__ |