File Coverage

blib/lib/Acme/Steganography/Image/Png.pm
Criterion Covered Total %
statement 116 199 58.2
branch 15 30 50.0
condition 5 9 55.5
subroutine 21 28 75.0
pod 1 7 14.2
total 158 273 57.8


line stmt bran cond sub pod time code
1             package Acme::Steganography::Image::Png;
2              
3 3     3   129151 use strict;
  3         6  
  3         123  
4 3     3   15 use vars qw($VERSION @ISA);
  3         7  
  3         179  
5              
6 3     3   2539 use Imager;
  3         100262  
  3         24  
7             require Class::Accessor;
8 3     3   210 use Carp;
  3         5  
  3         3840  
9              
10             @ISA = qw(Class::Accessor);
11              
12             $VERSION = '0.06';
13              
14             my @keys = qw(offset data section x y datum_length done filename_generator
15             suffix);
16              
17             # What arguments can we accept to the constructor.
18             # Am I reinventing the wheel here?
19             my %keys;
20             @keys{@keys} = ();
21              
22             sub _keys {
23 3     3   11 return \%keys;
24             }
25              
26             Acme::Steganography::Image::Png->mk_accessors(@keys);
27              
28             # This will get refactored out at some point to support other formats.
29             sub generate_header {
30 17     17 0 25 my ($self) = shift;
31 17         49 my $section = $self->section;
32              
33 17         208 my $header = pack 'w', $section;
34 17 100       42 if (!$section) {
35 1         1 $header .= pack 'w', length ${$self->data};
  1         4  
36             }
37 17         376 $header;
38             }
39              
40             sub default_filename_generator {
41 17     17 0 28 my $state = shift;
42 17   100     50 $state ||= 0;
43 17         30 my $new_state = $state+1;
44             # really unimaginative filenames by default
45 17         45 ($state, $new_state);
46             }
47              
48             package Acme::Steganography::Image::Png::FlashingNeonSignGrey;
49              
50 3     3   19 use vars '@ISA';
  3         4  
  3         698  
51             @ISA = 'Acme::Steganography::Image::Png';
52              
53             # Raw data as a greyscale PNG
54              
55             sub make_image {
56 17     17   22 my $self = shift;
57 17         65 my $img = new Imager;
58 17         353 $img->read(data=>$_[0], type => 'raw', xsize => $self->x,
59             ysize => $self->y, datachannels=>1, storechannels=>1, bits=>8);
60 17         13932 $img;
61             }
62              
63             sub calculate_datum_length {
64 1     1   2 my $self = shift;
65 1         4 $self->x * $self->y;
66             }
67              
68             sub extract_payload {
69 17     17   31 my ($class, $img) = @_;
70 17         17 my $datum;
71 17         65 $img->write(data=> \$datum, type => 'raw');
72 17         437 $datum;
73             }
74              
75             package Acme::Steganography::Image::Png::RGB::556;
76              
77 3     3   15 use vars '@ISA';
  3         5  
  3         1165  
78             @ISA = 'Acme::Steganography::Image::Png::RGB';
79              
80             # Raw data in the low bits of a colour image
81              
82             Acme::Steganography::Image::Png->mk_accessors('raw');
83              
84             sub extract_payload {
85 0     0   0 my ($class, $img) = @_;
86 0         0 my ($raw, $data);
87 0         0 $img->write(data=> \$raw, type => 'raw');
88 0         0 my $end = length ($raw)/3;
89              
90 0         0 for (my $offset = 0; $offset < $end; ++$offset) {
91 0         0 my ($red, $green, $blue) = unpack 'x' . ($offset * 3) . 'C3', $raw;
92 0         0 my $datum = (($red & 0x1F) << 11) | (($green & 0x1F) << 6) | ($blue & 0x3F);
93 0         0 $data .= pack 'n', $datum;
94             }
95 0         0 $data;
96             }
97              
98             sub make_image {
99 0     0   0 my $self = shift;
100             # We get a copy to play with
101 0         0 my $raw = $self->raw;
102 0         0 my $offset = length ($raw)/3;
103 0         0 my $img = new Imager;
104              
105 0         0 while ($offset--) {
106 0         0 my $datum = unpack 'x' . ($offset * 2) . 'n', $_[0];
107 0         0 my $rgb = substr ($raw, $offset * 3, 3);
108             # Pack 16 bits into the low bits of R G and B
109 0         0 $rgb &= "\xE0\xE0\xC0";
110 0         0 $rgb |= pack 'C3', $datum >> 11, ($datum >> 6) & 0x1F, $datum & 0x3F;
111 0         0 substr($raw, $offset * 3, 3, $rgb);
112             }
113 0         0 $img->read(data=>$raw, type => 'raw', xsize => $self->x,
114             ysize => $self->y, datachannels => 3,interleave => 0);
115 0         0 $img;
116             }
117              
118             sub calculate_datum_length {
119 0     0   0 my $self = shift;
120 0         0 $self->x * $self->y * 2;
121             }
122              
123             package Acme::Steganography::Image::Png::RGB::556FS;
124              
125 3     3   15 use vars '@ISA';
  3         4  
  3         1330  
