File Coverage

blib/lib/GD/Cairo.pm
Criterion Covered Total %
statement 37 39 94.8
branch n/a
condition n/a
subroutine 13 13 100.0
pod n/a
total 50 52 96.1


line stmt bran cond sub pod time code
1             package GD::Cairo;
2              
3 1     1   24221 use 5.006;
  1         4  
  1         42  
4 1     1   7 use strict;
  1         2  
  1         41  
5 1     1   5 use warnings;
  1         2  
  1         34  
6              
7             require Exporter;
8 1     1   784 use Encode;
  1         242001  
  1         246  
9              
10             our @ISA = qw(Exporter);
11              
12             # Items to export into callers namespace by default. Note: do not export
13             # names by default without a very good reason. Use EXPORT_OK instead.
14             # Do not simply export all your public functions/methods/constants.
15              
16             # This allows declaration use GD::Cairo ':all';
17             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
18             # will save memory.
19             our %EXPORT_TAGS = ( 'gd' => [ qw(
20             gdBrushed
21             gdDashSize
22             gdMaxColors
23             gdStyled
24             gdStyledBrushed
25             gdTiled
26             gdTransparent
27             gdAntiAliased
28             gdArc
29             gdChord
30             gdPie
31             gdNoFill
32             gdEdged
33             gdAlphaMax
34             gdAlphaOpaque
35             gdAlphaTransparent
36             gdTinyFont
37             gdSmallFont
38             gdMediumBoldFont
39             gdLargeFont
40             gdGiantFont
41             ) ] );
42              
43             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'gd'} } );
44              
45             our @EXPORT = qw(
46             );
47              
48             our $VERSION = '0.01';
49              
50 1     1   10 use constant PI => 4 * atan2 1, 1;
  1         3  
  1         71  
51 1     1   5 use constant PI_2 => 8 * atan2 1, 1;
  1         1  
  1         46  
52              
53 1     1   5 use constant GC_FONT_SLANT_NORMAL => 'normal';
  1         2  
  1         41  
54 1     1   6 use constant GC_FONT_SLANT_ITALIC => 'italic';
  1         2  
  1         58  
55 1     1   6 use constant GC_FONT_SLANT_OBLIQUE => 'oblique';
  1         2  
  1         47  
56              
57 1     1   25 use constant GC_FONT_WEIGHT_NORMAL => 'normal';
  1         2  
  1         44  
58 1     1   6 use constant GC_FONT_WEIGHT_BOLD => 'bold';
  1         2  
  1         97  
59              
60             use constant {
61 1         206 'gdAntiAliased' => -7,
62             'gdTransparent' => -6,
63             'gdTiled' => -5,
64             'gdStyledBrushed' => -4,
65             'gdBrushed' => -3,
66             'gdStyled' => -2,
67             'gdDashSize' => 4,
68             'gdMaxColors' => 256,
69             'gdArc' => 0,
70             'gdPie' => 0,
71             'gdChord' => 1,
72             'gdNoFill' => 2,
73             'gdEdged' => 4,
74             'gdAlphaMax' => 127,
75             'gdAlphaOpaque' => 0,
76             'gdAlphaTransparent' => 127,
77 1     1   5 };
  1         2  
78              
79 1     1   2194 use Cairo;
  0            
  0            
