File Coverage

blib/lib/Image/Base/GD.pm
Criterion Covered Total %
statement 61 220 27.7
branch 22 134 16.4
condition 5 54 9.2
subroutine 10 19 52.6
pod 10 11 90.9
total 108 438 24.6


line stmt bran cond sub pod time code
1             # Copyright 2010, 2011, 2012 Kevin Ryde
2              
3             # This file is part of Image-Base-GD.
4             #
5             # Image-Base-GD is free software; you can redistribute it and/or modify
6             # it under the terms of the GNU General Public License as published by the
7             # Free Software Foundation; either version 3, or (at your option) any later
8             # version.
9             #
10             # Image-Base-GD is distributed in the hope that it will be useful, but
11             # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12             # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13             # for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Image-Base-GD. If not, see .
17              
18              
19             package Image::Base::GD;
20 11     11   257360 use 5.006;
  11         44  
  11         434  
21 11     11   64 use strict;
  11         19  
  11         324  
22 11     11   58 use warnings;
  11         24  
  11         385  
23 11     11   53 use Carp;
  11         29  
  11         963  
24              
25 11     11   65 use vars '$VERSION', '@ISA';
  11         17  
  11         717  
26             $VERSION = 15;
27              
28 11     11   19507 use Image::Base 1.12; # version 1.12 for ellipse() $fill
  11         21247  
  11         36402  
