package GD::Text::Arc;
$GD::Text::Arc::VERSION = '0.01';
use strict;
use GD 1.2; # fails if version < 1.20 (no TrueType support)
use GD::Text;
use Carp;
use constant PI => 4 * atan2(1, 1);
@GD::Text::Arc::ISA = qw(GD::Text);
sub new
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $gd = shift;
ref($gd) and $gd->isa('GD::Image')
or croak "Not a GD::Image object";
my $self = $class->SUPER::new() or return;
$self->{gd} = $gd;
$self->_init();
$self->set(@_);
bless $self => $class;
}
# fill these in with defaults
my %defaults = (
align => 'left',
angle => 0,
font => '',
text => '',
orientation => 'clockwise',
side => 'outside',
compress_factor => .9,
points_to_pixels_factor => .8
);
sub _init
{
my $self = shift;
while (my ($k, $v) = each(%defaults))
{
$self->{$k} = $v;
}
$self->{colour} = $self->{gd}->colorsTotal - 1,
$self->{color} = $self->{colour};
$self->{center_x} = ($self->{gd}->getBounds())[0] / 2;
$self->{center_y} = ($self->{gd}->getBounds())[1] / 2;
$self->{radius} = _min( $self->{center_x}, $self->{center_y});
}
sub set
{
my $self = shift;
$@ = "Incorrect attribute list (one left over)", return if @_%2;
my %args = @_;
my @super;
foreach (keys %args)
{
/^align/ and do {
$self->set_align($args{$_});
next;
};
/^angle/ and do {
$self->set_angle($args{$_});
next;
};
/^center_x/ and do {
$self->{center_x} = $args{$_};
next;
};
/^center_y/ and do {
$self->{center_y} = $args{$_};
next;
};
/^orientation/ and do {
$self->{orientation} = $args{$_};
next;
};
/^side/ and do {
$self->{side} = $args{$_};
next;
};
/^radius/ and do {
$self->{radius} = $args{$_};
next;
};
/^colou?r$/ and do {
$self->{colour} = $args{$_};
$self->{color} = $args{$_};
next;
};
# Save anything unknown to pass off to SUPER class
push @super, $_, $args{$_};
}
$self->SUPER::set(@super);
}
# get is inherited unchanged
# redefine these methods which use non-TrueType fonts
{
no warnings;
sub gdTinyFont { carp "Not a TrueType font" }
sub gdSmallFont { carp "Not a TrueType font" }
sub gdMediumBoldFont { carp "Not a TrueType font" }
sub gdLargeFont { carp "Not a TrueType font" }
sub gdGiantFont { carp "Not a TrueType font" }
sub _set_builtin_font { carp "Not a TrueType font" }
}
# FIXME: these two methods are not very useful yet.
sub set_align
{
my $self = shift;
local $_ = shift or return;
if (/^left/ || /^center/ || /^right/)
{
$self->{align} = $_;
return $_;
}
else
{
carp "Illegal alignment: $_";
return;
}
}
sub set_angle
{
my $self = shift;
local $_ = shift or return;
if (undef or /\d\.?\d*/ )
{
$self->{angle} = $_;
return $_;
}
else
{
carp "Not numeric angle: $_";
return;
}
}
sub draw
{
my $self = shift;
$@ = "No text set", return unless $self->{text};
$@ = "No colour set", return unless$self->{colour};
$@ = "No font set", return unless $self->{font};
$@ = "No font size set", return unless $self->{ptsize};
my $angle = $self->get('angle') || 0;
my $colour = $self->get('colour');
my $font = $self->get('font');
my $fontsize = $self->get('ptsize');
my $string = $self->get('text');
my $centerX = $self->get('center_x') ||
($self->{gd}->getBounds())[0] / 2;
my $centerY = $self->get('center_y') ||
($self->{gd}->getBounds())[1] / 2;
my $r = $self->get('radius') || _min($centerX, $centerY);
my $side = $self->get('side') || 'outside';
# orientation default == blank == counterclockwise == -1
my $orientation = ($self->get('orientation') eq 'clockwise') ? 1: -1;
# correct radius for height of letters if counterclockwise and outside
$r += ($fontsize* $self->get('points_to_pixels_factor'))
if ($orientation <0 and $side eq 'outside');
# correct radius the other way if clockwise and inside
$r -= ($fontsize* $self->get('points_to_pixels_factor'))
if ($orientation >0 and $side ne 'outside');
# correct spacing between letters
my $compressFactor = $self->get('compress_factor');
my @letters = split //, $string;
my @widths = $self->get_widths();
#######################################################################:
#
# GD allows .ttf text a position (x,y), and an angle rotation (theta).
# both are measured from the lower-left corner of the string.
#
# We want to draw each letter separately to approximate a smooth curve.
# Ideally, the position (x,y) would be from the center of the letter.
# Since it is from the corner, plotting each letter as-is will look
# jaggy, because of the difference in position and angle. To fix
# this we can either adjust (x,y) or theta.
#
# theta seemed simpler to adjust.
#
# thetaL = thetaN - (1/2 radWidth * orientation)
#
# where:
# angles are measured from 0-o'clock, positive
# increasing clockwise.
#
# thetaN = the angle of the letter to calculate its position.
# thetaL = the angle to draw the letter at.
# radWidth = letter width in radians
# orientation = -1 for counterclockwise, +1 for clockwise
#######################################################################
# calculate start angle for positioning (x,y) with thetaN
my $thetaN = 0;
foreach my $n (@widths) {
$thetaN += ($n * $orientation);
}
$thetaN /= 2; # 1/2 width, in pixels
$thetaN /= $r; # ..in radians,
$thetaN /= $compressFactor; # ..with compression factor
$thetaN += PI if ($orientation < 0);
# draw each letter
foreach my $n (0..$#letters) {
my $radWidth = ($widths[$n]) / ($r * $compressFactor);
my $thetaL = $thetaN - ($radWidth/2 * $orientation) ;
$thetaL = $thetaL - PI if ($orientation < 0);
my $xN = $centerX - $r * sin($thetaN);
my $yN = $centerY - $r * cos($thetaN);
$self->{gd}->stringFT($colour, $font, $fontsize, $thetaL,
$xN, $yN, $letters[$n]) || return 0;
$thetaN -= ($radWidth * $orientation);
}
return 1;
}
#
# get_widths - in array context, return a list of character-widths in pixels.
# in scalar context, return a total width of the string.
sub get_widths {
my $self = shift;
my @widths;
my $total;
my @letters = split //, $self->get('text');
#######################################################################
# for character x, width(x) is not useful because .ttf fonts
# account for kerning. width(x1) + width(x2) + width(x3)
# is categorically different from width(x1.x2.x3).
#
# By process of elimination: an OK formula to find width(x2):
# assume x1 is a space, and perform:
# width(x1.x2.x3) - (width(x1) + width(x3)).
#
# If x2 is a space, make it wider; if it is (A|C|V) make it narrower.
#
# Whew. This should probably be simplified.
#######################################################################
foreach my $n (0..$#letters) {
my $nextLetter = $letters[$n+1] || " ";
my $lastLetter = " ";
my $thiswidth = ($self->width($lastLetter.$letters[$n].$nextLetter)
-
($self->width($lastLetter) +
$self->width($nextLetter)));
$thiswidth -=2 if ($letters[$n] =~ /[AVC]/);
$thiswidth +=2 if ($letters[$n] =~ / /);
push @widths, $thiswidth;
$total += $thiswidth;
}
return (wantarray ? @widths : $total);
}
#
# get_height - return the best guess for the height of the letters in pixels.
sub get_height {
my $self = shift;
return $self->get('ptsize') * $self->get('points_to_pixels_factor');
}
sub _min {
return ($_[0] < $_[1] ? $_[0] : $_[1]);
}
1;
|