File Coverage

blib/lib/Image/PNM.pm
Criterion Covered Total %
statement 258 269 95.9
branch 60 80 75.0
condition 9 30 30.0
subroutine 31 33 93.9
pod 7 7 100.0
total 365 419 87.1


line stmt bran cond sub pod time code
1             package Image::PNM;
2             BEGIN {
3 7     7   112557 $Image::PNM::AUTHORITY = 'cpan:DOY';
4             }
5             $Image::PNM::VERSION = '0.01';
6 7     7   43 use strict;
  7         8  
  7         175  
7 7     7   23 use warnings;
  7         8  
  7         18126  
8             # ABSTRACT: parse and generate PNM (PBM, PGM, PPM) files
9              
10              
11              
12             sub new {
13 7     7 1 62 my $class = shift;
14 7         14 my ($data) = @_;
15              
16 7         18 my $self = bless {}, $class;
17              
18 7 50       35 if (ref $data) {
    100          
19 0         0 $self->_parse_string($data);
20             }
21             elsif ($data) {
22 6         29 $self->_parse_file($data);
23             }
24             else {
25 1         5 $self->{w} = 1;
26 1         2 $self->{h} = 1;
27 1         2 $self->{max} = 1;
28 1         2 $self->{pixels} = [[0]];
29             }
30              
31 7         70 return $self;
32             }
33              
34              
35             sub as_string {
36 37     37 1 54 my $self = shift;
37 37         45 my ($format) = @_;
38              
39 37         62 my $method = "_as_string_$format";
40 37 50       138 die "Unknown format $format"
41             unless $self->can($method);
42              
43 37         98 return $self->$method;
44             }
45              
46              
47             sub width {
48 7     7 1 36 my $self = shift;
49 7         11 my ($w) = @_;
50 7 100       20 if (defined($w)) {
51 1         2 for my $row (@{ $self->{pixels} }) {
  1         3  
52 1 50       3 if ($w > $self->{w}) {
53             push @$row, 0
54 1         8 for 1..($w - $self->{w});
55             }
56             else {
57             pop @$row
58 0         0 for 1..($self->{w} - $w);
59             }
60             }
61 1         3 $self->{w} = $w;
62             }
63 7         37 return $self->{w};
64             }
65              
66              
67             sub height {
68 7     7 1 17 my $self = shift;
69 7         9 my ($h) = @_;
70 7 100       24 if (defined($h)) {
71 1 50       2 if ($h > $self->{h}) {
72 7         14 push @{ $self->{pixels} }, [ (0) x $self->{w} ]
73 1         3 for 1..($h - $self->{h});
74             }
75             else {
76 0         0 pop @{ $self->{pixels} }
77 0         0 for 1..($self->{h} - $h);
78             }
79 1         2 $self->{h} = $h;
80             }
81 7         25 return $self->{h};
82             }
83              
84              
85             sub max_pixel_value {
86 7     7 1 18 my $self = shift;
87 7         10 my ($max) = @_;
88 7 100       29 if (defined($max)) {
89 1         1 for my $row (@{ $self->{pixels} }) {
  1         2  
90 8         9 @$row = map { $_ * $self->{max} / $max } @$row;
  48         55  
91             }
92 1         2 $self->{max} = $max;
93             }
94 7         21 return $self->{max};
95             }
96              
97              
98             sub pixel {
99 54     54 1 142 my $self = shift;
100 54         52 my ($row, $col, $new_value) = @_;
101              
102 54 100       89 if (defined($new_value)) {
103 144         192 $new_value = ref($new_value)
104 48 50       68 ? [ map { $_ * $self->{max} } @$new_value ]
105             : $new_value * $self->{max};
106             }
107 54         159 my $pixel = $self->raw_pixel($row, $col, $new_value);
108 54         70 return [ map { $_ / $self->{max} } @$pixel ];
  162         283  
109             }
110              
111              
112             sub raw_pixel {
113 76     76 1 95 my $self = shift;
114 76         70 my ($row, $col, $new_value) = @_;
115              
116 76         89 my $pixel = $self->{pixels}[$row][$col];
117 76 50       122 die "invalid pixel location ($row, $col)"
118             unless defined $pixel;
119              
120 76 100       102 if (defined($new_value)) {
121 64         66 $self->{pixels}[$row][$col] = $new_value;
122 64         47 $pixel = $new_value;
123             }
124              
125 76 100       127 if (!ref $pixel) {
126 8         14 $pixel = [ $pixel, $pixel, $pixel ];
127             }
128              
129 76         135 return $pixel;
130             }
131              
132             sub _as_string_P1 {
133 6     6   8 my $self = shift;
134              
135 6         20 my $data = <
136             P1
137             $self->{w} $self->{h}
138             HEADER
139              
140 6         8 for my $row (@{ $self->{pixels} }) {
  6         15  
141 288         175 $data .= join(' ', map {
142 48         53 my $val;
143 288 100       302 if (ref($_)) {
144 96         116 $val = $self->_to_greyscale(@$_);
145             }
146             else {
147 192         132 $val = $_;
148             }
149 288 100       540 $val * 2 > $self->{max} ? '0' : '1'
150             } @$row) . "\n";
151             }
152              
153 6         23 return $data;
154             }
155              
156             sub _as_string_P2 {
157 6     6   11 my $self = shift;
158              
159 6         29 my $data = <
160             P2
161             $self->{w} $self->{h}
162             $self->{max}
163             HEADER
164              
165 6         8 for my $row (@{ $self->{pixels} }) {
  6         17  
166             $data .= join(' ', map {
167 48 100       49 if (ref($_)) {
  288         293  
168 96         113 $self->_to_greyscale(@$_)
169             }
170             else {
171 192         202 $_
172             }
173             } @$row) . "\n";
174             }
175              
176 6         22 return $data;
177             }
178              
179             sub _as_string_P3 {
180 7     7   11 my $self = shift;
181              
182 7         33 my $data = <
183             P3
184             $self->{w} $self->{h}
185             $self->{max}
186             HEADER
187              
188 7         10 for my $row (@{ $self->{pixels} }) {
  7         19  
189 336 100       621 $data .= join(' ', map {
190 56         55 ref($_) ? join(' ', @$_) : "$_ $_ $_"
191             } @$row) . "\n";
192             }
193              
194 7         30 return $data;
195             }
196              
197             sub _as_string_P4 {
198 6     6   9 my $self = shift;
199              
200 6         23 my $data = <
201             P4
202             $self->{w} $self->{h}
203             HEADER
204              
205 6         9 for my $row (@{ $self->{pixels} }) {
  6         16  
206 288         327 my @vals = map {
207 48         59 my $val;
208 288 100       300 if (ref($_)) {
209 96         125 $val = $self->_to_greyscale(@$_);
210             }
211             else {
212 192         141 $val = $_;
213             }
214 288 100       478 $val * 2 > $self->{max} ? '0' : '1'
215             } @$row;
216 48         202 push @vals, '0' until @vals % 8 == 0;
217 48         75 while (@vals) {
218 48         87 my $bits = join('', splice(@vals, 0, 8));
219 48         70 my $byte = oct("0b$bits");
220 48         134 $data .= pack("C", $byte);
221             }
222             }
223              
224 6         33 return $data;
225             }
226              
227             sub _as_string_P5 {
228 6     6   10 my $self = shift;
229              
230 6         27 my $data = <
231             P5
232             $self->{w} $self->{h}
233             $self->{max}
234             HEADER
235              
236 6         10 for my $row (@{ $self->{pixels} }) {
  6         16  
237             $data .= pack("C*", map {
238 48 100       80 if (ref($_)) {
  288         302  
239 96         115 $self->_to_greyscale(@$_)
240             }
241             else {
242 192         222 $_
243             }
244             } @$row);
245             }
246              
247 6         27 return $data;
248             }
249              
250             sub _as_string_P6 {
251 6     6   8 my $self = shift;
252              
253 6         27 my $data = <
254             P6
255             $self->{w} $self->{h}
256             $self->{max}
257             HEADER
258              
259 6         11 for my $row (@{ $self->{pixels} }) {
  6         16  
260 288 100       490 $data .= pack("C*", map {
261 48         50 ref($_) ? @$_ : ($_, $_, $_)
262             } @$row);
263             }
264              
265 6         29 return $data;
266             }
267              
268             sub _parse_string {
269 0     0   0 my $self = shift;
270 0         0 my ($string) = @_;
271              
272             return $self->_parse_pnm(sub {
273 0     0   0 my ($line, $rest) = split /\n/, $string, 2;
274 0 0 0     0 return unless length($line) || length($rest);
275 0         0 $string = $rest;
276 0         0 return "$line\n";
277 0         0 });
278             }
279              
280             sub _parse_file {
281 6     6   8 my $self = shift;
282 6         13 my ($filename) = @_;
283              
284 6 50       260 open my $fh, '<', $filename
285             or die "Couldn't open $filename for reading: $!";
286              
287 6     218   42 return $self->_parse_pnm(sub { scalar <$fh> });
  218         450  
288             }
289              
290             sub _parse_pnm {
291 6     6   12 my $self = shift;
292 6         11 my ($next_line) = @_;
293              
294             my $next_line_nocomments = sub {
295 212     212   145 my $line;
296 212         273 while (!length($line)) {
297 218         203 $line = $next_line->();
298 218 50       316 return unless defined($line);
299 218         373 $line =~ s/#.*//s;
300             }
301 212         238 return $line;
302 6         23 };
303              
304 6         13 chomp(my $format = $next_line_nocomments->());
305 6         15 chomp(my $dimensions = $next_line_nocomments->());
306              
307 6         42 my ($w, $h) = $dimensions =~ /^([0-9]+)\s+([0-9]+)$/;
308 6 50 33     40 die "Invalid dimensions: $dimensions"
309             unless $w && $h;
310 6         38 $self->{w} = $w;
311 6         11 $self->{h} = $h;
312              
313 6         12 my $method = "_parse_pnm_$format";
314 6 50       44 die "Don't know how to parse PNM files of format $format"
315             unless $self->can($method);
316 6         23 return $self->$method($next_line_nocomments);
317             }
318              
319             sub _parse_pnm_P1 {
320 1     1   2 my $self = shift;
321 1         2 my ($next_line) = @_;
322              
323 1         1 $self->{max} = 1;
324              
325 1         4 my $next_word = $self->_make_next_word($next_line, 0);
326              
327 1         2 $self->{pixels} = [];
328 1         6 for my $i (1..$self->{h}) {
329 8         8 my $row = [];
330 8         10 for my $j (1..$self->{w}) {
331 48 100       47 push @$row, $next_word->() ? '0' : '1';
332             }
333 8         5 push @{ $self->{pixels} }, $row;
  8         20  
334             }
335             }
336              
337             sub _parse_pnm_P2 {
338 1     1   2 my $self = shift;
339 1         1 my ($next_line) = @_;
340              
341 1         2 chomp (my $max = $next_line->());
342 1 50 33     9 die "Invalid max color value: $max"
343             unless $max =~ /^[0-9]+$/ && $max > 0;
344 1         2 $self->{max} = $max;
345              
346 1         4 my $next_word = $self->_make_next_word($next_line, 1);
347              
348 1         2 $self->{pixels} = [];
349 1         5 for my $i (1..$self->{h}) {
350 8         8 my $row = [];
351 8         10 for my $j (1..$self->{w}) {
352 48         54 push @$row, $next_word->();
353             }
354 8         6 push @{ $self->{pixels} }, $row;
  8         20  
355             }
356             }
357              
358             sub _parse_pnm_P3 {
359 1     1   2 my $self = shift;
360 1         1 my ($next_line) = @_;
361              
362 1         2 chomp (my $max = $next_line->());
363 1 50 33     9 die "Invalid max color value: $max"
364             unless $max =~ /^[0-9]+$/ && $max > 0;
365 1         2 $self->{max} = $max;
366              
367 1         5 my $next_word = $self->_make_next_word($next_line, 1);
368              
369 1         3 $self->{pixels} = [];
370 1         7 for my $i (1..$self->{h}) {
371 8         8 my $row = [];
372 8         10 for my $j (1..$self->{w}) {
373 48         56 push @$row, [
374             $next_word->(),
375             $next_word->(),
376             $next_word->(),
377             ];
378             }
379 8         8 push @{ $self->{pixels} }, $row;
  8         19  
380             }
381             }
382              
383             sub _parse_pnm_P4 {
384 1     1   2 my $self = shift;
385 1         2 my ($next_line) = @_;
386              
387 1         1 $self->{max} = 1;
388              
389 1         4 my $next_word = $self->_make_next_bitfield($next_line, 1);
390              
391 1         2 $self->{pixels} = [];
392 1         6 for my $i (1..$self->{h}) {
393 8         7 my $row = [];
394 8         8 for my $j (1..$self->{w}) {
395 48 100       49 push @$row, $next_word->() ? '0' : '1';
396             }
397 8         7 push @{ $self->{pixels} }, $row;
  8         21  
398             }
399             }
400              
401             sub _parse_pnm_P5 {
402 1     1   2 my ($self) = shift;
403 1         1 my ($next_line) = @_;
404              
405 1         2 chomp (my $max = $next_line->());
406 1 50 33     8 die "Invalid max color value: $max"
407             unless $max =~ /^[0-9]+$/ && $max > 0;
408 1         3 $self->{max} = $max;
409              
410 1         4 my $next_word = $self->_make_next_bitfield($next_line, 0);
411              
412 1         2 $self->{pixels} = [];
413 1         5 for my $i (1..$self->{h}) {
414 8         7 my $row = [];
415 8         11 for my $j (1..$self->{w}) {
416 48         81 push @$row, $next_word->();
417             }
418 8         7 push @{ $self->{pixels} }, $row;
  8         26  
419             }
420             }
421              
422             sub _parse_pnm_P6 {
423 1     1   2 my $self = shift;
424 1         2 my ($next_line) = @_;
425              
426 1         1 chomp (my $max = $next_line->());
427 1 50 33     8 die "Invalid max color value: $max"
428             unless $max =~ /^[0-9]+$/ && $max > 0;
429 1         2 $self->{max} = $max;
430              
431 1         5 my $next_word = $self->_make_next_bitfield($next_line, 0);
432              
433 1         3 $self->{pixels} = [];
434 1         4 for my $i (1..$self->{h}) {
435 8         8 my $row = [];
436 8         12 for my $j (1..$self->{w}) {
437 48         57 push @$row, [
438             $next_word->(),
439             $next_word->(),
440             $next_word->(),
441             ];
442             }
443 8         9 push @{ $self->{pixels} }, $row;
  8         22  
444             }
445             }
446              
447             sub _make_next_word {
448 3     3   5 my $self = shift;
449 3         5 my ($next_line, $ws) = @_;
450              
451 3         5 my @words;
452             return sub {
453 240 100   240   310 if (!@words) {
454 193         173 my $line = $next_line->();
455 193 50       259 return unless $line;
456 193         567 chomp($line);
457 193 100       633 if ($ws) {
458 192         250 @words = split ' ', $line;
459             }
460             else {
461 1         13 @words = split '', $line;
462             }
463             }
464 240         218 my $word = shift @words;
465 240 50 33     1684 die "Invalid color: $word"
      33        
466             unless $word =~ /^[0-9]+$/ && $word >= 0 && $word <= $self->{max};
467 240         386 return $word;
468 3         16 };
469             }
470              
471             sub _make_next_bitfield {
472 3     3   5 my $self = shift;
473 3         5 my ($next_line, $bits) = @_;
474              
475 3         4 my @words;
476             return sub {
477 240 100   240   326 if (!@words) {
478 3         6 my $line = $next_line->();
479 3 50       423 return unless $line;
480 3 100       8 if ($bits) {
481 1         3 my $padding = 8 - ($self->{w} % 8);
482 1         3 my $per = int($self->{w} / 8) + 1;
483 1         3 while (length($line)) {
484 8         10 my $chunk = substr($line, 0, $per, '');
485 8         36 push @words, map {
486 8         23 split '', sprintf("%08b", $_)
487             } unpack("C*", $chunk);
488 8         24 pop @words for 1..$padding;
489             }
490             }
491             else {
492 2         50 @words = unpack("C*", $line);
493             }
494             }
495 240         207 my $word = shift @words;
496 240 50 33     1263 die "Invalid color: $word"
      33        
497             unless $word =~ /^[0-9]+$/ && $word >= 0 && $word <= $self->{max};
498 240         380 return $word;
499 3         17 };
500             }
501              
502             sub _to_greyscale {
503 384     384   312 my $self = shift;
504 384         368 my ($r, $g, $b) = @_;
505             # luma calculation
506             # https://en.wikipedia.org/wiki/YUV
507 384         641 int(0.2126*$r + 0.7152*$g + 0.0722*$b + 0.5)
508             }
509              
510              
511             1;
512              
513             __END__