File Coverage

blib/lib/Image/PNG/Rewriter.pm
Criterion Covered Total %
statement 103 115 89.5
branch 22 46 47.8
condition 1 2 50.0
subroutine 18 21 85.7
pod 13 13 100.0
total 157 197 79.7


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__