29             @ISA = ('Image::Base');
30              
31             # uncomment this to run the ### lines
32             #use Smart::Comments '###';
33              
34              
35             sub new {
36 6     6 1 2076 my ($class, %params) = @_;
37             ### Image-Base-GD new(): %params
38              
39             # $obj->new(...) means make a copy, with some extra settings
40 6 50       25 if (ref $class) {
41 0         0 my $self = $class;
42 0         0 $class = ref $self;
43 0 0       0 if (! defined $params{'-gd'}) {
44 0         0 $params{'-gd'} = $self->get('-gd')->clone;
45             }
46             # inherit everything else
47 0         0 %params = (%$self, %params);
48             ### copy params: \%params
49             }
50              
51 6         47 my $self = bless { -allocate_colours => 1,
52             -zlib_compression => -1,
53             -file_format => 'png' }, $class;
54 6 100       26 if (! defined $params{'-gd'}) {
55 5 100       29 if (defined (my $filename = delete $params{'-file'})) {
56 2         8 $self->load ($filename);
57              
58             } else {
59 3         8 my $truecolor = !! delete $params{'-truecolor'};
60 3         10 my $width = delete $params{'-width'};
61 3         7 my $height = delete $params{'-height'};
62 3         1767 require GD;
63 0   0     0 my $gd = $self->{'-gd'} = GD::Image->new ($width, $height, $truecolor)
64             || croak "Cannot create GD"; # undef if cannot create
65 0         0 $gd->alphaBlending(0);
66             }
67             }
68 1         5 $self->set (%params);
69             ### new made: $self
70 1         4 return $self;
71             }
72              
73             my %attr_to_get_method = (-width => 'width',
74             -height => 'height',
75             -ncolours => 'colorsTotal',
76              
77             # these not documented yet ...
78             -truecolor => 'isTrueColor',
79             -interlaced => 'interlaced');
80             sub _get {
81 0     0   0 my ($self, $key) = @_;
82             ### Image-Base-GD _get(): $key
83              
84 0 0       0 if (my $method = $attr_to_get_method{$key}) {
85 0         0 return $self->{'-gd'}->$method;
86             }
87 0         0 return $self->SUPER::_get ($key);
88             }
89              
90             sub set {
91 3     3 1 9 my ($self, %param) = @_;
92             ### Image-Base-GD set(): \%param
93              
94 3         7 foreach my $key ('-width', '-height', '-ncolours') {
95 9 50       34 if (exists $param{$key}) {
96 0         0 croak "Attribute $key is read-only";
97             }
98             }
99              
100             # these not documented yet ...
101 3 50       15 if (exists $param{'-interlaced'}) {
102 0         0 $self->{'gd'}->interlaced (delete $param{'-interlaced'});
103             }
104 3 50       23 if (exists $param{'-truecolor'}) {
105 0         0 my $gd = $self->{'gd'};
106 0 0       0 if (delete $param{'-truecolor'}) {
107 0 0       0 if (! $gd->isTrueColor) {
108 0         0 die "How to turn palette into truecolor?";
109             }
110             } else {
111 0 0       0 if ($gd->isTrueColor) {
112 0         0 $gd->trueColorToPalette;
113             }
114             }
115             }
116              
117 3         39 %$self = (%$self, %param);
118             }
119              
120             sub load {
121 2     2 1 4 my ($self, $filename) = @_;
122 2 50       8 if (@_ == 1) {
123 0         0 $filename = $self->{'-file'};
124             } else {
125 2         9 $self->set('-file', $filename);
126             }
127              
128 2         4 my $fh;
129 2 50       113 open $fh, '<', $filename
130             or croak "Cannot open file $filename: $!";
131 2 50       11 binmode $fh
132             or croak 'Error setting binary mode: ',$!;
133              
134 2         4 my $filepos = tell($fh);
135 2         4 my $bytes = '';
136 2         45 read($fh,$bytes,9);
137             ### $bytes
138              
139 2         4 my $file_format;
140             my $method;
141 2 50 33     46 if ($bytes =~ /^\x89PNG/) { $file_format = 'png'; }
  0 50 33     0  
    50          
    50          
    50          
    100          
    50          
    50          
142 0         0 elsif ($bytes =~ /^\xFF\xD8/) { $file_format = 'jpeg'; }
143 0         0 elsif ($bytes =~ /^GIF8/) { $file_format = 'gif'; }
144 0         0 elsif ($bytes =~ /^gd2\0/) { $file_format = 'gd2'; }
145 0         0 elsif ($bytes =~ m{^/\* XPM \*/}) { $file_format = 'xpm'; }
146 1         2 elsif ($bytes =~ m/^#define /) { $file_format = 'xbm';
147 1         2 $method = "_newFromXbm"; }
148 0         0 elsif ($bytes =~ m/^\0\0/) { $file_format = 'wbmp';
149 0         0 $method = "_newFromWBMP"; }
150              
151             # Image::WMF (as of 1.01) doesn't have a file reader to then extend perhaps.
152             # elsif ($bytes =~ m/^\327\315\306\232/) {
153             # require Image::WMF;
154             # my $class = 'Image::WMF';
155             # $file_format = 'wmf';
156             # $method = "newFromWMF"; }
157              
158             # GD::SVG (as of 0.33) doesn't have a file reader to then extend perhaps.
159             # elsif ($bytes =~ m/^
160             # require GD::SVG;
161             # my $class = 'GD::SVG::Image';
162             # $file_format = 'svg';
163             # $method = "newFromSVG"; }
164              
165             elsif ($bytes =~ /^\xFF[\xFF\xFE]/
166             || (length($bytes) >= 4
167             && do {
168 1         6 my ($width, $height) = unpack 'nn', $bytes;
169 1         16 -s $fh == 4 + 3 + 256*3 + $width * $height
170             })) {
171 1         3 $file_format = 'gd';
172             } else {
173 0         0 croak "Unrecognised file format";
174             }
175 2   66     11 $method ||= "newFrom\u$file_format";
176             ### $method
177              
178 2         4 my $fh_filename = $filename;
179 2 50 33     35 if ($file_format eq 'xpm' || ! seek($fh,$filepos,0)) {
180 0         0 require File::Temp;
181 0         0 my $tempfh = File::Temp->new (UNLINK => 0);
182 0 0       0 binmode $tempfh or croak 'Error setting binary mode: ',$!;
183              
184 0         0 my $rest = do { local $/; <$fh> }; # slurp
  0         0  
  0         0  
185 0 0       0 print $tempfh $bytes, $rest or croak 'Error writing temp file: ',$!;
186 0 0       0 seek $tempfh, 0, 0 or croak "Error rewinding temp file: $!";
187              
188             # require File::Copy;
189             # File::Copy::copy($fh,$tempfh)
190             # or croak "Error copying $filename: $!";
191             ### input size: -s $fh
192             ### copied size: -s $tempfh
193             ### tell fh: tell($fh)
194             ### tell temp: tell($tempfh)
195              
196 0 0       0 close $fh or croak "Error closing $filename: $!";
197              
198 0         0 $fh = $tempfh;
199 0         0 $fh_filename = $tempfh->filename;
200             }
201             ### tell: tell($fh)
202              
203 2         1198 require GD;
204 0         0 my $gd;
205 0 0       0 if ($file_format eq 'xpm') {
206             # newFromXpm() will only read a filename, not a handle
207             ### newFromXpm(): $fh_filename
208 0         0 $gd = GD::Image->newFromXpm($fh_filename);
209             } else {
210 0         0 $gd = GD::Image->$method($fh);
211             }
212             ### $gd
213              
214 0 0       0 close $fh
215             or croak "Error closing $fh_filename: $!";
216              
217 0 0       0 if (! $gd) {
218 0         0 croak "Unrecognised data or error reading ",$filename;
219              
220             # undef $@;
221             # my $err = $@;
222             # newFromXpm() error message dodgy
223             # if (defined $err) {
224             # croak $err;
225             # } else {
226             # }
227             }
228              
229 0         0 $self->{'-gd'} = $gd;
230 0         0 $self->{'-file_format'} = $file_format;
231 0         0 $gd->alphaBlending(0);
232             }
233              
234             # check -file_format, don't call an arbitrary func/method through its name
235             my %file_format_save_method = (jpeg => 'jpeg',
236             gif => 'gif',
237             gd => 'gd',
238             gd2 => 'gd2',
239             png => 'png',
240             svg => 'svg', # experimental for GD::SVG::Image
241             wmf => 'wmf', # experimental for Image::WMF
242             );
243             my %text_mode = (svg => 1);
244              
245             sub save {
246 0     0 1 0 my ($self, $filename) = @_;
247             ### Image-Base-GD save(): @_
248 0 0       0 if (@_ == 2) {
249 0         0 $self->set('-file', $filename);
250             } else {
251 0         0 $filename = $self->{'-file'};
252             }
253             ### $filename
254              
255 0         0 my $gd = $self->{'-gd'};
256 0         0 my $file_format;
257 0 0       0 if (defined ($file_format = $self->{'-file_format'})) {
258 0         0 $file_format = lc($file_format);
259             } else {
260 0         0 $file_format = 'png'; # default
261             }
262              
263 0         0 my $data;
264 0 0       0 if ($file_format eq 'png') {
    0          
    0          
    0          
265 0         0 $data = $gd->png ($self->get('-zlib_compression'));
266             } elsif ($file_format eq 'jpeg') {
267 0         0 my $quality = $self->get('-quality_percent');
268 0 0       0 $data = $gd->jpeg (defined $quality ? $quality : -1);
269             } elsif ($file_format eq 'wbmp') {
270             # In libgd 2.0.36 gdImageWBMPCtx() the "foreground" index arg becomes
271             # WBMP_BLACK. In WAP world black is the foreground is it? In any case
272             # 'black' here makes save+load of a GD to wbmp come back the right way
273             # around.
274             # http://www.wapforum.org/what/technical/SPEC-WAESpec-19990524.pdf
275             ### wbmp fg: $self->colour_to_index('black')
276 0         0 $data = $gd->wbmp ($self->colour_to_index('black'));
277             } elsif (my $method = $file_format_save_method{$file_format}) {
278 0         0 $data = $gd->$method;
279             } else {
280 0         0 croak 'Cannot save file format ',$file_format;
281             }
282              
283             # or maybe File::Slurp::write_file($filename,{binmode=>':raw'})
284 0         0 my $fh;
285 0 0 0     0 (open $fh, '>', $filename
      0        
      0        
      0        
286             and ($text_mode{$file_format} || binmode($fh))
287             and print $fh $data
288             and close $fh)
289             or croak "Error writing $filename: $!";
290             }
291              
292             #------------------------------------------------------------------------------
293              
294             sub xy {
295 0     0 1 0 my ($self, $x, $y, $colour) = @_;
296             ### Image-Base-GD xy(): $x,$y,$colour
297              
298 0         0 my $gd = $self->{'-gd'};
299 0 0       0 if (@_ == 4) {
300 0         0 $gd->setPixel ($x, $y, $self->colour_to_index($colour));
301             ### setPixel: $self->colour_to_index($colour)
302             } else {
303 0         0 my $pixel = $gd->getPixel ($x, $y);
304             #### getPixel: $pixel
305 0 0       0 if ($pixel == $gd->transparent) {
306             #### is transparent
307 0         0 return 'None';
308             }
309 0 0       0 if ($pixel >= 0x7F000000) {
310             #### pixel has fully-transparent alpha 0x7F
311 0         0 return 'None';
312             }
313             #### rgb: $gd->rgb($pixel)
314 0         0 return sprintf ('#%02X%02X%02X', $gd->rgb($pixel));
315             }
316             }
317              
318             sub line {
319 0     0 1 0 my ($self, $x1, $y1, $x2, $y2, $colour) = @_;
320             ### Image-Base-GD line(): @_
321              
322 0         0 $self->{'-gd'}->line ($x1,$y1,$x2,$y2, $self->colour_to_index($colour));
323             }
324              
325             sub rectangle {
326 0     0 1 0 my ($self, $x1, $y1, $x2, $y2, $colour, $fill) = @_;
327             ### Image-Base-GD rectangle(): @_[1..$#_]
328              
329             # ### index: $self->colour_to_index($colour)
330              
331             # libgd circa 2.0.35 gdImageFilledRectangle() has a bug where if the x1,x2
332             # range is all negative then it draws a pixels in the x=0 left edge. Or
333             # similarly if y1,y2 all negative then it draws in the y=0 top edge.
334             # Think this is a bug, the comments in the code suggest it's supposed to
335             # drawn nothing for all-negative. In any case avoid this in the interests
336             # of behaving like other Image-Base new style clipping 0,0,width,height.
337             #
338 0 0 0     0 if ($x2 < 0 || $y2 < 0) {
339             ### all negative, workaround to drawn nothing ...
340 0         0 return;
341             }
342              
343             # libgd circa 2.0.35 has a bug where it draws a $y1==$y2 unfilled
344             # rectangle with dodgy sides like
345             #
346             # * *
347             # ********
348             # * *
349             #
350             # As a workaround send $y1==$y2 to filledRectangle() instead.
351             #
352 0 0 0     0 my $method = ($fill || $y1 == $y2
353             ? 'filledRectangle'
354             : 'rectangle');
355 0         0 $self->{'-gd'}->$method ($x1,$y1,$x2,$y2, $self->colour_to_index($colour));
356             }
357              
358             sub ellipse {
359 0     0 1 0 my ($self, $x1, $y1, $x2, $y2, $colour, $fill) = @_;
360             ### Image-GD ellipse: "$x1, $y1, $x2, $y2, $colour, ".($fill||0)
361              
362             # If width $xw or height $yw is an odd number then GD draws the extra
363             # pixel on the higher value side, ie. the centre is the rounded-down
364             # position. Dunno if that should be relied on.
365             #
366             # some versions of libgd prior to 2.0.36 seem to draw nothing for
367             # filledEllipse() on an x1==x2 y1==y2 single-pixel ellipse. Try sending 1
368             # or 2 pixel wide or high to the base ellipse() and from there to
369             # filledRectangle() instead.
370             #
371 0         0 my $xw = $x2 - $x1;
372 0         0 my $yw = $y2 - $y1;
373 0         0 my $gd = $self->{'-gd'};
374 0 0 0     0 if ($gd->isa('GD::SVG::Image')
      0        
      0        
      0        
375             || ($xw > 1 && ! ($xw & 1)
376             && $yw > 1 && ! ($yw & 1))) {
377             ### x centre: $x1 + $xw/2
378             ### y centre: $y1 + $yw/2
379             ### $xw+1
380             ### $yw+1
381 0 0       0 my $method = ($fill ? 'filledEllipse' : 'ellipse');
382 0         0 $gd->$method ($x1 + $xw/2, $y1 + $yw/2,
383             $xw+1, $yw+1,
384             $self->colour_to_index($colour));
385             } else {
386             ### use Image-Base by pixels ...
387 0         0 shift->SUPER::ellipse(@_);
388             }
389             }
390              
391             sub diamond {
392 0     0 1 0 my ($self, $x1, $y1, $x2, $y2, $colour, $fill) = @_;
393             ### Image-Base-GD diamond() ...
394              
395 0         0 my $gd = $self->{'-gd'};
396 0 0 0     0 if ($x1 == $x2 || $y1 == $y2) {
397             # as of libgd 2.0.36 a filledpolygon of 1x1 or Nx1 draws no pixels, go
398             # to rectangle in that case
399 0         0 $gd->filledRectangle ($x1,$y1,$x2,$y2,
400             $self->colour_to_index($colour));
401              
402             } else {
403 0         0 my $xh = ($x2 - $x1);
404 0         0 my $yh = ($y2 - $y1);
405 0         0 my $xeven = ($xh & 1);
406 0         0 my $yeven = ($yh & 1);
407 0         0 $xh = int($xh / 2);
408 0         0 $yh = int($yh / 2);
409             ### assert: $x1+$xh+$xeven == $x2-$xh
410             ### assert: $y1+$yh+$yeven == $y2-$yh
411              
412 0         0 my $poly = GD::Polygon->new;
413 0         0 $poly->addPt ($x1+$xh,$y1); # top centre
414              
415             # left
416 0         0 $poly->addPt ($x1,$y1+$yh);
417 0 0       0 if ($yeven) { $poly->addPt ($x1,$y2-$yh); }
  0         0  
418              
419             # bottom
420 0         0 $poly->addPt ($x1+$xh,$y2);
421 0 0       0 if ($xeven) { $poly->addPt ($x2-$xh,$y2); }
  0         0  
422              
423             # right
424 0 0       0 if ($yeven) { $poly->addPt ($x2,$y2-$yh); }
  0         0  
425 0         0 $poly->addPt ($x2,$y1+$yh);
426              
427             # top again
428 0 0       0 if ($xeven) { $poly->addPt ($x2-$xh,$y1); }
  0         0  
429              
430             ### $poly
431 0 0       0 my $method = ($fill ? 'filledPolygon' : 'openPolygon');
432 0         0 $gd->$method ($poly, $self->colour_to_index($colour));
433             }
434             }
435              
436             sub add_colours {
437 0     0 1 0 my $self = shift;
438             ### add_colours: @_
439              
440 0         0 my $gd = $self->{'-gd'};
441 0 0       0 if ($gd->isTrueColor) {
442             ### no allocation in truecolor
443 0         0 return;
444             }
445              
446 0         0 foreach my $colour (@_) {
447             ### $colour
448 0 0       0 if ($colour eq 'None') {
449 0 0       0 if ($gd->transparent() != -1) {
450             ### transparent already: $gd->transparent()
451 0         0 next;
452             }
453 0 0       0 if ((my $index = $self->{'-gd'}->colorAllocateAlpha(0,0,0,127)) != -1) {
454 0         0 $gd->transparent ($index);
455             ### transparent now: $gd->transparent
456 0         0 next; # successful
457             }
458              
459             } else {
460 0         0 my @rgb = _colour_to_rgb255($colour);
461 0 0 0     0 if ($gd->can('colorExact') # not available in Image::WMF
462             && $gd->colorExact(@rgb) != -1) {
463             ### already exists: $gd->colorExact(@rgb)
464 0         0 next;
465             }
466 0 0       0 if ($gd->colorAllocate(@rgb) != -1) {
467             ### allocated
468 0         0 next;
469             }
470             }
471 0         0 croak "Cannot allocate colour: $colour";
472             }
473             }
474              
475             # not documented yet ...
476             sub colour_to_index {
477 0     0 0 0 my ($self, $colour) = @_;
478             ### Image-Base-GD colour_to_index(): $colour
479 0         0 my $gd = $self->{'-gd'};
480             # while ($gd->isa('GD::Window')) {
481             # $gd = $gd->{im};
482             # }
483              
484 0 0       0 if ($colour eq 'None') {
485 0 0       0 if ($gd->isTrueColor) {
486             ### truecolor transparent: $gd->colorAllocateAlpha(0,0,0,127)
487 0         0 return $gd->colorAllocateAlpha(0,0,0,127);
488             }
489              
490             # Crib note: gdImageColorExactAlpha() doesn't take the single
491             # transparent() colour as equivalent to all transparents but instead
492             # looks for R,G,B to match as well as the alpha.
493             #
494 0 0       0 if ((my $index = $gd->transparent) != -1) {
495             ### existing palette transparent: $index
496 0         0 return $index;
497             }
498 0 0       0 if (! $self->{'-allocate_colours'}) {
499 0         0 croak "No transparent index set";
500             }
501 0 0       0 if ((my $index = $self->{'-gd'}->colorAllocate(0,0,0)) != -1) {
502 0         0 $gd->transparent ($index);
503             ### transparent now: $gd->transparent
504 0         0 return $index;
505             }
506 0         0 croak "No colour cells free to create transparent";
507             }
508              
509 0         0 my @rgb = _colour_to_rgb255($colour);
510             ### @rgb
511 0 0       0 if ($self->{'-allocate_colours'}) {
512 0 0 0     0 if ($gd->can('colorExact') # not available in Image::WMF
513             && (my $index = $gd->colorExact (@rgb)) != -1) {
514             ### existing exact: $index
515 0         0 return $index;
516             }
517 0 0       0 if ((my $index = $gd->colorAllocate (@rgb)) != -1) {
518             ### allocate: $index
519 0         0 return $index;
520             }
521             }
522             ### closest: $gd->colorClosest(@rgb)
523 0         0 return $gd->colorClosest (@rgb);
524             }
525              
526             sub _colour_to_rgb255 {
527 10     10   5851 my ($colour) = @_;
528             # Crib: [:xdigit:] matches some wide chars, but hex() as of perl 5.12.4
529             # doesn't accept them, so only 0-9A-F
530 10 50       51 if ($colour =~ /^#(([0-9A-F]{3}){1,4})$/i) {
531 10         38 my $len = length($1)/3; # of each group, so 1,2,3 or 4
532 10         35 return (map {hex(substr($_ x 2, 0, 2))} # first 2 chars of replicated
  30         107  
533             substr ($colour, 1, $len), # full size groups
534             substr ($colour, 1+$len, $len),
535             substr ($colour, -$len));
536             }
537 0           require GD::Simple;
538 0 0         if (defined (my $aref = GD::Simple->color_names->{lc($colour)})) {
539             ### table: $aref
540 0           return @$aref;
541             }
542 0           croak "Unknown colour: $colour";
543             }
544              
545             1;
546             __END__