GD::Cairo - GD API wrapper around Cairo


GD-Cairo documentation Contained in the GD-Cairo distribution.

Index


Code Index:

NAME

Top

GD::Cairo - GD API wrapper around Cairo

SYNOPSIS

Top

  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;

DESCRIPTION

Top

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.

METHODS

Top

See <GD>.

GD::Cairo-specific methods

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).

GD::Cairo::ignoreMissing( [ WARN ] )

Ignore any missing functionality in GD::Cairo that may be in GD.

$data = $img->png

Return the image in PNG format.

$data = $img->pdf

Return the image in PDF format.

$data = $img->svg

Return the image in SVG format.

TODO

Top

new(*FILEHANDLE)
new($filename)
new($data)
newFrom*

(newFromPngData implemented.)

colorClosestHWB
setAntiAliasedDontBlend($color [,$flag])
dashedLine

This is deprecated anyway.

fillToBorder

Unlikely to ever work.

clone
trueColorToPalette
alphaBlending
saveAlpha
interlaced

Ignored.

compare($image2)
clip($x1,$y1,$x2,$y2)
boundsSafe($x,$y)
GD::Polygon, GD::Polyline
GD::Simple

BUGS

Top

Patches/suggestions are welcome.

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.

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.

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.

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.

SetStyle

Does not support gdStyledBrushed.

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.

SEE ALSO

Top

Cairo, GD, GD::SVG (includes extensive discussion of why translating GD to a vector library is difficult).

http://cairographics.org/manual/

AUTHOR

Top

Tim D Brody, <tdb01r@ecs.soton.ac.uk>

COPYRIGHT AND LICENSE

Top


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 = \&copyResized;
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__