126             @ISA = 'Acme::Steganography::Image::Png::RGB::556';
127              
128             # Raw data in the low bits of a colour image, with Floyd-Steinberg dithering
129             # to spread the errors around. Share and enjoy, share and enjoy.
130              
131             sub make_image {
132 0     0   0 my $self = shift;
133             # We get a copy to play with
134 0         0 my $raw = $self->raw;
135 0         0 my $img = new Imager;
136 0         0 my $next_row;
137              
138 0         0 my $xsize = $self->x;
139 0         0 my $ysize = $self->y;
140              
141 0         0 for (my $y = $ysize; $y-- > 0; ) {
142             # New row
143 0         0 my $this_row = $next_row;
144 0         0 undef $next_row;
145              
146 0         0 for (my $x = $xsize; $x-- > 0; ) {
147 0         0 my $offset = $y * $xsize + $x;
148              
149             # I'm not sure if I've got the algorithm correct.
150 0         0 my $datum = unpack 'x' . ($offset * 2) . 'n', $_[0];
151              
152 0         0 my @rgb = unpack 'x' . ($offset * 3) . 'C3', $raw;
153 0         0 foreach (0..2) {
154 0   0     0 $rgb[$_] += $this_row->[$x + 1][$_] || 0;
155             # And this is most definitely an empirical hack, as there seem to be
156             # big systematic problems if the errors drive things outside the range
157             # 0-255
158 0 0       0 if ($rgb[$_] > 255) {
    0          
159 0         0 $rgb[$_] = 255;
160             } elsif ($rgb[$_] < 0) {
161 0         0 $rgb[$_] = 0;
162             }
163             }
164             # What we'd ideally have liked to output
165 0         0 my @rgb_ideal = @rgb;
166             # Pack 16 bits into the low bits of R G and B
167 0         0 $rgb[0] = ($rgb[0] & 0xE0) | $datum >> 11;
168 0         0 $rgb[1] = ($rgb[1] & 0xE0) | (($datum >> 6) & 0x1F);
169 0         0 $rgb[2] = ($rgb[2] & 0xC0) | ($datum & 0x3F);
170 0         0 substr($raw, $offset * 3, 3, pack 'C3', @rgb);
171              
172             # Calculate the error and dither it
173             # 7 x
174             # 1 5 3
175             # Note that the backwards dithering is why we need the +1 on the co-ords.
176 0         0 foreach (0..2) {
177 0         0 my $error = ($rgb_ideal[$_] - $rgb[$_]) / 16;
178 0         0 $this_row->[$x][$_] += $error * 7;
179 0         0 $next_row->[$x + 2][$_] += $error * 3;
180 0         0 $next_row->[$x + 1][$_] += $error * 5;
181 0         0 $next_row->[$x][$_] += $error;
182             }
183             }
184             }
185              
186 0         0 $img->read(data=>$raw, type => 'raw', xsize => $xsize,
187             ysize => $ysize, datachannels => 3,interleave => 0);
188 0         0 $img;
189             }
190              
191             package Acme::Steganography::Image::Png::RGB::323;
192              
193 3     3   21 use vars '@ISA';
  3         4  
  3         1376  
194             @ISA = 'Acme::Steganography::Image::Png::RGB';
195              
196             # Raw data in the low bits of a colour image
197              
198             Acme::Steganography::Image::Png->mk_accessors('raw');
199              
200             sub extract_payload {
201 0     0   0 my ($class, $img) = @_;
202 0         0 my ($raw, $data);
203 0         0 $img->write(data=> \$raw, type => 'raw');
204 0         0 my $end = length ($raw)/3;
205              
206 0         0 for (my $offset = 0; $offset < $end; ++$offset) {
207 0         0 my ($red, $green, $blue) = unpack 'x' . ($offset * 3) . 'C3', $raw;
208 0         0 my $datum = (($red & 0x7) << 5) | (($green & 0x3) << 3) | ($blue & 0x7);
209 0         0 $data .= chr $datum;
210             }
211 0         0 $data;
212             }
213              
214             sub make_image {
215 0     0   0 my $self = shift;
216             # We get a copy to play with
217 0         0 my $raw = $self->raw;
218 0         0 my $offset = length ($raw)/3;
219 0         0 my $img = new Imager;
220              
221 0         0 while ($offset--) {
222 0         0 my $datum = unpack "x$offset C", $_[0];
223 0         0 my $rgb = substr ($raw, $offset * 3, 3);
224             # Pack 8 bits into the low bits of R G and B
225 0         0 $rgb &= "\xF8\xFC\xF8";
226 0         0 $rgb |= ("\x07\x03\x07" & pack 'C3', $datum >> 5, $datum >> 3, $datum);
227 0         0 substr($raw, $offset * 3, 3, $rgb);
228             }
229 0         0 $img->read(data=>$raw, type => 'raw', xsize => $self->x,
230             ysize => $self->y, datachannels => 3,interleave => 0);
231 0         0 $img;
232             }
233              
234             sub calculate_datum_length {
235 0     0   0 my $self = shift;
236 0         0 $self->x * $self->y;
237             }
238              
239             package Acme::Steganography::Image::Png::RGB;
240              
241 3     3   15 use vars '@ISA';
  3         5  
  3         3065  
