KW Perl Mongers Perl Modules: A Look Under the Hood #17

Code

        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;   

<< Previous | Index | Next >> Copyright © 2004 Daniel Allen