80             use Data::Dumper;
81              
82             our $EXTENTS_SELF;
83             our $TRUECOLOR = 0;
84             our $ANTIALIAS = 0;
85              
86             use vars qw( $AUTOLOAD );
87              
88             # Preloaded methods go here.
89              
90             sub _new
91             {
92             my( $class, @opts ) = @_;
93              
94             my $self = bless {
95             background_color => undef,
96             colors => [],
97             operations => [],
98             transparent => undef,
99             thickness => 1,
100             brush => undef,
101             style => {},
102             }, $class;
103             }
104              
105             sub newFromSurface
106             {
107             my( $class, $surface ) = @_;
108              
109             my $self = $class->_new();
110              
111             $self->{surface} = $surface;
112              
113             $self->{context} = Cairo::Context->create( $surface );
114             $self->{context}->set_line_width( $self->{thickness} );
115              
116             $self->{width} = $surface->get_width;
117             $self->{height} = $surface->get_height;
118              
119             $EXTENTS_SELF = $self;
120              
121             return $self;
122             }
123              
124             sub new
125             {
126             my( $class, $w, $h, $truecolor ) = @_;
127              
128             $truecolor = $TRUECOLOR if scalar(@_) == 3;
129             my $format = $truecolor ? 'argb32' : 'a8';
130             $format = 'argb32';
131              
132             my $surface = Cairo::ImageSurface->create( $format, $w, $h );
133              
134             return $class->newFromSurface( $surface );
135             }
136              
137             sub newFromPngData
138             {
139             my( $class, $data, $truecolor ) = @_;
140              
141             pos($data) = 0;
142             my $surface = Cairo::ImageSurface->create_from_png_stream(sub {
143             my( $closure, $length ) = @_;
144             use bytes;
145             my $buffer = substr($data,pos($data),$length);
146             pos($data) += $length;
147             return $buffer;
148             });
149              
150             return $class->newFromSurface( $surface );
151             }
152              
153             sub getCairoContext
154             {
155             $_[0]->{context};
156             }
157              
158             sub getCairoImageSurface
159             {
160             $_[0]->{surface};
161             }
162              
163             sub getCairoPattern
164             {
165             $_[0]->{brush};
166             }
167              
168             sub trueColor
169             {
170             my( $self, $truecolor ) = @_;
171              
172             $TRUECOLOR = $truecolor;
173             }
174              
175             sub newPalette
176             {
177             my( $class, $w, $h ) = @_;
178              
179             # my $surface = Cairo::ImageSurface->create( 'a8', $w, $h );
180             my $surface = Cairo::ImageSurface->create( 'argb32', $w, $h );
181              
182             return $class->newFromSurface( $surface );
183             }
184              
185             sub newTrueColor
186             {
187             my( $class, $w, $h ) = @_;
188              
189             my $surface = Cairo::ImageSurface->create( 'argb32', $w, $h );
190              
191             return $class->newFromSurface( $surface );
192             }
193              
194             sub ignoreMissing
195             {
196             my( $warn ) = @_;
197              
198             if( $warn )
199             {
200             *AUTOLOAD = sub {
201             $AUTOLOAD =~ s/^.*:://;
202             return if $AUTOLOAD =~ /^[A-Z]/;
203             Carp::carp "I don't know how to '$AUTOLOAD' - it may be supported in GD but isn't in the GD::Cairo wrapper. You may need to fix this";
204             };
205             }
206             else
207             {
208             *AUTOLOAD = sub {}
209             }
210             }
211              
212             sub _color
213             {
214             my( $self, $index ) = @_;
215              
216             my $color;
217              
218             if( $index == gdAntiAliased )
219             {
220             Carp::croak "You must call setAntiAliased before using gdAntiAliased"
221             unless defined $self->{antialiased};
222             $color = $self->{antialiased};
223             }
224             else
225             {
226             $color = $self->{colors}->[$index]
227             or Carp::croak "Invalid color $index - perhaps you need to call colorAllocate";
228             }
229              
230             return $color;
231             }
232              
233             sub _color_to_index
234             {
235             my( $self, $color ) = @_;
236              
237             my $i = 0;
238             for(@{$self->{colors}})
239             {
240             return $i if( _color_eq( $color, $_ ) );
241             ++$i;
242             }
243              
244             die "No color allocated for [".join(',',@$color)."]";
245             }
246              
247             sub _color_index_to_role
248             {
249             my( $self, $index, $x, $y ) = @_;
250              
251             if( $index == gdBrushed or $index == gdTiled )
252             {
253             $x ||= 0;
254             $y ||= 0;
255             unless( defined $self->{brush} )
256             {
257             Carp::croak "Can't use gdBrushed without first calling setBrush";
258             }
259             my $w = $self->{brush}->width;
260             my $h = $self->{brush}->height;
261             my $thickness = $w > $h ? $w : $h;
262             my $style = gdBrushed == $index ? 'repeat' : 'repeat';
263             return
264             set_source_surface => [$self->{brush}->{surface}, $x, $y],
265             set_line_width => [$thickness],
266             sub {
267             my( $cr ) = @_;
268             my $pattern = $cr->get_source;
269             $pattern->set_filter( 'bilinear' );
270             $pattern->set_extend( $style );
271             } => [];
272             }
273             elsif( $index == gdStyled )
274             {
275             Carp::croak "Can only apply gdStyled to lines";
276             }
277             elsif( $index == gdAntiAliased )
278             {
279             return
280             set_source_rgba => $self->_color( $index ),
281             set_antialias => ['default'],
282             set_line_width => [$self->{thickness}];
283             }
284             else
285             {
286             return
287             set_source_rgba => $self->_color( $index ),
288             set_antialias => ['none'],
289             set_line_width => [$self->{thickness}];
290             }
291             }
292              
293             sub _color_eq
294             {
295             for(0..3) { return 0 if $_[0]->[$_] != $_[1]->[$_] };
296             return 1;
297             }
298              
299             sub _shape_color
300             {
301             my( $self, $shape ) = @_;
302              
303             for(my $i = 0; $i < @$shape; $i+=2)
304             {
305             if( $shape->[$i] eq 'set_source_rgba' )
306             {
307             return $shape->[$i+1];
308             }
309             }
310              
311             return undef;
312             }
313              
314             *GD::Cairo::colorAllocateAlpha = \&colorAllocate;
315             *GD::Cairo::colorClosest = \&colorAllocate;
316             *GD::Cairo::colorExact = \&colorAllocate;
317             *GD::Cairo::colorResolve = \&colorAllocate;
318             sub colorAllocate
319             {
320             my( $self, $red, $green, $blue, $alpha ) = @_;
321              
322             $red /= 255;
323             $green /= 255;
324             $blue /= 255;
325             $alpha = @_ == 4 ? 1 : (1 - $alpha / 127);
326              
327             for(my $i = 0; $i < @{$self->{colors}}; ++$i)
328             {
329             my @color = @{$self->{colors}->[$i]};
330             if( $color[0] == $red and $color[1] == $green and $color[2] == $blue and $color[3] == $alpha )
331             {
332             return $i;
333             }
334             }
335              
336             push @{$self->{colors}}, [$red, $green, $blue, $alpha];
337              
338             return $#{$self->{colors}};
339             }
340              
341             sub colorDeallocate
342             {
343             my( $self, $color ) = @_;
344              
345             # Unimplemented
346             }
347              
348             sub colorsTotal
349             {
350             my( $self ) = @_;
351              
352             if( $self->isTrueColor )
353             {
354             return undef;
355             }
356             else
357             {
358             return scalar(@{$self->{colors}});
359             }
360             }
361              
362             sub _in_shape
363             {
364             my( $self, $x, $y ) = @_;
365              
366             my $cr = $self->{context};
367              
368             my $i = -1;
369             my $shape;
370             my $color;
371              
372             for($i = $#{$self->{operations}}; $i > -1; --$i, undef $color)
373             {
374             $shape = $self->{operations}->[$i];
375             $cr->save;
376             for(my $j = 0; $j < @$shape; $j+=2)
377             {
378             my( $f, $opts ) = @$shape[$j,$j+1];
379             if( $f eq 'fill' or $f eq 'stroke' or $f eq 'paint' )
380             {
381             }
382             elsif( $f eq 'set_source_rgba' )
383             {
384             $color = $opts;
385             }
386             elsif( ref($f) eq 'CODE' )
387             {
388             }
389             else
390             {
391             $cr->$f( @$opts );
392             }
393             }
394             my $in_fill = $cr->in_fill( $x, $y );
395             $cr->restore;
396             last if $in_fill;
397             }
398              
399             if( $i != -1 )
400             {
401             return $i, $shape, $color;
402             }
403             else
404             {
405             return ();
406             }
407             }
408              
409             sub _convert_style_to_dashes
410             {
411             my( $self, @colors ) = @_;
412              
413             my %lines;
414             my %components = map({ ($_ == gdTransparent) ? () : ($_ => 1) } @colors);
415              
416             foreach my $color (keys %components)
417             {
418             my $dash_map = join '', map({ $_ == $color ? 1 : 0 } @colors);
419             my @opts = (0); # dash offset
420             while(length($dash_map))
421             {
422             if( $dash_map =~ s/^(1+)// )
423             {
424             push @opts, length($1);
425             }
426             if( $dash_map =~ s/^(0+)// )
427             {
428             push @opts, length($1);
429             }
430             }
431             unshift @opts, 0 if $colors[0] != $color; # gap or color first
432              
433             $lines{$color} = \@opts;
434             }
435              
436             return %lines;
437             }
438              
439             sub _set_brush
440             {
441             my( $self, $shape, $index, %opts ) = @_;
442              
443             my $x = exists($opts{x}) ? $opts{x} : 0;
444             my $y = exists($opts{y}) ? $opts{y} : 0;
445             unless( defined $self->{brush} )
446             {
447             Carp::croak "Can't use gdBrushed without first calling setBrush";
448             }
449             my $w = $self->{brush}->width;
450             my $h = $self->{brush}->height;
451             my $thickness = $w > $h ? $w : $h;
452             my $style = gdBrushed == $index ? 'repeat' : 'repeat';
453             unshift @$shape,
454             set_source_surface => [$self->{brush}->{surface}, $x, $y],
455             set_line_width => [$thickness],
456             sub {
457             my( $cr ) = @_;
458             my $pattern = $cr->get_source;
459             $pattern->set_filter( 'bilinear' );
460             $pattern->set_extend( $style );
461             } => [];
462             }
463              
464             sub _stroke_shape
465             {
466             my( $self, $shape, $index, %opts ) = @_;
467              
468             my $antialias = defined($opts{'antialias'}) ?
469             $opts{'antialias'} :
470             ($index == gdAntiAliased or $ANTIALIAS) ? 'default' : 'none';
471              
472             if( $index == gdBrushed or $index == gdTiled )
473             {
474             $self->_set_brush( $shape, $index, %opts );
475             }
476             elsif( $index == gdStyled )
477             {
478             unless( scalar(keys %{$self->{style}}) > 0 )
479             {
480             Carp::croak "Can't use gdStyled without first calling setStyle";
481             }
482              
483             while(my( $color, $dashes ) = each %{$self->{style}})
484             {
485             my @new_shape = @$shape;
486             unshift @new_shape,
487             set_source_rgba => $self->_color( $color ),
488             set_dash => $dashes,
489             set_line_width => [$self->{thickness}],
490             set_antialias => [$antialias];
491             push @new_shape, stroke => [];
492              
493             push @{$self->{operations}}, \@new_shape;
494             }
495              
496             return; # Don't add $shape
497             }
498             else
499             {
500             unshift @$shape,
501             set_source_rgba => $self->_color( $index ),
502             set_antialias => [$antialias],
503             set_line_width => [$self->{thickness}];
504             }
505              
506             push @$shape, stroke => [];
507              
508             push @{$self->{operations}}, $shape;
509             }
510              
511             sub _fill_shape
512             {
513             my( $self, $shape, $index, %opts ) = @_;
514              
515             my $antialias = defined($opts{'antialias'}) ?
516             $opts{'antialias'} :
517             ($index == gdAntiAliased or $ANTIALIAS) ? 'default' : 'none';
518              
519             if( $index == gdBrushed or $index == gdTiled )
520             {
521             $self->_set_brush( $shape, $index, %opts );
522             }
523             elsif( $index == gdStyled )
524             {
525             Carp::croak "Can only apply gdStyled to lines";
526             }
527             else
528             {
529             unshift @$shape,
530             set_source_rgba => $self->_color( $index ),
531             set_antialias => [$antialias];
532             }
533              
534             push @$shape, fill => [];
535              
536             push @{$self->{operations}}, $shape;
537             }
538              
539             sub _paint_shape
540             {
541             my( $self, $shape, $index, %opts ) = @_;
542              
543             if( $index == gdBrushed or $index == gdTiled )
544             {
545             $self->_set_brush( $shape, $index, %opts );
546             }
547             elsif( $index == gdStyled )
548             {
549             Carp::croak "Can only apply gdStyled to lines";
550             }
551             else
552             {
553             unshift @$shape, set_source_rgba => $self->_color( $index );
554             }
555              
556             push @$shape, paint => [];
557              
558             push @{$self->{operations}}, $shape;
559             }
560              
561             sub fill
562             {
563             my( $self, $x, $y, $color ) = @_;
564              
565             my $cr = $self->{context};
566              
567             # Background
568             if( 0 == scalar @{$self->{operations}} )
569             {
570             $self->{background_color} = $self->_color( $color );
571             }
572             # Find the first shape that contains $x,$y
573             # If it's a stroke then 'fill' it by adding the fill behind, otherwise
574             # replace it with the new color
575             elsif( my( $i, $shape, $shape_color ) = $self->_in_shape( $x, $y ) )
576             {
577             my @new_shape;
578             my $stroked = 0;
579             for(my $j = 0; $j < @$shape; $j+=2)
580             {
581             my( $f, $opts ) = @$shape[$j,$j+1];
582             if( $f eq 'stroke' )
583             {
584             $stroked = 1;
585             }
586             elsif(
587             $f eq 'stroke' or
588             $f eq 'fill' or
589             $f eq 'set_source_rgba' or
590             $f eq 'set_source_surface' )
591             {
592             }
593             else
594             {
595             push @new_shape, $f => $opts;
596             }
597             }
598             $self->_fill_shape( \@new_shape, $color );
599             if( $stroked )
600             {
601             splice(@{$self->{operations}},$i,0,pop @{$self->{operations}});
602             }
603             else
604             {
605             splice(@{$self->{operations}},$i,1,pop @{$self->{operations}});
606             }
607             }
608             }
609              
610             sub getPixel
611             {
612             my( $self, $x, $y ) = @_;
613              
614             my $color;
615              
616             # Try finding the pixel in a shape
617             if( my( $i, $shape, $c ) = $self->_in_shape( $x, $y ) )
618             {
619             $color = $c;
620             }
621             # See if they setPixel this pixel
622             elsif( exists $self->{pixels}->{"${x}x${y}"} )
623             {
624             return $self->{pixels}->{"${x}x${y}"};
625             }
626             # Or the background
627             elsif( defined $self->{background_color} )
628             {
629             $color = $self->{background_color};
630             }
631             # GetPixel must return something
632             else
633             {
634             $color = $self->{colors}->[0];
635             }
636              
637             return $self->_color_to_index( $color );
638             }
639              
640             sub setPixel
641             {
642             my( $self, $x, $y, $color ) = @_;
643              
644             if( $color == gdBrushed )
645             {
646             my $w = $self->{brush}->width;
647             my $h = $self->{brush}->height;
648             $self->copy( $self->{brush}, $x - $w/2, $y - $h/2, 0, 0, $w, $h );
649             }
650             else
651             {
652             $self->{pixels}->{"${x}x${y}"} = $color;
653             push @{$self->{operations}}, [
654             set_source_rgba => $self->_color( $color ),
655             set_line_width => [1],
656             set_antialias => ['none'],
657             move_to => [$x-1,$y],
658             line_to => [$x,$y],
659             stroke => []
660             ];
661             }
662             }
663              
664             sub rgb
665             {
666             my( $self, $index ) = @_;
667              
668             return map { sprintf("%.0f", $_ * 255) } @{$self->{colors}->[$index]}[0..2];
669             }
670              
671             sub transparent
672             {
673             my( $self, $index ) = @_;
674              
675             if( 1 == @_ )
676             {
677             return defined $self->{transparent} ?
678             $self->_color_to_index( $self->{transparent} ) :
679             -1;
680             }
681              
682             return $self->{transparent} = $index > -1 ?
683             $self->{colors}->[$index] :
684             -1;
685             }
686              
687             *setTile = \&setBrush;
688             sub setBrush
689             {
690             my( $self, $image ) = @_;
691              
692             unless( $image->isa( 'GD::Cairo' ) )
693             {
694             $image = GD::Cairo->newFromPngData( $image->png );
695             }
696             $self->{brush} = $image;
697             }
698              
699             sub setStyle
700             {
701             my( $self, @colors ) = @_;
702              
703             my %lines = $self->_convert_style_to_dashes( @colors );
704              
705             $self->{style} = \%lines;
706             }
707              
708             sub setThickness
709             {
710             my( $self, $thickness ) = @_;
711              
712             $self->{thickness} = $thickness;
713             }
714              
715             sub setAntiAliased
716             {
717             my( $self, $color ) = @_;
718              
719             $self->{antialiased} = $self->_color( $color );
720             }
721              
722             sub rectangle
723             {
724             my( $self, $x, $y, $x2, $y2, $color ) = @_;
725              
726             my $shape = [
727             rectangle => [$x, $y, $x2-$x, $y2-$y],
728             ];
729              
730             $self->_stroke_shape( $shape, $color,
731             x => $x,
732             y => $y,
733             antialias => 'none'
734             );
735             }
736              
737             sub filledRectangle
738             {
739             my( $self, $x, $y, $x2, $y2, $color ) = @_;
740              
741             my $shape = [
742             rectangle => [$x, $y, $x2-$x, $y2-$y],
743             ];
744              
745             $self->_fill_shape( $shape, $color,
746             x => $x,
747             y => $y,
748             antialias => 'none'
749             );
750             }
751              
752             sub _polygon
753             {
754             my( $self, $polygon, $color ) = @_;
755              
756             my @shape = (move_to => [$polygon->getPt(0)]);
757            
758             my(undef, @vertices) = $polygon->vertices;
759             push @shape, line_to => $_ for @vertices;
760              
761             return \@shape;
762             }
763              
764             # I think polygon is a synonym of openPolygon?
765             *polygon = \&openPolygon;
766             sub openPolygon
767             {
768             my( $self, $polygon, $color ) = @_;
769              
770             my $shape = _polygon( @_ );
771              
772             push @$shape, close_path => [];
773              
774             $self->_stroke_shape( $shape, $color );
775             }
776              
777             sub unclosedPolygon
778             {
779             my( $self, $polygon, $color ) = @_;
780              
781             my $shape = _polygon( @_ );
782              
783             $self->_stroke_shape( $shape, $color );
784             }
785              
786             sub filledPolygon
787             {
788             my( $self, $polygon, $color ) = @_;
789              
790             my $shape = _polygon( @_ );
791              
792             push @$shape, close_path => [];
793              
794             $self->_fill_shape( $shape, $color );
795             }
796              
797             sub line
798             {
799             my( $self, $x, $y, $x2, $y2, $color ) = @_;
800              
801             if( abs($x2-$x) < 1 and abs($y2-$y) < 1 )
802             {
803             return $self->setPixel( $x, $y, $color );
804             }
805              
806             my $shape = [
807             new_path => [],
808             move_to => [$x, $y],
809             line_to => [$x2, $y2]
810             ];
811              
812             my $antialias = ($x == $x2 or $y == $y2) ? 'none' : undef;
813              
814             $self->_stroke_shape( $shape, $color,
815             x => $x,
816             y => $y,
817             antialias => $antialias
818             );
819             }
820              
821             sub _ellipse
822             {
823             my( $self, $x, $y, $w, $h, $color ) = @_;
824              
825             my $s = 0;
826             my $e = PI_2;
827              
828             [
829             save => [],
830             translate => [$x - .5, $y],
831             scale => [$w/2 - .5, $h/2],
832             arc => [0, 0, 1, $s, $e ],
833             close_path => [],
834             restore => [],
835             ];
836             }
837              
838             sub ellipse
839             {
840             my( $self, $x, $y, $w, $h, $color ) = @_;
841              
842             return unless $w > 0 and $h > 0;
843              
844             my $shape = _ellipse( @_ );
845              
846             $self->_stroke_shape( $shape, $color,
847             x => $x,
848             y => $y
849             );
850             }
851              
852             sub filledEllipse
853             {
854             my( $self, $x, $y, $w, $h, $color ) = @_;
855              
856             return unless $w > 0 and $h > 0;
857              
858             my $shape = _ellipse( @_ );
859              
860             $self->_fill_shape( $shape, $color,
861             x => $x,
862             y => $y
863             );
864             }
865              
866             sub _arc
867             {
868             my( $self, $x, $y, $w, $h, $s, $e, $color ) = @_;
869              
870             $s = $s/180*PI;
871             $e = $e/180*PI;
872              
873             [
874             save => [],
875             translate => [$x - .5, $y],
876             scale => [$w/2 - .5, $h/2],
877             arc => [0, 0, 1, $s, $e ],
878             restore => [],
879             ];
880             }
881              
882             sub arc
883             {
884             my( $self, $x, $y, $w, $h, $s, $e, $color ) = @_;
885              
886             return unless $w > 0 and $h > 0;
887              
888             my $shape = _arc( @_ );
889              
890             $self->_stroke_shape( $shape, $color,
891             x => $x,
892             y => $y,
893             );
894             }
895              
896             sub filledArc
897             {
898             my( $self, $x, $y, $w, $h, $s, $e, $color, $arc_style ) = @_;
899              
900             return unless $w > 0 and $h > 0;
901              
902             $arc_style ||= 0;
903              
904             my $shape = [];
905              
906             # Cairo doesn't support chords
907             if( $arc_style & gdChord )
908             {
909             $s = $s/180*PI;
910             $e = $e/180*PI;
911              
912             my $x1 = $x + ($w/2) * cos($s);
913             my $y1 = $y + ($h/2) * sin($s);
914              
915             my $x2 = $x + ($w/2) * cos($e);
916             my $y2 = $y + ($h/2) * sin($e);
917              
918             push @$shape,
919             move_to => [$x1,$y1],
920             line_to => [$x2,$y2];
921             }
922             else
923             {
924             $shape = _arc( @_ );
925             }
926              
927             push @$shape,
928             line_to => [$x, $y],
929             close_path => [];
930              
931             if( $arc_style & gdNoFill )
932             {
933             $self->_stroke_shape( $shape, $color );
934             }
935             else
936             {
937             $self->_fill_shape( $shape, $color );
938             }
939             }
940              
941             sub copy
942             {
943             my( $self, $sourceImage, $dstX, $dstY, $srcX, $srcY, $width, $height ) = @_;
944              
945             unless( $sourceImage->isa( 'GD::Cairo' ) )
946             {
947             $sourceImage = GD::Cairo->newFromPngData( $sourceImage->png );
948             }
949              
950             push @{$self->{operations}}, [
951             set_source_surface => [$sourceImage->{surface}, $dstX-$srcX, $dstY-$srcY],
952             rectangle => [$dstX,$dstY,$width,$height],
953             fill => []
954             ];
955             }
956              
957             *copyResampled = \©Resized;
958             sub copyResized
959             {
960             my( $self, $sourceImage, $dstX, $dstY, $srcX, $srcY, $destW, $destH, $srcW, $srcH ) = @_;
961              
962             unless( $sourceImage->isa( 'GD::Cairo' ) )
963             {
964             $sourceImage = GD::Cairo->newFromPngData( $sourceImage->png );
965             }
966              
967             my $scaleX = $destW / $srcW;
968             my $scaleY = $destH / $srcH;
969              
970             push @{$self->{operations}}, [
971             set_source_surface => [$sourceImage->{surface}, 0, 0],
972             sub {
973             my( $cr ) = @_;
974             my $pattern = $cr->get_source;
975             $pattern->set_filter( 'bilinear' );
976             my $matrix = $pattern->get_matrix;
977             $matrix->translate( $srcX, $srcY );
978             $matrix->scale( 1/$scaleX, 1/$scaleY );
979             $matrix->translate( -1*$dstX, -1*$dstY );
980             $pattern->set_matrix( $matrix );
981             } => [],
982             translate => [$dstX,$dstY],
983             scale => [$scaleX,$scaleY],
984             rectangle => [0,0,$srcW,$srcH],
985             fill => [],
986             ];
987             }
988              
989             sub copyRotated
990             {
991             my( $self, $sourceImage, $dstX, $dstY, $srcX, $srcY, $width, $height, $angle ) = @_;
992              
993             $angle = $angle/180*PI;
994              
995             unless( $sourceImage->isa( 'GD::Cairo' ) )
996             {
997             $sourceImage = GD::Cairo->newFromPngData( $sourceImage->png );
998             }
999              
1000             my $w = $sourceImage->width;
1001             my $h = $sourceImage->height;
1002              
1003             push @{$self->{operations}}, [
1004             set_source_surface => [$sourceImage->{surface}, 0, 0],
1005             sub {
1006             my( $cr ) = @_;
1007             my $pattern = $cr->get_source;
1008             $pattern->set_filter( 'bilinear' );
1009             my $matrix = $pattern->get_matrix;
1010             $matrix->translate( $w/2, $h/2 );
1011             $matrix->rotate( $angle );
1012             $matrix->translate( -1*$dstX, -1*$dstY );
1013             $pattern->set_matrix( $matrix );
1014             } => [],
1015             translate => [$dstX, $dstY],
1016             rotate => [$angle],
1017             rectangle => [$width/-2,$height/-2,$width,$height],
1018             fill => [],
1019             ];
1020             }
1021              
1022             sub _rotate_point
1023             {
1024             my( $x, $y, $ox, $oy, $angle ) = @_;
1025              
1026             $x -= $ox;
1027             $y -= $oy;
1028              
1029             my $xx = $x * cos($angle) + $y * sin($angle);
1030             my $yy = -1 * $x * sin($angle) + $y * cos($angle);
1031              
1032             return( $xx + $ox, $yy + $oy );
1033             }
1034              
1035             sub _extents
1036             {
1037             my( $self, $font, $ptsize, $angle, $x, $y, $string ) = @_;
1038              
1039             my $cr = $self->{context};
1040              
1041             $cr->save;
1042             $cr->select_font_face( $font, GC_FONT_SLANT_NORMAL, GC_FONT_SLANT_NORMAL );
1043             $cr->set_font_size( $ptsize );
1044             # $cr->rotate( $angle );
1045             my $extents = $cr->text_extents( $string );
1046             $cr->restore;
1047              
1048             return (
1049             _rotate_point( $x + $extents->{x_bearing},
1050             $y + $extents->{y_bearing}, $x, $y, $angle ),
1051             _rotate_point( $x + $extents->{x_bearing} + $extents->{width},
1052             $y + $extents->{y_bearing}, $x, $y, $angle ),
1053             _rotate_point( $x + $extents->{x_bearing} + $extents->{width},
1054             $y + $extents->{y_bearing} + $extents->{height}, $x, $y, $angle ),
1055             _rotate_point( $x + $extents->{x_bearing},
1056             $y + $extents->{y_bearing} + $extents->{height}, $x, $y, $angle ),
1057             );
1058             }
1059              
1060             sub gdTinyFont
1061             {
1062             GD::Cairo::Font->load( 'gdTinyFont' );
1063             }
1064             sub gdSmallFont
1065             {
1066             GD::Cairo::Font->load( 'gdSmallFont' );
1067             }
1068             sub gdMediumBoldFont
1069             {
1070             GD::Cairo::Font->load( 'gdMediumBoldFont' );
1071             }
1072             sub gdLargeFont
1073             {
1074             GD::Cairo::Font->load( 'gdLargeFont' );
1075             }
1076             sub gdGiantFont
1077             {
1078             GD::Cairo::Font->load( 'gdGiantFont' );
1079             }
1080              
1081             *char = \&string;
1082             sub string
1083             {
1084             my( $self, $font, $x, $y, $string, $color, $angle ) = @_;
1085              
1086             $string = Encode::decode("iso-8859-1", $string) unless utf8::is_utf8($string);
1087              
1088             $color = $self->_color( $color );
1089             $angle ||= 0;
1090              
1091             my $ptsize = $font->width * 1.7;
1092             my $weight = GC_FONT_WEIGHT_NORMAL;
1093             if( $font->width == 7 ) # gdMediumBoldFont
1094             {
1095             $weight = GC_FONT_WEIGHT_BOLD;
1096             }
1097              
1098             my @bounds = $self->_extents( 'Monospace', $ptsize, 0, 0, 0, $string );
1099              
1100             if( $angle > 0 )
1101             {
1102             $x += $bounds[7]-$bounds[1];
1103             }
1104             else
1105             {
1106             $y += $bounds[7]-$bounds[1];
1107             }
1108              
1109             push @{$self->{operations}}, [
1110             set_source_rgba => $color,
1111             select_font_face => [ 'Monospace', GC_FONT_SLANT_NORMAL, $weight ],
1112             set_font_size => [$ptsize],
1113             move_to => [$x, $y],
1114             rotate => [$angle],
1115             show_text => [$string],
1116             ];
1117             }
1118              
1119             *charUp = \&stringUp;
1120             sub stringUp
1121             {
1122             $_[0]->string(@_[1..5],PI*1.5);
1123             }
1124              
1125             sub stringFT
1126             {
1127             my( $self, $color, $fontname, $ptsize, $angle, $x, $y, $string ) = @_;
1128              
1129             $string = Encode::decode("iso-8859-1", $string) unless utf8::is_utf8($string);
1130              
1131             $color = $self->_color( $color );
1132              
1133             $angle *= -1; # Already in radians, but in reverse
1134              
1135             my @bounds = $EXTENTS_SELF->_extents( 'Sans-Serif', @_[3..7] );
1136            
1137             return @bounds unless ref($self);
1138              
1139             push @{$self->{operations}}, [
1140             set_source_rgba => $color,
1141             select_font_face => [ 'Sans-Serif', GC_FONT_SLANT_NORMAL, GC_FONT_WEIGHT_NORMAL ],
1142             set_font_size => [$ptsize],
1143             move_to => [$x,$y],
1144             rotate => [$angle],
1145             show_text => [$string],
1146             ];
1147              
1148             return @bounds;
1149             }
1150              
1151             sub interlaced {}
1152              
1153             sub getBounds
1154             {
1155             my( $self ) = @_;
1156              
1157             ($self->width, $self->height);
1158             }
1159              
1160             sub width { $_[0]->{width} }
1161             sub height { $_[0]->{height} }
1162              
1163             sub isTrueColor
1164             {
1165             my( $self ) = @_;
1166              
1167             my $format = $self->{surface}->get_format;
1168              
1169             return $format eq 'argb32' ? 1 : 0;
1170             }
1171              
1172             sub _render_operations
1173             {
1174             my( $self ) = @_;
1175              
1176             my $cr = $self->{context};
1177              
1178             if( defined($self->{background_color}) )
1179             {
1180             my @color = @{$self->{background_color}};
1181             if( defined($self->{transparent}) and
1182             _color_eq( \@color, $self->{transparent} ) )
1183             {
1184             $color[3] = 0;
1185             }
1186             $cr->save;
1187             $cr->set_operator( 'source' );
1188             $cr->set_source_rgba( @color );
1189             $cr->paint;
1190             $cr->restore;
1191             }
1192              
1193             foreach my $shape (@{$self->{operations}})
1194             {
1195             $cr->save;
1196             for(my $i = 0; $i < @$shape; $i+=2)
1197             {
1198             my( $f, $opts ) = @$shape[$i,$i+1];
1199             if( ref($f) eq 'CODE' )
1200             {
1201             &$f( $cr, @$opts );
1202             }
1203             else
1204             {
1205             $cr->$f( @$opts );
1206             }
1207             }
1208             $cr->restore;
1209             }
1210              
1211             $cr->show_page;
1212             }
1213              
1214             sub _write_buffer
1215             {
1216             my( $self, $class ) = @_;
1217              
1218             my $buffer = '';
1219             my $surface = $class->create_from_stream( sub { $buffer .= $_[1] }, '', $self->width, $self->height );
1220             my $context = Cairo::Context->create( $surface );
1221              
1222             $self->{context} = $context;
1223             $self->_render_operations;
1224              
1225             return $buffer;
1226             }
1227              
1228             sub _write_file
1229             {
1230             my( $self, $filename, $class ) = @_;
1231              
1232             my $surface = $class->create( $filename, $self->width, $self->height );
1233             my $context = Cairo::Context->create( $surface );
1234              
1235             $self->{context} = $context;
1236             $self->_render_operations;
1237             }
1238              
1239             sub png
1240             {
1241             my( $self ) = @_;
1242              
1243             $self->_render_operations;
1244              
1245             my $buffer = '';
1246             $self->{surface}->write_to_png_stream(sub { $buffer .= $_[1] }, '');
1247              
1248             return $buffer;
1249             }
1250             sub writePng
1251             {
1252             my( $self, $filename ) = @_;
1253              
1254             open(my $fh, ">", $filename) or die "Error writing to $filename: $!";
1255             binmode($fh);
1256             print $fh $self->png;
1257             close($fh);
1258             }
1259              
1260             sub pdf
1261             {
1262             _write_buffer( $_[0], 'Cairo::PdfSurface' );
1263             }
1264             sub writePdf
1265             {
1266             _write_file( $_[0], $_[1], 'Cairo::PdfSurface' );
1267             }
1268              
1269             sub svg
1270             {
1271             _write_buffer( $_[0], 'Cairo::SvgSurface' );
1272             }
1273             sub writeSvg
1274             {
1275             _write_file( $_[0], $_[1], 'Cairo::SvgSurface' );
1276             }
1277              
1278             package GD::Cairo::Font;
1279              
1280             # Utility class to create GD::Font stub classes that work with GD::Cairo
1281              
1282             use strict;
1283              
1284             our %GD_FONTS = (
1285             gdTinyFont => {
1286             nchars => 256,
1287             offset => 0,
1288             width => 5,
1289             height => 8
1290             },
1291             gdSmallFont => {
1292             nchars => 256,
1293             offset => 0,
1294             width => 6,
1295             height => 13
1296             },
1297             gdMediumBoldFont => {
1298             nchars => 256,
1299             offset => 0,
1300             width => 7,
1301             height => 13
1302             },
1303             gdLargeFont => {
1304             nchars => 256,
1305             offset => 0,
1306             width => 8,
1307             height => 16
1308             },
1309             gdGiantFont => {
1310             nchars => 256,
1311             offset => 0,
1312             width => 9,
1313             height => 15
1314             },
1315             );
1316              
1317             our %FONT_CACHE;
1318              
1319             sub load
1320             {
1321             my( $class, $font ) = @_;
1322              
1323             $class = "${class}::$font";
1324              
1325             return $FONT_CACHE{$font} ||= bless $GD_FONTS{$font}, $class;
1326             }
1327              
1328             sub nchars { $_[0]->{nchars} }
1329             sub offset { $_[0]->{offset} }
1330             sub width { $_[0]->{width} }
1331             sub height { $_[0]->{height} }
1332              
1333             package GD::Cairo::Font::gdTinyFont;
1334              
1335             our @ISA = qw( GD::Cairo::Font );
1336              
1337             package GD::Cairo::Font::gdSmallFont;
1338              
1339             our @ISA = qw( GD::Cairo::Font );
1340              
1341             package GD::Cairo::Font::gdMediumBoldFont;
1342              
1343             our @ISA = qw( GD::Cairo::Font );
1344              
1345             package GD::Cairo::Font::gdLargeFont;
1346              
1347             our @ISA = qw( GD::Cairo::Font );
1348              
1349             package GD::Cairo::Font::gdGiantFont;
1350              
1351             our @ISA = qw( GD::Cairo::Font );
1352              
1353             1;
1354              
1355             # Autoload methods go after =cut, and are processed by the autosplit program.
1356              
1357             __END__