| GD-Cairo documentation | Contained in the GD-Cairo distribution. |
GD::Cairo - GD API wrapper around Cairo
use GD; # Needed for constants and GD::Polygon use GD::Cairo; # use GD; use GD::Cairo qw( :gd ); # Import GD constants and fonts # my $img = GD::Image->new( 400, 300, 1 ); my $img = GD::Cairo->new( 400, 300, 1 ); print $fh $img->svg;
This module provides a GD API emulation for the Cairo graphics library. Cairo is a vector-based drawing package that aims to provide consistent output to many graphics contexts/formats.
See <GD>.
Create a new image of WIDTH by HEIGHT. WIDTH and HEIGHT are in user-space units (e.g. pixels for PNG or points for PDF).
Ignore any missing functionality in GD::Cairo that may be in GD.
Return the image in PNG format.
Return the image in PDF format.
Return the image in SVG format.
(newFromPngData implemented.)
This is deprecated anyway.
Unlikely to ever work.
Ignored.
Patches/suggestions are welcome.
I don't think Cairo supports paletted images, see http://cairographics.org/manual/cairo-Image-Surfaces.html#cairo-format-t.
PngSurface doesn't appear to reliably translate coordinates onto the surface e.g. a point at 0,0 doesn't get rendered at all.
StringFT* will always render using 'Sans-Serif' and String* using 'Monospace' (which depend on fontconfig). I need an example for loading fonts with Cairo.
GD renders brushes by repeatedly rendering the brush (an image) along the path the given shape provides. This isn't practically achievable with Cairo (AFAIK), so instead I repeat the image along the path/fill.
Does not support gdStyledBrushed.
In order to support GD::Image::fill GD::Cairo builds a stack of operations, which makes it memory inefficient compared to writing direct to a GD::Image surface.
GD::Cairo also stores a hash entry for every pixel set with setPixel to support getPixel.
Cairo, GD, GD::SVG (includes extensive discussion of why translating GD to a vector library is difficult).
http://cairographics.org/manual/
Tim D Brody, <tdb01r@ecs.soton.ac.uk>
Copyright (C) 2007 by Tim D Brody
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.8 or, at your option, any later version of Perl 5 you may have available.
| GD-Cairo documentation | Contained in the GD-Cairo distribution. |
package GD::Cairo; use 5.006; use strict; use warnings; require Exporter; use Encode; our @ISA = qw(Exporter); # Items to export into callers namespace by default. Note: do not export # names by default without a very good reason. Use EXPORT_OK instead. # Do not simply export all your public functions/methods/constants. # This allows declaration use GD::Cairo ':all'; # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK # will save memory. our %EXPORT_TAGS = ( 'gd' => [ qw( gdBrushed gdDashSize gdMaxColors gdStyled gdStyledBrushed gdTiled gdTransparent gdAntiAliased gdArc gdChord gdPie gdNoFill gdEdged gdAlphaMax gdAlphaOpaque gdAlphaTransparent gdTinyFont gdSmallFont gdMediumBoldFont gdLargeFont gdGiantFont ) ] ); our @EXPORT_OK = ( @{ $EXPORT_TAGS{'gd'} } ); our @EXPORT = qw( ); our $VERSION = '0.01'; use constant PI => 4 * atan2 1, 1; use constant PI_2 => 8 * atan2 1, 1; use constant GC_FONT_SLANT_NORMAL => 'normal'; use constant GC_FONT_SLANT_ITALIC => 'italic'; use constant GC_FONT_SLANT_OBLIQUE => 'oblique'; use constant GC_FONT_WEIGHT_NORMAL => 'normal'; use constant GC_FONT_WEIGHT_BOLD => 'bold'; use constant { 'gdAntiAliased' => -7, 'gdTransparent' => -6, 'gdTiled' => -5, 'gdStyledBrushed' => -4, 'gdBrushed' => -3, 'gdStyled' => -2, 'gdDashSize' => 4, 'gdMaxColors' => 256, 'gdArc' => 0, 'gdPie' => 0, 'gdChord' => 1, 'gdNoFill' => 2, 'gdEdged' => 4, 'gdAlphaMax' => 127, 'gdAlphaOpaque' => 0, 'gdAlphaTransparent' => 127, }; use Cairo; use Data::Dumper; our $EXTENTS_SELF; our $TRUECOLOR = 0; our $ANTIALIAS = 0; use vars qw( $AUTOLOAD ); # Preloaded methods go here. sub _new { my( $class, @opts ) = @_; my $self = bless { background_color => undef, colors => [], operations => [], transparent => undef, thickness => 1, brush => undef, style => {}, }, $class; } sub newFromSurface { my( $class, $surface ) = @_; my $self = $class->_new(); $self->{surface} = $surface; $self->{context} = Cairo::Context->create( $surface ); $self->{context}->set_line_width( $self->{thickness} ); $self->{width} = $surface->get_width; $self->{height} = $surface->get_height; $EXTENTS_SELF = $self; return $self; } sub new { my( $class, $w, $h, $truecolor ) = @_; $truecolor = $TRUECOLOR if scalar(@_) == 3; my $format = $truecolor ? 'argb32' : 'a8'; $format = 'argb32'; my $surface = Cairo::ImageSurface->create( $format, $w, $h ); return $class->newFromSurface( $surface ); } sub newFromPngData { my( $class, $data, $truecolor ) = @_; pos($data) = 0; my $surface = Cairo::ImageSurface->create_from_png_stream(sub { my( $closure, $length ) = @_; use bytes; my $buffer = substr($data,pos($data),$length); pos($data) += $length; return $buffer; }); return $class->newFromSurface( $surface ); } sub getCairoContext { $_[0]->{context}; } sub getCairoImageSurface { $_[0]->{surface}; } sub getCairoPattern { $_[0]->{brush}; } sub trueColor { my( $self, $truecolor ) = @_; $TRUECOLOR = $truecolor; } sub newPalette { my( $class, $w, $h ) = @_; # my $surface = Cairo::ImageSurface->create( 'a8', $w, $h ); my $surface = Cairo::ImageSurface->create( 'argb32', $w, $h ); return $class->newFromSurface( $surface ); } sub newTrueColor { my( $class, $w, $h ) = @_; my $surface = Cairo::ImageSurface->create( 'argb32', $w, $h ); return $class->newFromSurface( $surface ); } sub ignoreMissing { my( $warn ) = @_; if( $warn ) { *AUTOLOAD = sub { $AUTOLOAD =~ s/^.*:://; return if $AUTOLOAD =~ /^[A-Z]/; 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"; }; } else { *AUTOLOAD = sub {} } } sub _color { my( $self, $index ) = @_; my $color; if( $index == gdAntiAliased ) { Carp::croak "You must call setAntiAliased before using gdAntiAliased" unless defined $self->{antialiased}; $color = $self->{antialiased}; } else { $color = $self->{colors}->[$index] or Carp::croak "Invalid color $index - perhaps you need to call colorAllocate"; } return $color; } sub _color_to_index { my( $self, $color ) = @_; my $i = 0; for(@{$self->{colors}}) { return $i if( _color_eq( $color, $_ ) ); ++$i; } die "No color allocated for [".join(',',@$color)."]"; } sub _color_index_to_role { my( $self, $index, $x, $y ) = @_; if( $index == gdBrushed or $index == gdTiled ) { $x ||= 0; $y ||= 0; unless( defined $self->{brush} ) { Carp::croak "Can't use gdBrushed without first calling setBrush"; } my $w = $self->{brush}->width; my $h = $self->{brush}->height; my $thickness = $w > $h ? $w : $h; my $style = gdBrushed == $index ? 'repeat' : 'repeat'; return set_source_surface => [$self->{brush}->{surface}, $x, $y], set_line_width => [$thickness], sub { my( $cr ) = @_; my $pattern = $cr->get_source; $pattern->set_filter( 'bilinear' ); $pattern->set_extend( $style ); } => []; } elsif( $index == gdStyled ) { Carp::croak "Can only apply gdStyled to lines"; } elsif( $index == gdAntiAliased ) { return set_source_rgba => $self->_color( $index ), set_antialias => ['default'], set_line_width => [$self->{thickness}]; } else { return set_source_rgba => $self->_color( $index ), set_antialias => ['none'], set_line_width => [$self->{thickness}]; } } sub _color_eq { for(0..3) { return 0 if $_[0]->[$_] != $_[1]->[$_] }; return 1; } sub _shape_color { my( $self, $shape ) = @_; for(my $i = 0; $i < @$shape; $i+=2) { if( $shape->[$i] eq 'set_source_rgba' ) { return $shape->[$i+1]; } } return undef; } *GD::Cairo::colorAllocateAlpha = \&colorAllocate; *GD::Cairo::colorClosest = \&colorAllocate; *GD::Cairo::colorExact = \&colorAllocate; *GD::Cairo::colorResolve = \&colorAllocate; sub colorAllocate { my( $self, $red, $green, $blue, $alpha ) = @_; $red /= 255; $green /= 255; $blue /= 255; $alpha = @_ == 4 ? 1 : (1 - $alpha / 127); for(my $i = 0; $i < @{$self->{colors}}; ++$i) { my @color = @{$self->{colors}->[$i]}; if( $color[0] == $red and $color[1] == $green and $color[2] == $blue and $color[3] == $alpha ) { return $i; } } push @{$self->{colors}}, [$red, $green, $blue, $alpha]; return $#{$self->{colors}}; } sub colorDeallocate { my( $self, $color ) = @_; # Unimplemented } sub colorsTotal { my( $self ) = @_; if( $self->isTrueColor ) { return undef; } else { return scalar(@{$self->{colors}}); } } sub _in_shape { my( $self, $x, $y ) = @_; my $cr = $self->{context}; my $i = -1; my $shape; my $color; for($i = $#{$self->{operations}}; $i > -1; --$i, undef $color) { $shape = $self->{operations}->[$i]; $cr->save; for(my $j = 0; $j < @$shape; $j+=2) { my( $f, $opts ) = @$shape[$j,$j+1]; if( $f eq 'fill' or $f eq 'stroke' or $f eq 'paint' ) { } elsif( $f eq 'set_source_rgba' ) { $color = $opts; } elsif( ref($f) eq 'CODE' ) { } else { $cr->$f( @$opts ); } } my $in_fill = $cr->in_fill( $x, $y ); $cr->restore; last if $in_fill; } if( $i != -1 ) { return $i, $shape, $color; } else { return (); } } sub _convert_style_to_dashes { my( $self, @colors ) = @_; my %lines; my %components = map({ ($_ == gdTransparent) ? () : ($_ => 1) } @colors); foreach my $color (keys %components) { my $dash_map = join '', map({ $_ == $color ? 1 : 0 } @colors); my @opts = (0); # dash offset while(length($dash_map)) { if( $dash_map =~ s/^(1+)// ) { push @opts, length($1); } if( $dash_map =~ s/^(0+)// ) { push @opts, length($1); } } unshift @opts, 0 if $colors[0] != $color; # gap or color first $lines{$color} = \@opts; } return %lines; } sub _set_brush { my( $self, $shape, $index, %opts ) = @_; my $x = exists($opts{x}) ? $opts{x} : 0; my $y = exists($opts{y}) ? $opts{y} : 0; unless( defined $self->{brush} ) { Carp::croak "Can't use gdBrushed without first calling setBrush"; } my $w = $self->{brush}->width; my $h = $self->{brush}->height; my $thickness = $w > $h ? $w : $h; my $style = gdBrushed == $index ? 'repeat' : 'repeat'; unshift @$shape, set_source_surface => [$self->{brush}->{surface}, $x, $y], set_line_width => [$thickness], sub { my( $cr ) = @_; my $pattern = $cr->get_source; $pattern->set_filter( 'bilinear' ); $pattern->set_extend( $style ); } => []; } sub _stroke_shape { my( $self, $shape, $index, %opts ) = @_; my $antialias = defined($opts{'antialias'}) ? $opts{'antialias'} : ($index == gdAntiAliased or $ANTIALIAS) ? 'default' : 'none'; if( $index == gdBrushed or $index == gdTiled ) { $self->_set_brush( $shape, $index, %opts ); } elsif( $index == gdStyled ) { unless( scalar(keys %{$self->{style}}) > 0 ) { Carp::croak "Can't use gdStyled without first calling setStyle"; } while(my( $color, $dashes ) = each %{$self->{style}}) { my @new_shape = @$shape; unshift @new_shape, set_source_rgba => $self->_color( $color ), set_dash => $dashes, set_line_width => [$self->{thickness}], set_antialias => [$antialias]; push @new_shape, stroke => []; push @{$self->{operations}}, \@new_shape; } return; # Don't add $shape } else { unshift @$shape, set_source_rgba => $self->_color( $index ), set_antialias => [$antialias], set_line_width => [$self->{thickness}]; } push @$shape, stroke => []; push @{$self->{operations}}, $shape; } sub _fill_shape { my( $self, $shape, $index, %opts ) = @_; my $antialias = defined($opts{'antialias'}) ? $opts{'antialias'} : ($index == gdAntiAliased or $ANTIALIAS) ? 'default' : 'none'; if( $index == gdBrushed or $index == gdTiled ) { $self->_set_brush( $shape, $index, %opts ); } elsif( $index == gdStyled ) { Carp::croak "Can only apply gdStyled to lines"; } else { unshift @$shape, set_source_rgba => $self->_color( $index ), set_antialias => [$antialias]; } push @$shape, fill => []; push @{$self->{operations}}, $shape; } sub _paint_shape { my( $self, $shape, $index, %opts ) = @_; if( $index == gdBrushed or $index == gdTiled ) { $self->_set_brush( $shape, $index, %opts ); } elsif( $index == gdStyled ) { Carp::croak "Can only apply gdStyled to lines"; } else { unshift @$shape, set_source_rgba => $self->_color( $index ); } push @$shape, paint => []; push @{$self->{operations}}, $shape; } sub fill { my( $self, $x, $y, $color ) = @_; my $cr = $self->{context}; # Background if( 0 == scalar @{$self->{operations}} ) { $self->{background_color} = $self->_color( $color ); } # Find the first shape that contains $x,$y # If it's a stroke then 'fill' it by adding the fill behind, otherwise # replace it with the new color elsif( my( $i, $shape, $shape_color ) = $self->_in_shape( $x, $y ) ) { my @new_shape; my $stroked = 0; for(my $j = 0; $j < @$shape; $j+=2) { my( $f, $opts ) = @$shape[$j,$j+1]; if( $f eq 'stroke' ) { $stroked = 1; } elsif( $f eq 'stroke' or $f eq 'fill' or $f eq 'set_source_rgba' or $f eq 'set_source_surface' ) { } else { push @new_shape, $f => $opts; } } $self->_fill_shape( \@new_shape, $color ); if( $stroked ) { splice(@{$self->{operations}},$i,0,pop @{$self->{operations}}); } else { splice(@{$self->{operations}},$i,1,pop @{$self->{operations}}); } } } sub getPixel { my( $self, $x, $y ) = @_; my $color; # Try finding the pixel in a shape if( my( $i, $shape, $c ) = $self->_in_shape( $x, $y ) ) { $color = $c; } # See if they setPixel this pixel elsif( exists $self->{pixels}->{"${x}x${y}"} ) { return $self->{pixels}->{"${x}x${y}"}; } # Or the background elsif( defined $self->{background_color} ) { $color = $self->{background_color}; } # GetPixel must return something else { $color = $self->{colors}->[0]; } return $self->_color_to_index( $color ); } sub setPixel { my( $self, $x, $y, $color ) = @_; if( $color == gdBrushed ) { my $w = $self->{brush}->width; my $h = $self->{brush}->height; $self->copy( $self->{brush}, $x - $w/2, $y - $h/2, 0, 0, $w, $h ); } else { $self->{pixels}->{"${x}x${y}"} = $color; push @{$self->{operations}}, [ set_source_rgba => $self->_color( $color ), set_line_width => [1], set_antialias => ['none'], move_to => [$x-1,$y], line_to => [$x,$y], stroke => [] ]; } } sub rgb { my( $self, $index ) = @_; return map { sprintf("%.0f", $_ * 255) } @{$self->{colors}->[$index]}[0..2]; } sub transparent { my( $self, $index ) = @_; if( 1 == @_ ) { return defined $self->{transparent} ? $self->_color_to_index( $self->{transparent} ) : -1; } return $self->{transparent} = $index > -1 ? $self->{colors}->[$index] : -1; } *setTile = \&setBrush; sub setBrush { my( $self, $image ) = @_; unless( $image->isa( 'GD::Cairo' ) ) { $image = GD::Cairo->newFromPngData( $image->png ); } $self->{brush} = $image; } sub setStyle { my( $self, @colors ) = @_; my %lines = $self->_convert_style_to_dashes( @colors ); $self->{style} = \%lines; } sub setThickness { my( $self, $thickness ) = @_; $self->{thickness} = $thickness; } sub setAntiAliased { my( $self, $color ) = @_; $self->{antialiased} = $self->_color( $color ); } sub rectangle { my( $self, $x, $y, $x2, $y2, $color ) = @_; my $shape = [ rectangle => [$x, $y, $x2-$x, $y2-$y], ]; $self->_stroke_shape( $shape, $color, x => $x, y => $y, antialias => 'none' ); } sub filledRectangle { my( $self, $x, $y, $x2, $y2, $color ) = @_; my $shape = [ rectangle => [$x, $y, $x2-$x, $y2-$y], ]; $self->_fill_shape( $shape, $color, x => $x, y => $y, antialias => 'none' ); } sub _polygon { my( $self, $polygon, $color ) = @_; my @shape = (move_to => [$polygon->getPt(0)]); my(undef, @vertices) = $polygon->vertices; push @shape, line_to => $_ for @vertices; return \@shape; } # I think polygon is a synonym of openPolygon? *polygon = \&openPolygon; sub openPolygon { my( $self, $polygon, $color ) = @_; my $shape = _polygon( @_ ); push @$shape, close_path => []; $self->_stroke_shape( $shape, $color ); } sub unclosedPolygon { my( $self, $polygon, $color ) = @_; my $shape = _polygon( @_ ); $self->_stroke_shape( $shape, $color ); } sub filledPolygon { my( $self, $polygon, $color ) = @_; my $shape = _polygon( @_ ); push @$shape, close_path => []; $self->_fill_shape( $shape, $color ); } sub line { my( $self, $x, $y, $x2, $y2, $color ) = @_; if( abs($x2-$x) < 1 and abs($y2-$y) < 1 ) { return $self->setPixel( $x, $y, $color ); } my $shape = [ new_path => [], move_to => [$x, $y], line_to => [$x2, $y2] ]; my $antialias = ($x == $x2 or $y == $y2) ? 'none' : undef; $self->_stroke_shape( $shape, $color, x => $x, y => $y, antialias => $antialias ); } sub _ellipse { my( $self, $x, $y, $w, $h, $color ) = @_; my $s = 0; my $e = PI_2; [ save => [], translate => [$x - .5, $y], scale => [$w/2 - .5, $h/2], arc => [0, 0, 1, $s, $e ], close_path => [], restore => [], ]; } sub ellipse { my( $self, $x, $y, $w, $h, $color ) = @_; return unless $w > 0 and $h > 0; my $shape = _ellipse( @_ ); $self->_stroke_shape( $shape, $color, x => $x, y => $y ); } sub filledEllipse { my( $self, $x, $y, $w, $h, $color ) = @_; return unless $w > 0 and $h > 0; my $shape = _ellipse( @_ ); $self->_fill_shape( $shape, $color, x => $x, y => $y ); } sub _arc { my( $self, $x, $y, $w, $h, $s, $e, $color ) = @_; $s = $s/180*PI; $e = $e/180*PI; [ save => [], translate => [$x - .5, $y], scale => [$w/2 - .5, $h/2], arc => [0, 0, 1, $s, $e ], restore => [], ]; } sub arc { my( $self, $x, $y, $w, $h, $s, $e, $color ) = @_; return unless $w > 0 and $h > 0; my $shape = _arc( @_ ); $self->_stroke_shape( $shape, $color, x => $x, y => $y, ); } sub filledArc { my( $self, $x, $y, $w, $h, $s, $e, $color, $arc_style ) = @_; return unless $w > 0 and $h > 0; $arc_style ||= 0; my $shape = []; # Cairo doesn't support chords if( $arc_style & gdChord ) { $s = $s/180*PI; $e = $e/180*PI; my $x1 = $x + ($w/2) * cos($s); my $y1 = $y + ($h/2) * sin($s); my $x2 = $x + ($w/2) * cos($e); my $y2 = $y + ($h/2) * sin($e); push @$shape, move_to => [$x1,$y1], line_to => [$x2,$y2]; } else { $shape = _arc( @_ ); } push @$shape, line_to => [$x, $y], close_path => []; if( $arc_style & gdNoFill ) { $self->_stroke_shape( $shape, $color ); } else { $self->_fill_shape( $shape, $color ); } } sub copy { my( $self, $sourceImage, $dstX, $dstY, $srcX, $srcY, $width, $height ) = @_; unless( $sourceImage->isa( 'GD::Cairo' ) ) { $sourceImage = GD::Cairo->newFromPngData( $sourceImage->png ); } push @{$self->{operations}}, [ set_source_surface => [$sourceImage->{surface}, $dstX-$srcX, $dstY-$srcY], rectangle => [$dstX,$dstY,$width,$height], fill => [] ]; } *copyResampled = \©Resized; sub copyResized { my( $self, $sourceImage, $dstX, $dstY, $srcX, $srcY, $destW, $destH, $srcW, $srcH ) = @_; unless( $sourceImage->isa( 'GD::Cairo' ) ) { $sourceImage = GD::Cairo->newFromPngData( $sourceImage->png ); } my $scaleX = $destW / $srcW; my $scaleY = $destH / $srcH; push @{$self->{operations}}, [ set_source_surface => [$sourceImage->{surface}, 0, 0], sub { my( $cr ) = @_; my $pattern = $cr->get_source; $pattern->set_filter( 'bilinear' ); my $matrix = $pattern->get_matrix; $matrix->translate( $srcX, $srcY ); $matrix->scale( 1/$scaleX, 1/$scaleY ); $matrix->translate( -1*$dstX, -1*$dstY ); $pattern->set_matrix( $matrix ); } => [], translate => [$dstX,$dstY], scale => [$scaleX,$scaleY], rectangle => [0,0,$srcW,$srcH], fill => [], ]; } sub copyRotated { my( $self, $sourceImage, $dstX, $dstY, $srcX, $srcY, $width, $height, $angle ) = @_; $angle = $angle/180*PI; unless( $sourceImage->isa( 'GD::Cairo' ) ) { $sourceImage = GD::Cairo->newFromPngData( $sourceImage->png ); } my $w = $sourceImage->width; my $h = $sourceImage->height; push @{$self->{operations}}, [ set_source_surface => [$sourceImage->{surface}, 0, 0], sub { my( $cr ) = @_; my $pattern = $cr->get_source; $pattern->set_filter( 'bilinear' ); my $matrix = $pattern->get_matrix; $matrix->translate( $w/2, $h/2 ); $matrix->rotate( $angle ); $matrix->translate( -1*$dstX, -1*$dstY ); $pattern->set_matrix( $matrix ); } => [], translate => [$dstX, $dstY], rotate => [$angle], rectangle => [$width/-2,$height/-2,$width,$height], fill => [], ]; } sub _rotate_point { my( $x, $y, $ox, $oy, $angle ) = @_; $x -= $ox; $y -= $oy; my $xx = $x * cos($angle) + $y * sin($angle); my $yy = -1 * $x * sin($angle) + $y * cos($angle); return( $xx + $ox, $yy + $oy ); } sub _extents { my( $self, $font, $ptsize, $angle, $x, $y, $string ) = @_; my $cr = $self->{context}; $cr->save; $cr->select_font_face( $font, GC_FONT_SLANT_NORMAL, GC_FONT_SLANT_NORMAL ); $cr->set_font_size( $ptsize ); # $cr->rotate( $angle ); my $extents = $cr->text_extents( $string ); $cr->restore; return ( _rotate_point( $x + $extents->{x_bearing}, $y + $extents->{y_bearing}, $x, $y, $angle ), _rotate_point( $x + $extents->{x_bearing} + $extents->{width}, $y + $extents->{y_bearing}, $x, $y, $angle ), _rotate_point( $x + $extents->{x_bearing} + $extents->{width}, $y + $extents->{y_bearing} + $extents->{height}, $x, $y, $angle ), _rotate_point( $x + $extents->{x_bearing}, $y + $extents->{y_bearing} + $extents->{height}, $x, $y, $angle ), ); } sub gdTinyFont { GD::Cairo::Font->load( 'gdTinyFont' ); } sub gdSmallFont { GD::Cairo::Font->load( 'gdSmallFont' ); } sub gdMediumBoldFont { GD::Cairo::Font->load( 'gdMediumBoldFont' ); } sub gdLargeFont { GD::Cairo::Font->load( 'gdLargeFont' ); } sub gdGiantFont { GD::Cairo::Font->load( 'gdGiantFont' ); } *char = \&string; sub string { my( $self, $font, $x, $y, $string, $color, $angle ) = @_; $string = Encode::decode("iso-8859-1", $string) unless utf8::is_utf8($string); $color = $self->_color( $color ); $angle ||= 0; my $ptsize = $font->width * 1.7; my $weight = GC_FONT_WEIGHT_NORMAL; if( $font->width == 7 ) # gdMediumBoldFont { $weight = GC_FONT_WEIGHT_BOLD; } my @bounds = $self->_extents( 'Monospace', $ptsize, 0, 0, 0, $string ); if( $angle > 0 ) { $x += $bounds[7]-$bounds[1]; } else { $y += $bounds[7]-$bounds[1]; } push @{$self->{operations}}, [ set_source_rgba => $color, select_font_face => [ 'Monospace', GC_FONT_SLANT_NORMAL, $weight ], set_font_size => [$ptsize], move_to => [$x, $y], rotate => [$angle], show_text => [$string], ]; } *charUp = \&stringUp; sub stringUp { $_[0]->string(@_[1..5],PI*1.5); } sub stringFT { my( $self, $color, $fontname, $ptsize, $angle, $x, $y, $string ) = @_; $string = Encode::decode("iso-8859-1", $string) unless utf8::is_utf8($string); $color = $self->_color( $color ); $angle *= -1; # Already in radians, but in reverse my @bounds = $EXTENTS_SELF->_extents( 'Sans-Serif', @_[3..7] ); return @bounds unless ref($self); push @{$self->{operations}}, [ set_source_rgba => $color, select_font_face => [ 'Sans-Serif', GC_FONT_SLANT_NORMAL, GC_FONT_WEIGHT_NORMAL ], set_font_size => [$ptsize], move_to => [$x,$y], rotate => [$angle], show_text => [$string], ]; return @bounds; } sub interlaced {} sub getBounds { my( $self ) = @_; ($self->width, $self->height); } sub width { $_[0]->{width} } sub height { $_[0]->{height} } sub isTrueColor { my( $self ) = @_; my $format = $self->{surface}->get_format; return $format eq 'argb32' ? 1 : 0; } sub _render_operations { my( $self ) = @_; my $cr = $self->{context}; if( defined($self->{background_color}) ) { my @color = @{$self->{background_color}}; if( defined($self->{transparent}) and _color_eq( \@color, $self->{transparent} ) ) { $color[3] = 0; } $cr->save; $cr->set_operator( 'source' ); $cr->set_source_rgba( @color ); $cr->paint; $cr->restore; } foreach my $shape (@{$self->{operations}}) { $cr->save; for(my $i = 0; $i < @$shape; $i+=2) { my( $f, $opts ) = @$shape[$i,$i+1]; if( ref($f) eq 'CODE' ) { &$f( $cr, @$opts ); } else { $cr->$f( @$opts ); } } $cr->restore; } $cr->show_page; } sub _write_buffer { my( $self, $class ) = @_; my $buffer = ''; my $surface = $class->create_from_stream( sub { $buffer .= $_[1] }, '', $self->width, $self->height ); my $context = Cairo::Context->create( $surface ); $self->{context} = $context; $self->_render_operations; return $buffer; } sub _write_file { my( $self, $filename, $class ) = @_; my $surface = $class->create( $filename, $self->width, $self->height ); my $context = Cairo::Context->create( $surface ); $self->{context} = $context; $self->_render_operations; } sub png { my( $self ) = @_; $self->_render_operations; my $buffer = ''; $self->{surface}->write_to_png_stream(sub { $buffer .= $_[1] }, ''); return $buffer; } sub writePng { my( $self, $filename ) = @_; open(my $fh, ">", $filename) or die "Error writing to $filename: $!"; binmode($fh); print $fh $self->png; close($fh); } sub pdf { _write_buffer( $_[0], 'Cairo::PdfSurface' ); } sub writePdf { _write_file( $_[0], $_[1], 'Cairo::PdfSurface' ); } sub svg { _write_buffer( $_[0], 'Cairo::SvgSurface' ); } sub writeSvg { _write_file( $_[0], $_[1], 'Cairo::SvgSurface' ); } package GD::Cairo::Font; # Utility class to create GD::Font stub classes that work with GD::Cairo use strict; our %GD_FONTS = ( gdTinyFont => { nchars => 256, offset => 0, width => 5, height => 8 }, gdSmallFont => { nchars => 256, offset => 0, width => 6, height => 13 }, gdMediumBoldFont => { nchars => 256, offset => 0, width => 7, height => 13 }, gdLargeFont => { nchars => 256, offset => 0, width => 8, height => 16 }, gdGiantFont => { nchars => 256, offset => 0, width => 9, height => 15 }, ); our %FONT_CACHE; sub load { my( $class, $font ) = @_; $class = "${class}::$font"; return $FONT_CACHE{$font} ||= bless $GD_FONTS{$font}, $class; } sub nchars { $_[0]->{nchars} } sub offset { $_[0]->{offset} } sub width { $_[0]->{width} } sub height { $_[0]->{height} } package GD::Cairo::Font::gdTinyFont; our @ISA = qw( GD::Cairo::Font ); package GD::Cairo::Font::gdSmallFont; our @ISA = qw( GD::Cairo::Font ); package GD::Cairo::Font::gdMediumBoldFont; our @ISA = qw( GD::Cairo::Font ); package GD::Cairo::Font::gdLargeFont; our @ISA = qw( GD::Cairo::Font ); package GD::Cairo::Font::gdGiantFont; our @ISA = qw( GD::Cairo::Font ); 1; # Autoload methods go after =cut, and are processed by the autosplit program. __END__