File Coverage

blib/lib/GD/Text/Arc.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package GD::Text::Arc;
2              
3             $GD::Text::Arc::VERSION = '0.02';
4              
5 1     1   2440 use strict;
  1         2  
  1         52  
6 1     1   1605 use GD 1.2; # fails if version < 1.20 (no TrueType support)
  0            
  0            
7             use base qw(GD::Text);
8             use Carp;
9              
10             use constant PI => 4 * atan2(1, 1);
11              
12             sub new
13             {
14             my $proto = shift;
15             my $class = ref($proto) || $proto;
16             my $gd = shift;
17             ref($gd) and $gd->isa('GD::Image')
18             or croak "Not a GD::Image object";
19             my $self = $class->SUPER::new() or return;
20             $self->{gd} = $gd;
21             bless $self => $class;
22             $self->_init();
23             $self->set(@_);
24             return $self;
25             }
26              
27             # fill these in with defaults
28              
29             my %defaults = (
30             align => 'left',
31             angle => 0,
32             font => '',
33             text => '',
34             orientation => 'clockwise',
35             side => 'outside',
36             compress_factor => .9,
37             points_to_pixels_factor => .8
38              
39             );
40              
41             sub _init
42             {
43             my $self = shift;
44             while (my ($k, $v) = each(%defaults))
45             {
46             $self->{$k} = $v;
47             }
48             $self->{colour} = 1; # if indexed, 1 is the first non-background color.
49             # if truecolor, (0,0,1) is nearly black.
50             $self->{color} = $self->{colour};
51             $self->{center_x} = ($self->{gd}->getBounds())[0] / 2;
52             $self->{center_y} = ($self->{gd}->getBounds())[1] / 2;
53             $self->{radius} = _min( $self->{center_x}, $self->{center_y});
54             }
55              
56             sub set
57             {
58             my $self = shift;
59             $@ = "Incorrect attribute list (one left over)", return if @_%2;
60             my %args = @_;
61             my @super;
62              
63             foreach (keys %args)
64             {
65             /^align/ and do {
66             $self->set_align($args{$_});
67             next;
68             };
69             /^angle/ and do {
70             $self->set_angle($args{$_});
71             next;
72             };
73             /^center_x/ and do {
74             $self->{center_x} = $args{$_};
75             next;
76             };
77             /^center_y/ and do {
78             $self->{center_y} = $args{$_};
79             next;
80             };
81             /^orientation/ and do {
82             $self->{orientation} = $args{$_};
83             next;
84             };
85             /^side/ and do {
86             $self->{side} = $args{$_};
87             next;
88             };
89             /^radius/ and do {
90             $self->{radius} = $args{$_};
91             next;
92             };
93              
94             /^colou?r$/ and do {
95             $self->{colour} = $args{$_};
96             $self->{color} = $args{$_};
97             next;
98             };
99             # Save anything unknown to pass off to SUPER class
100             push @super, $_, $args{$_};
101             }
102              
103             $self->SUPER::set(@super);
104             }
105              
106             # get is inherited unchanged
107              
108             # redefine these methods which use non-TrueType fonts
109             {
110             no warnings;
111             sub gdTinyFont { carp "Not a TrueType font" }
112             sub gdSmallFont { carp "Not a TrueType font" }
113             sub gdMediumBoldFont { carp "Not a TrueType font" }
114             sub gdLargeFont { carp "Not a TrueType font" }
115             sub gdGiantFont { carp "Not a TrueType font" }
116             sub _set_builtin_font { carp "Not a TrueType font" }
117             }
118              
119             # FIXME: these two methods are not very useful yet.
120             sub set_align
121             {
122             my $self = shift;
123             local $_ = shift or return;
124              
125             if (/^left/ || /^center/ || /^right/)
126             {
127             $self->{align} = $_;
128             return $_;
129             }
130             else
131             {
132             carp "Illegal alignment: $_";
133             return;
134             }
135             }
136              
137             sub set_angle
138             {
139             my $self = shift;
140             local $_ = shift or return;
141              
142             if (undef or /\d\.?\d*/ )
143             {
144             $self->{angle} = $_;
145             return $_;
146             }
147             else
148             {
149             carp "Not numeric angle: $_";
150             return;
151             }
152             }
153              
154             sub draw
155             {
156             my $self = shift;
157              
158             $@ = "No text set", return unless $self->{text};
159             $@ = "No colour set", return unless$self->{colour};
160             $@ = "No font set", return unless $self->{font};
161             $@ = "No font size set", return unless $self->{ptsize};
162              
163             my $angle = $self->get('angle') || 0;
164             my $colour = $self->get('colour');
165             my $font = $self->get('font');
166             my $fontsize = $self->get('ptsize');
167             my $string = $self->get('text');
168             my $centerX = $self->get('center_x') ||
169             ($self->{gd}->getBounds())[0] / 2;
170             my $centerY = $self->get('center_y') ||
171             ($self->{gd}->getBounds())[1] / 2;
172             my $r = $self->get('radius') || _min($centerX, $centerY);
173             my $side = $self->get('side') || 'outside';
174              
175             # orientation default == blank == counterclockwise == -1
176             my $orientation = ($self->get('orientation') eq 'clockwise') ? 1: -1;
177              
178             # correct radius for height of letters if counterclockwise and outside
179             $r += ($fontsize* $self->get('points_to_pixels_factor'))
180             if ($orientation <0 and $side eq 'outside');
181            
182             # correct radius the other way if clockwise and inside
183             $r -= ($fontsize* $self->get('points_to_pixels_factor'))
184             if ($orientation >0 and $side ne 'outside');
185              
186              
187             # correct spacing between letters
188             my $compressFactor = $self->get('compress_factor');
189              
190             my @letters = split //, $string;
191             my @widths = $self->get_widths();
192              
193              
194             #######################################################################:
195             #
196             # GD allows .ttf text a position (x,y), and an angle rotation (theta).
197             # both are measured from the lower-left corner of the string.
198             #
199             # We want to draw each letter separately to approximate a smooth curve.
200             # Ideally, the position (x,y) would be from the center of the letter.
201             # Since it is from the corner, plotting each letter as-is will look
202             # jaggy, because of the difference in position and angle. To fix
203             # this we can either adjust (x,y) or theta.
204             #
205             # theta seemed simpler to adjust.
206             #
207             # thetaL = thetaN - (1/2 radWidth * orientation)
208             #
209             # where:
210             # angles are measured from 12-o'clock, positive
211             # increasing clockwise.
212             #
213             # thetaN = the angle of the letter to calculate its position.
214             # thetaL = the angle to draw the letter at.
215             # radWidth = letter width in radians
216             # orientation = -1 for counterclockwise, +1 for clockwise
217             #######################################################################
218              
219             # calculate start angle for positioning (x,y) with thetaN
220             my $thetaN = 0;
221             foreach my $n (@widths) {
222             $thetaN += ($n * $orientation);
223             }
224             $thetaN /= 2; # 1/2 width, in pixels
225             $thetaN /= $r; # ..in radians,
226             $thetaN /= $compressFactor; # ..with compression factor
227              
228             $thetaN += PI if ($orientation < 0);
229              
230             # draw each letter
231             foreach my $n (0..$#letters) {
232              
233             my $radWidth = ($widths[$n]) / ($r * $compressFactor);
234             my $thetaL = $thetaN - ($radWidth/2 * $orientation) ;
235             $thetaL = $thetaL - PI if ($orientation < 0);
236              
237             my $xN = $centerX - $r * sin($thetaN);
238             my $yN = $centerY - $r * cos($thetaN);
239              
240             $self->{gd}->stringFT($colour, $font, $fontsize, $thetaL,
241             $xN, $yN, $letters[$n]) || return 0;
242              
243             $thetaN -= ($radWidth * $orientation);
244             }
245             return 1;
246             }
247              
248             #
249             # get_widths - in array context, return a list of character-widths in pixels.
250             # in scalar context, return a total width of the string.
251              
252             sub get_widths {
253             my $self = shift;
254              
255             my @widths;
256             my $total;
257             my @letters = split //, $self->get('text');
258              
259             #######################################################################
260             # for character x, width(x) is not useful because .ttf fonts
261             # account for kerning. width(x1) + width(x2) + width(x3)
262             # is categorically different from width(x1.x2.x3).
263             #
264             # By process of elimination: an OK formula to find width(x2):
265             # assume x1 is a space, and perform:
266             # width(x1.x2.x3) - (width(x1) + width(x3)).
267             #
268             # If x2 is a space, make it wider; if it is (A|C|V) make it narrower.
269             #
270             # Whew. This should probably be simplified.
271             #######################################################################
272              
273             foreach my $n (0..$#letters) {
274             my $nextLetter = $letters[$n+1] || " ";
275             my $lastLetter = " ";
276              
277             my $thiswidth = ($self->width($lastLetter.$letters[$n].$nextLetter)
278             -
279             ($self->width($lastLetter) +
280             $self->width($nextLetter)));
281              
282             $thiswidth -=2 if ($letters[$n] =~ /[AVC]/);
283             $thiswidth +=2 if ($letters[$n] =~ / /);
284             push @widths, $thiswidth;
285             $total += $thiswidth;
286             }
287              
288             return (wantarray ? @widths : $total);
289             }
290              
291             #
292             # get_height - return the best guess for the height of the letters in pixels.
293              
294             sub get_height {
295             my $self = shift;
296              
297             return $self->get('ptsize') * $self->get('points_to_pixels_factor');
298             }
299              
300             sub _min {
301             return ($_[0] < $_[1] ? $_[0] : $_[1]);
302             }
303            
304             =head1 NAME
305              
306             GD::Text::Arc - draw TrueType text along an arc.
307              
308             =head1 SYNOPSIS
309              
310             use GD::Text::Arc;
311              
312             my $image = GD::Image->new(600,500);
313              
314             my $gray = $image->colorAllocate(75,75,75);
315             my $boldfont = "Adventure.ttf";
316             my $text = "here's a line.";
317              
318             my $ta = GD::Text::Arc->new($image,
319             colour => $gray,
320             ptsize => $size,
321             font => $boldfont,
322             radius => $radius,
323             center_x => $centerX,
324             center_y => $centerY,
325             text => $text,
326             side => 'inside'
327             orientation => 'clockwise'
328             );
329              
330             $ta->draw;
331              
332             $ta->set('color', $red);
333             $ta->set('ptsize', $huge);
334             $ta->set('orientation', 'counterclockwise');
335              
336             $ta->draw;
337              
338             =head1 DESCRIPTION
339              
340             This module provides a way to draw TrueType text along a curve (such as
341             around the bottom or top of a circle). It is to be used with GD::Text
342             (version > 1.20) and GD graphics objects.
343              
344             =head1 METHODS
345              
346             =head2 GD::Text->new($gd_object, attrib => value, ... )
347              
348             Create a new object. The first argument has to be a valid GD::Image
349             object. See the C method for attributes.
350              
351             =head2 $gd_text->set_font( font, size )
352              
353             Set the font to use for the string, using absolute or relative
354             TrueType font names. See L<"GD::Text"> and L<"GD::Text::font_path">
355             for details.
356              
357             =head2 $gd_text->set_text('some text')
358              
359             Set the text to operate on.
360             Returns true on success and false on error.
361              
362             =head2 $gd_text->set( attrib => value, ... )
363              
364             The set method is the preferred way to set attributes.
365             Valid attributes are:
366              
367             =over 4
368              
369             =item text
370              
371             The text to operate on, see also C.
372              
373             =item font, ptsize
374              
375             The font to use and the point size. Also see C.
376              
377             =item colour, color
378              
379             Synonyms. The colour to use to draw the string. This should be the index
380             of the colour in the GD::Image object's palette. For a true-color GD::Image,
381             the default value is nearly black: (0,0,1). For indexed images, the default
382             value is the first non-background colour in the GD object's palette at the
383             time of the creation of C<$gd_text>.
384              
385             =item center_x, center_y
386              
387             The center point for the circle. Defaults to 1/2 the width and height
388             of the containing GD object.
389              
390             =item radius
391              
392             The radius of the circle, which is either drawn outside or inside the
393             text (depending on C attribute; see below). The default
394             radius is the lesser of center_x or center_y.
395              
396             =item orientation
397              
398             Direction of the text. Valid values: clockwise, counterclockwise.
399             Default is counterclockwise (that is, written along the bottom of the
400             circle, such as from 8-oclock to 5-oclock on a clock face.)
401              
402             =item side
403              
404             Whether the text is drawn inside the radius of the circle or outside.
405             Valid values: inside, outside. Default is outside.
406              
407             =item align
408              
409             Not implemented yet; but will allow text to be left- or right- aligned
410             around a point on the circle. At present, text is center-aligned..
411              
412             =item angle
413              
414             Not implemented yet; but will set the point on the circle around which
415             text is centered (or started or ended, depending on alignment). At
416             present, angle is set to 0 (top of the circle) for clockwise
417             orientation, and PI (bottom of the circle) for counterclockwise
418             orientation.
419              
420             =item compress_factor, points_to_pixels_factor
421              
422             These parameters were found by experimentation and seem to apply to
423             any fonts and point-sizes I've tried, but might need adjusting under
424             some situations.
425              
426             compress_factor adjusts spacing between characters. It is .9 by
427             default; a larger value reduces the space between characters.
428              
429             points_to_pixels_factor adjusts the radius (by a fraction of the
430             point-size) to compensate for the height of letters.
431             It is .8 by default; a larger value increases the radius.
432              
433             =back
434              
435             Returns true on success, false on any error, even if it was partially
436             successful. When an error is returned, no guarantees are given about
437             the correctness of the attributes.
438              
439             =head2 $gd_text->get( attrib, ... )
440              
441             Get the value of an attribute.
442             Return a list of the attribute values in list context, and the value of
443             the first attribute in scalar context.
444              
445             The attributes that can be retrieved are all the ones that can be set,
446             plus those described in L<"GD::Text">.:
447              
448             Note that unlike with GD::Text::Align, you can get() both 'color' and
449             'colour'. Vive la difference!
450              
451             =head2 $gd_text->get_widths()
452              
453             In array context, returns a list of character-widths for the text to
454             be drawn. In scalar context, return the total width of the string
455             measured in pixels. Because the way the characters are drawn (to
456             acount for kerning between adjacent characters), this is more accurate
457             than $gd_text->width. This is mostly an internal method, but might be
458             useful to make sure your text is shorter than, say, PI * radius
459             characters.
460              
461             =head2 $gd_text->draw()
462              
463             Draw the string according to coordinates, radius, orientation, and font.
464             Returns true on success, false on any error.
465              
466             =head1 NOTES
467              
468             =head2 GD::Text::Arc::PI
469              
470             Owing to how 'use constant' works, for free, you get a definition of Pi
471             that is as good as your floating-point math. You can also access it
472             as method C<$gd_text-EPI>.
473              
474             =head1 BUGS
475              
476             Probably.
477              
478             I haven't implemented align() or angle() methods yet;
479             for now, align = center and angle = 0. I'm not sure this module will work
480             perfectly with all fonts. I've tried it with a few dozen, which look more
481             or less the way I wanted them to.
482              
483             Suggestions gratefully welcomed.
484              
485             =head1 COPYRIGHT
486              
487             Copyright (C) 2004
488             Daniel Allen Eda@coder.comE. All rights reserved.
489              
490             This program is free software; you can redistribute it and/or
491             modify it under the same terms as Perl itself.
492              
493             See F
494              
495             =head1 SEE ALSO
496              
497             GD(3), GD::Text(3), GD::Text::Wrap(3), GD::Text::Align(3)
498              
499             =cut
500              
501             1;