242             @ISA = 'Acme::Steganography::Image::Png';
243              
244             # Raw data in the low bits of a colour image
245              
246             sub write_images {
247 2     2   1151 my $self = shift;
248 2         4 my $victim = shift;
249              
250 2         3 my $img;
251 2 100 66     24 if (ref($victim) && $victim->isa('Imager')) {
252 1         3 $img = $victim;
253             } else {
254 1         9 $img = new Imager;
255 1 50       85 $img->open(file=>$victim, type=>'jpeg') or croak($img->errstr);
256             }
257              
258              
259 1         26 $self->x($img->getwidth());
260 1         42 $self->y($img->getheight());
261              
262 1         21 my $raw;
263 1 50       7 $img->write(data=> \$raw, type => 'raw')
264             or croak($img->errstr);
265              
266 0         0 $self->raw($raw);
267              
268 0         0 $self->SUPER::write_images;
269             }
270             package Acme::Steganography::Image::Png;
271              
272             sub generate_next_image {
273 17     17 0 32 my ($self) = shift;
274 17         39 my $datum = $self->generate_header;
275 17         70 my $offset = $self->offset;
276 17         169 my $datum_length = $self->datum_length;
277             # Fill our blob of data to the correct length
278 17         136 my $grab = $datum_length - length $datum;
279 17         23 $datum .= substr ${$self->data()}, $offset, $grab;
  17         40  
280 17         1877 $self->offset($offset + $grab);
281              
282 17 100       219 if (length $datum < $datum_length) {
  16 50       40  
283             # Need to pad it. NUL is so uninspiring.
284 1         38 $datum .= "N" x ($datum_length - length $datum);
285 1         5 $self->done(1);
286             } elsif (length ${$self->data()} == $self->offset) {
287 0         0 warn length $datum;
288             }
289 17         268 $self->section($self->section + 1);
290              
291 17         252 $self->make_image($datum);
292             }
293              
294             sub new {
295 3     3 1 5982 my $class = shift;
296 3 50       14 croak "Use a classname, not a reference for " . __PACKAGE__ . "::new"
297             if ref $class;
298 3         13 my $self = bless {}, $class;
299 3         9 my %args = @_;
300 3         50 my $acceptable = $self->_keys();
301 3         12 foreach (keys %args) {
302 0 0       0 croak "Unknown parameter $_" unless exists $acceptable->{$_};
303 0         0 $self->set($_, $args{$_});
304             }
305 3 50       46 $self->x(352) unless $args{x};
306 3 50       147 $self->y(288) unless $args{y};
307              
308             # Kowtow to the metadata bodging into filenames world
309 3         47 $self->suffix('.png');
310              
311 3         33 $self;
312             }
313              
314             sub type {
315 1     1 0 4 'png';
316             }
317              
318             sub write_images {
319 1     1 0 749 my $self = shift;
320 1         7 $self->section(0);
321 1         15 $self->offset(0);
322 1         13 $self->datum_length($self->calculate_datum_length());
323 1         42 my $type = $self->type;
324 1   50     9 my $filename_generator
325             = $self->filename_generator || \&default_filename_generator;
326              
327 1         16 my @filenames;
328              
329 1         2 my ($filename, $state);
330 1         7 while (!$self->done()) {
331 17         987 my $image = $self->generate_next_image;
332 17         49 ($filename, $state) = &$filename_generator($state);
333 17         69 $filename .= $self->suffix;
334 17         209 $image->write(file => $filename, type=> $type);
335 17         8233 push @filenames, $filename;
336             }
337 1         63 @filenames;
338             }
339              
340             # package method
341             sub read_files {
342 1     1 0 17220 my $class = shift;
343             # This is intentionally a "sparse" array to avoid some "interesting" DOS
344             # possibilities.
345 1         5 my $length;
346             my %got;
347 1         21 foreach my $file (@_) {
348 17         181 my $img = new Imager;
349 17 50       358 $img->open(file => $file) or carp "Can't read '$file': " . $img->errstr;
350 17         6448 my $payload = $class->extract_payload($img);
351 17         23 my $datum;
352             my $section;
353 17         1145 ($section, $datum) = unpack "wa*", $payload;
354 17 50       1192 if ($section == 0) {
355             # Oops. Strip off the length.
356 17         1063 ($length, $datum) = unpack "wa*", $datum;
357             }
358 17         105 $got{$section} = $datum;
359             }
360 1 50       12 carp "Did not find first section in files @_" unless defined $length;
361              
362 1         6 my $data = join '', map {$got{$_}} sort {$a <=> $b} keys %got;
  1         65  
  0         0  
363 1         58 substr ($data, $length) = '';
364              
365 1         8 $data;
366             }
367              
368             1;
369             __END__