File Coverage

blib/lib/Image/Xpm.pm
Criterion Covered Total %
statement 208 245 84.9
branch 85 138 61.5
condition 36 69 52.1
subroutine 14 18 77.7
pod 9 9 100.0
total 352 479 73.4


line stmt bran cond sub pod time code
1             package Image::Xpm; # Documented at the __END__
2              
3 2     2   29699 use strict;
  2         2  
  2         51  
4              
5 2     2   6 use vars qw($VERSION @ISA);
  2         2  
  2         101  
6             $VERSION = '1.13';
7              
8 2     2   952 use Image::Base;
  2         2499  
  2         61  
9              
10             @ISA = qw(Image::Base);
11              
12              
13 2     2   18 use Carp qw(carp croak);
  2         2  
  2         96  
14 2     2   412 use Symbol ();
  2         632  
  2         4719  
15              
16             ### Data structures
17             #
18             # We will call the characters that are used to signify a particular colour the
19             # cc's.
20             #
21             # -palette is a hash keyed by the cc's whose values are hashes of palette
22             # colours, e.g. key x colour pairs.
23             #
24             # -cindex hash is a hash keyed by colour name ('#ffffff', 'blue' etc) whose
25             # values are the cc's in the palette that represent that colour. Note that the
26             # colour names are all lowercased even if they are mixed case in the palette
27             # itself.
28             #
29             # -pixels is a string of cc's which is effectively a vector of 8, 16, 24, 32
30             # bits, etc.
31             #
32             # -extlines are lines of text used for any extensions; if we read any in we
33             # hold them with the image and write them out if the image is saved, but we do
34             # not process them.
35              
36              
37             # Private class data
38              
39             # If you inherit don't clobber these fields!
40             my @FIELD = qw(-file -width -height -ncolours -cpp -hotx -hoty -cc
41             -palette -cindex -pixels
42             -extname -extlines -comments -commentpixel -commentcolour);
43              
44             # States for parsing an xpm file
45             my $STATE_START = 0;
46             my $STATE_IN_COMMENT = 1;
47             my $STATE_ARRAY = 2;
48             my $STATE_VALUES = 3;
49             my $STATE_COLOURS = 4;
50             my $STATE_PIXELS = 5;
51             my $STATE_EXTENSIONS = 6;
52             my $STATE_FINISH = 7;
53              
54             my $MAX_CH = 255;
55             my $CH_BS = 127;
56             my $CH_BSLASH = 92;
57             my $CH_QUOTE = 39;
58             my $CH_DQUOTE = 34;
59             my $CH_SPACE = 32;
60              
61             my $UNSET = -1;
62              
63             ### Private methods
64             #
65             # _get object inherited
66             # _set object inherited
67             # _nextcc object
68             # _add_colour object
69             # _add_color object
70              
71              
72             sub _nextcc { # Object method
73 35     35   23 my $self = shift;
74             # my $class = ref($self) || $self;
75              
76 35         49 while (exists $self->{-palette}{$self->{-cc}}) {
77 52         99 my @ch = unpack "C$self->{-cpp}", $self->{-cc};
78 52         36 my $found = 0;
79 52         65 foreach my $i (reverse 0..$self->{-cpp} - 1) {
80 52 50       57 if ($ch[$i] < $MAX_CH) {
81 52         32 $ch[$i]++;
82 52   33     297 $ch[$i]++ # Skip BS, \, ' and " -- using magic nums for speed
      66        
      100        
83             while $ch[$i] == $CH_BS or $ch[$i] == $CH_BSLASH or
84             $ch[$i] == $CH_QUOTE or $ch[$i] == $CH_DQUOTE;
85 52         31 $found++;
86 52         38 last; # Finish as soon as we've incremented something
87             }
88             else {
89 0         0 $ch[$i] = $CH_SPACE; # Skip control chars
90             }
91             }
92 52 50       62 croak "_nextcc() ran out of palette characters" unless $found;
93 52         145 $self->{-cc} = pack "C$self->{-cpp}", @ch;
94             }
95              
96             croak "_nextcc() cpp is too small"
97 35 50       56 if length($self->{-cc}) > $self->{-cpp};
98              
99 35         54 $self->{-cc};
100             }
101              
102              
103             *_add_color = \&_add_colour;
104              
105             sub _add_colour { # Object method
106 38     38   28 my $self = shift;
107             # my $class = ref($self) || $self;
108 38         26 my $colour = shift;
109 38         35 my $lccolour = lc $colour;
110              
111 38 50       65 return $self->{-cindex}{$lccolour} if exists $self->{-cindex}{$lccolour};
112              
113 38 100       76 $self->{-cc} = $self->_nextcc if exists $self->{-palette}{$self->{-cc}};
114 38         92 $self->{-palette}{$self->{-cc}} = { c => $colour };
115 38         62 $self->{-cindex}{$lccolour} = $self->{-cc};
116 38         36 $self->{-ncolours}++;
117              
118 38         177 $self->{-cc};
119             }
120              
121              
122              
123       0     sub DESTROY {
124             ; # Save's time
125             }
126              
127              
128             ### Public methods
129              
130              
131             sub new { # Class and object method
132 8     8 1 2358 my $self = shift;
133 8   66     31 my $class = ref($self) || $self;
134 8 100       14 my $obj = ref $self ? $self : undef;
135 8         18 my %arg = @_;
136              
137             # Defaults
138 8         52 $self = {
139             '-hotx' => $UNSET,
140             '-hoty' => $UNSET,
141             '-cpp' => 1,
142             '-palette' => {},
143             '-cindex' => {},
144             '-pixels' => '',
145             '-comments' => [],
146             '-commentpixel' => '', # Typically /* pixels */
147             '-commentcolour' => '', # Typically /* colors */
148             '-extlines' => [],
149             };
150              
151 8         15 bless $self, $class;
152              
153             # If $obj->new copy original object's data
154 8 100       16 if (defined $obj) {
155 1         2 foreach my $field (@FIELD) {
156 16         58 $self->_set($field, $obj->_get($field));
157             }
158             }
159              
160             # Any options specified override
161 8         15 foreach my $field (@FIELD) {
162 128 100       232 $self->_set($field, $arg{$field}) if defined $arg{$field};
163             }
164              
165 8         23 $self->{-cc} = ' ' x $self->{-cpp};
166              
167 8         23 my $file = $self->get('-file');
168 8 100 66     81 if (defined $file and not $self->{-pixels}) {
169 4 50 66     39 $self->load if ref $file or -r $file;
170             }
171 8 50 66     42 croak "new() `$file' not found or unreadable"
172             if defined $file and not defined $self->get('-width');
173              
174 8         35 foreach my $field (qw(-width -height -cpp)) {
175 24 50       107 croak "new() $field must be set" unless defined $self->get($field);
176             }
177              
178 8 100       60 if (not $self->{-pixels}) {
179             $self->{-pixels} = ' ' x
180 3         57 ($self->{-width} * $self->{-height} * $self->{-cpp});
181 3         8 $self->_add_colour('white');
182             }
183              
184 8         18 $self;
185             }
186              
187              
188             # get() is inherited
189              
190              
191             sub set { # Object method
192 20     20 1 29 my $self = shift;
193             # my $class = ref($self) || $self;
194            
195 20         31 while (@_) {
196 26         35 my $field = shift;
197 26         28 my $val = shift;
198              
199 26 50       33 carp "set() -field has no value" unless defined $val;
200 26 50       66 carp "set() $field is read-only"
201             if $field =~
202             /^-(?:cpp|comments|cindex|ncolours|palette|pixels|
203             width|height|ext(?:name|lines))/ox;
204 26 50 33     55 carp "set() -hotx `$val' is out of range"
      66        
205             if $field eq '-hotx' and ($val < $UNSET or $val >= $self->get('-width'));
206 26 50 33     101 carp "set() -hoty `$val' is out of range"
      66        
207             if $field eq '-hoty' and ($val < $UNSET or $val >= $self->get('-height'));
208              
209 26         74 $self->_set($field, $val);
210             }
211             }
212              
213              
214             sub xy { # Object method
215 64     64 1 239 my $self = shift;
216              
217 64         44 my ($x, $y, $colour) = @_;
218              
219             # xy() is common so we can't afford the expense of method calls
220 64 100       63 if (defined $colour) {
221             substr($self->{-pixels},
222             ($y * $self->{-width} * $self->{-cpp}) + ($x * $self->{-cpp}),
223             $self->{-cpp}) =
224 41   100     106 $self->{-cindex}{lc $colour} || $self->_add_colour($colour);
225             }
226             else {
227             my $cc = substr($self->{-pixels},
228             ($y * $self->{-width} * $self->{-cpp}) + ($x * $self->{-cpp}),
229 23         42 $self->{-cpp});
230             return $self->{-palette}{$cc}{c} ||
231             $self->{-palette}{$cc}{m} ||
232             $self->{-palette}{$cc}{s} ||
233             $self->{-palette}{$cc}{g} ||
234 23   0     56 $self->{-palette}{$cc}{g4};
235             }
236             }
237              
238              
239             sub vec { # Object method
240 5     5 1 46 my $self = shift;
241              
242 5         6 my ($offset, $colour) = @_;
243              
244 5 100       7 if (defined $colour) {
245             substr($self->{-pixels}, $offset, $self->{-cpp}) =
246 1   33     7 $self->{-cindex}{lc $colour} || $self->_add_colour($colour);
247             }
248             else {
249 4         6 my $cc = substr($self->{-pixels}, $offset, $self->{-cpp});
250             return $self->{-palette}{$cc}{c} ||
251             $self->{-palette}{$cc}{m} ||
252             $self->{-palette}{$cc}{s} ||
253             $self->{-palette}{$cc}{g} ||
254 4   0     15 $self->{-palette}{$cc}{g4};
255             }
256             }
257              
258              
259             *rgb2color = \&rgb2colour;
260              
261             sub rgb2colour { # Class or object method
262 0     0 1 0 my $self = shift;
263             # my $class = ref($self) || $self;
264              
265 0         0 sprintf "#%02x%02x%02x", @_;
266             }
267              
268              
269             *add_colors = \&add_colours;
270              
271             sub add_colours { # Object method
272 0     0 1 0 my $self = shift;
273             # my $class = ref($self) || $self;
274              
275 0         0 $self->_add_colour(shift @_) while @_;
276             }
277              
278              
279             *del_color = \&del_colour;
280              
281             sub del_colour { # Object method
282 0     0 1 0 my $self = shift;
283             # my $class = ref($self) || $self;
284 0         0 my $colour = lc shift;
285              
286 0         0 my $cc = $self->{-cindex}{$colour};
287 0 0       0 return undef unless defined $cc; # Colour isn't there to delete
288              
289 0         0 my $cpp = $self->get(-cpp);
290              
291 0         0 for (my $i = 0; $i < length($self->{-pixels}) / $cpp; $i += $cpp) {
292 0 0       0 return 0 if substr($self->{-pixels}, $i, $cpp) eq $cc;
293             }
294              
295 0         0 delete $self->{-palette}{$cc};
296 0         0 delete $self->{-cindex}{$colour};
297 0         0 $self->{-ncolours}--;
298              
299 0         0 1;
300             }
301              
302              
303             sub load { # Object method
304 6     6 1 12 my $self = shift;
305             # my $class = ref($self) || $self;
306              
307 6   66     16 my $file = shift() || $self->get('-file');
308              
309 6 50       39 croak "load() no file specified" unless $file;
310              
311 6         9 $self->set('-file', $file);
312              
313 6         25 my ($width, $height, $ncolours, $cpp, $hotx, $hoty, $extname);
314 6         7 my $next_state = $STATE_START;
315 6         4 my $state = $STATE_START;
316 6         12 my $err = "load() file `$file' ";
317 6         5 my %palette;
318             my $i;
319 6         5 local $_;
320 6         12 my $fh = Symbol::gensym;
321              
322 6 100       59 if( not ref $file ) {
    50          
323 3 50       66 open $fh, $file or croak "load() failed to open `$file': $!" ;
324             }
325             elsif( ref($file) eq 'SCALAR' ) {
326 3 50       7 if( $] >= 5.008001 ) { # 5.8.0 dumps core when using "scalar open"
327 1 50   1   6 eval q{ open $fh, "<", $file } # avoid syntax error with pre-5.6 perls
  1         1  
  1         6  
  3         165  
328             or croak "cannot handle scalar value: $!";
329             }
330             else {
331 0         0 require IO::String;
332 0         0 $fh = IO::String->new( $$file );
333             }
334             }
335             else {
336 0 0       0 seek($file, 0, 0) or croak "load() can't rewind handle for `$file': $!";
337 0         0 $fh = $file;
338             }
339              
340 6         910 $self->{-palette} = {};
341 6         14 $self->{-cindex} = {};
342 6         8 $self->{-comments} = [];
343 6         8 $self->{-extlines} = [];
344 6         8 $self->{-pixels} = '';
345 6         9 $self->{-commentpixel} = '';
346 6         8 $self->{-commentcolour} = '';
347              
348             LINE:
349 6         44 while (<$fh>) {
350             # Blank lines
351 205 50       392 next LINE if /^\s*$/o;
352             # Starting comment
353 205 100       220 if ($state == $STATE_START) {
354 6 50       22 croak "$err does not begin with /* XPM */"
355             unless m,/\*\s*XPM\s*\*/,o;
356 6         5 $state = $STATE_ARRAY;
357 6         15 next LINE;
358             }
359             # Comment only lines
360 199 100       236 if (m,^(\s*/\*.*\*/\s*)$,o) {
361 9         12 my $comment = $1;
362 9 100       29 if ($comment =~ m,^\s*/\*\s*colou?rs?\s*\*/\s*$,o) {
    100          
363 3         13 $self->set(-commentcolour, $comment);
364             }
365             elsif ($comment =~ m,^\s*/\*\s*pixels?\s*\*/\s*$,o) {
366 3         6 $self->set(-commentpixel, $comment);
367             }
368             else {
369 3         3 push @{$self->{-comments}}, $comment;
  3         7  
370             }
371 9         43 next LINE;
372             }
373             # Start of multi-line comment
374 190 50 33     474 if ($state != $STATE_IN_COMMENT and m,^\s*/\*,o) {
375 0         0 push @{$self->{-comments}}, $_;
  0         0  
376 0         0 $next_state = $state; # Remember the state we're due for
377 0         0 $state = $STATE_IN_COMMENT;
378 0         0 next LINE;
379             }
380             # End of multi-line comment
381 190 50       202 if ($state == $STATE_IN_COMMENT) {
382 0         0 push @{$self->{-comments}}, $_;
  0         0  
383 0 0       0 $state = $next_state if m,\*/,o;
384 0         0 next LINE;
385             }
386             # Name of C string
387 190 100       198 if ($state == $STATE_ARRAY) {
388             ## While this line is specified in the xpm.ps document, the libXpm
389             ## library itself seems to ignore the contents of this line
390             ## completely. So Image::Xpm should also do.
391             # croak "$err does not have a proper C array name"
392             # unless /static\s+(?:const\s+)?char\s*\*\s*(?:const\s+)?[A-Za-z0-9_-]+\s*\[\s*\]\s*=\s*\{/o; #}
393 6         6 $state = $STATE_VALUES;
394 6         12 next LINE;
395             }
396             # Values line
397 184 100       206 if ($state == $STATE_VALUES) {
398 6         34 ($width, $height, $ncolours, $cpp, $hotx, $hoty, $extname) =
399             /"\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)
400             (?:\s+(-?\d+)\s+(-?\d+))?(?:\s+(\w+))?\s*"/ox;
401 6 50       13 croak "$err missing width" unless defined $width;
402 6 50       9 croak "$err missing height" unless defined $height;
403 6 50       12 croak "$err missing ncolours" unless defined $ncolours;
404 6 50       9 croak "$err missing cpp" unless defined $cpp;
405 6 50       14 croak "$err zero width is invalid" if $width == 0;
406 6 50       10 croak "$err zero height is invalid" if $height == 0;
407 6 50       7 croak "$err zero ncolours is invalid" if $ncolours == 0;
408 6 50       11 croak "$err zero cpp is invalid" if $cpp == 0;
409 6 50 66     49 if ((defined $hotx and not defined $hoty) or
      66        
      33        
      66        
      33        
410             (defined $hotx and $hotx >= $width) or
411             (defined $hoty and $hoty >= $height)) {
412 0         0 carp "$err deleted invalid hotspot";
413 0         0 $hotx = $hoty = $UNSET;
414             }
415 6 100       10 $hotx = $hoty = $UNSET unless defined $hotx ;
416 6 50       9 carp "$err unusually large cpp `$cpp'" if $cpp > 4;
417 6         11 $self->{-cpp} = $cpp; # Have to do this early as possible.
418 6         4 $i = 0;
419 6         4 $state = $STATE_COLOURS;
420 6         17 next LINE;
421             }
422             # Colour palette
423 178 100       192 if ($state == $STATE_COLOURS) {
424 42         121 /"(.{$cpp})/; #" No /o since this can vary between images!
425 42         49 my $cc = $1;
426 42         115 my %pair = /\s+(m|s|g4|g|c)\s+(#[A-Fa-f\d]{3,}|\w+)/go;
427 42 50       118 $self->{-cindex}{lc $pair{'c'}} = $cc if exists $pair{'c'};
428 42         85 $self->{-palette}{$cc} = { %pair };
429 42         51 $i++;
430 42 50       51 croak "$err palette larger than ncolors" if $i > $ncolours;
431 42 100       44 if ($i == $ncolours) {
432 6         5 $i = 0;
433 6         4 $state = $STATE_PIXELS;
434             }
435 42         104 next LINE;
436             }
437             # Pixels
438 136 100       155 if ($state == $STATE_PIXELS) {
439 131         177 /^\s*"(.*)"/o;
440 131         168 $self->{-pixels} .= $1;
441 131         72 $i++;
442 131 50       156 croak "$err more pixels than height indicates" if $i > $height;
443 131 50       152 $state = defined $extname ? $STATE_EXTENSIONS : $STATE_FINISH
    100          
444             if $i == $height;
445 131         229 next LINE;
446             }
447             # Extensions
448 5 50       8 if ($state == $STATE_EXTENSIONS) {
449 0 0       0 if (/XPMENDEXT/o) {
450 0         0 $state = $STATE_FINISH;
451             }
452             else {
453 0         0 push @{$self->{-extlines}}, $_;
  0         0  
454             }
455 0         0 next LINE;
456             }
457             # Finish
458 5 50       8 if ($state == $STATE_FINISH) {
459             ## The ending brace could also happened in the line before. So don't
460             ## do any checks anymore here.
461             # croak "$err invalid ending" unless /\}\s*;/;
462 5         5 last LINE;
463             }
464             }
465              
466 6 50       28 close $fh or croak "load() failed to close `$file': $!";
467              
468 6 50       6 push @{$self->{-extlines}}, "XPMENDEXT\n" if scalar @{$self->{-extlines}};
  0         0  
  6         25  
469              
470 6         20 $self->_set(-cpp => $cpp);
471 6         28 $self->_set(-width => $width);
472 6         21 $self->_set(-height => $height);
473 6         23 $self->_set(-ncolours => $ncolours);
474 6         20 $self->_set(-extname => $extname);
475              
476 6         29 $self->set(-hotx => $hotx, -hoty => $hoty);
477             }
478              
479              
480             sub save { # Object method
481 2     2 1 56 my $self = shift;
482             # my $class = ref($self) || $self;
483              
484 2   33     5 my $file = shift() || $self->get('-file');
485              
486 2 50       3 croak "save() no file specified" unless $file;
487              
488 2         3 $self->set('-file', $file);
489              
490 2         10 my ($width, $height, $cpp) = $self->get('-width', '-height', '-cpp');
491 2         20 my $line;
492 2         5 my $fh = Symbol::gensym;
493 2 50       140 open $fh, ">$file" or croak "save() failed to open `$file': $!";
494              
495 2         11 $file =~ s,^.*/,,o;
496 2         4 $file =~ s/\.xpm$//o;
497 2         5 $file =~ tr/[-_A-Za-z0-9]/_/c;
498              
499 2         18 print $fh "/* XPM */\nstatic char *", $file, "[] = {\n";
500 2         2 print $fh @{$self->get(-comments)};
  2         7  
501 2         23 $line = qq{"$width $height } . $self->get(-ncolours) . " $cpp "; #"
502 2 50       15 $line .= $self->get(-hotx) . " " . $self->get(-hoty) . " "
503             if $self->get(-hotx) > $UNSET;
504 2 50       16 $line .= $self->get(-extname) if defined $self->get(-extname);
505 2         18 $line =~ s/\s+$//o;
506 2         6 print $fh qq{$line",\n}, $self->get(-commentcolour); #"
507              
508 2         11 while (my ($cc, $pairs) = each (%{$self->{-palette}})) {
  26         58  
509 24         18 $line = qq{"$cc }; #"
510 24         15 foreach my $key (sort keys %{$pairs}) {
  24         33  
511 24         31 $line .= "$key $pairs->{$key} ";
512             }
513 24         44 $line =~ s/\s+$//o;
514 24         27 print $fh qq{$line",\n}; #"
515             }
516              
517 2         5 print $fh $self->get(-commentpixel);
518              
519 2         12 my $comma = ',';
520 2         5 for (my $y = 0; $y < $height; $y++) {
521 53 100       57 $comma = '' if $y == $height - 1;
522             print $fh
523             '"',
524 53         98 substr($self->{-pixels}, $y * $width * $cpp, $width * $cpp),
525             qq{"$comma\n}; #"
526             }
527              
528 2         2 print $fh @{$self->get(-extlines)}, "};\n";
  2         3  
529              
530 2 50       76 close $fh or croak "save() failed to close `$file': $!";
531             }
532              
533              
534             1;
535              
536              
537             __END__