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__
=head1 NAME
GD::Cairo - GD API wrapper around Cairo
=head1 SYNOPSIS
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;
=head1 DESCRIPTION
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.
=head1 METHODS
See <GD>.
=head2 GD::Cairo-specific methods
=over 4
=item GD::Cairo->new( WIDTH, HEIGHT [, TRUECOLOR ] )
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).
=item GD::Cairo::ignoreMissing( [ WARN ] )
Ignore any missing functionality in GD::Cairo that may be in GD.
=item $data = $img->png
Return the image in PNG format.
=item $data = $img->pdf
Return the image in PDF format.
=item $data = $img->svg
Return the image in SVG format.
=back
=head1 TODO
=over 4
=item new(*FILEHANDLE)
=item new($filename)
=item new($data)
=item newFrom*
(newFromPngData implemented.)
=item colorClosestHWB
=item setAntiAliasedDontBlend($color [,$flag])
=item dashedLine
This is deprecated anyway.
=item fillToBorder
Unlikely to ever work.
=item clone
=item trueColorToPalette
=item alphaBlending
=item saveAlpha
=item interlaced
Ignored.
=item compare($image2)
=item clip($x1,$y1,$x2,$y2)
=item boundsSafe($x,$y)
=item GD::Polygon, GD::Polyline
=item GD::Simple
=head1 BUGS
Patches/suggestions are welcome.
=head2 Images are always true colour
I don't think Cairo supports paletted images, see http://cairographics.org/manual/cairo-Image-Surfaces.html#cairo-format-t.
=head2 Alignment in PNG Output
PngSurface doesn't appear to reliably translate coordinates onto the surface e.g. a point at 0,0 doesn't get rendered at all.
=head2 StringFT/String/StringUp
StringFT* will always render using 'Sans-Serif' and String* using 'Monospace' (which depend on fontconfig). I need an example for loading fonts with Cairo.
=head2 SetBrush
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.
=head2 SetStyle
Does not support gdStyledBrushed.
=head2 Memory Usage
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.
=head1 SEE ALSO
L<Cairo>, L<GD>, L<GD::SVG> (includes extensive discussion of why translating GD to a vector library is difficult).
http://cairographics.org/manual/
=head1 AUTHOR
Tim D Brody, E<lt>tdb01r@ecs.soton.ac.ukE<gt>
=head1 COPYRIGHT AND LICENSE
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.
=cut