File Coverage

blib/lib/Treemap/Output/Imager.pm
Criterion Covered Total %
statement 54 135 40.0
branch 4 58 6.9
condition 10 44 22.7
subroutine 10 16 62.5
pod 2 9 22.2
total 80 262 30.5


line stmt bran cond sub pod time code
1             package Treemap::Output::Imager;
2              
3 1     1   66452 use 5.006;
  1         3  
  1         32  
4 1     1   4 use strict;
  1         1  
  1         26  
5 1     1   4 use warnings;
  1         1  
  1         30  
6 1     1   4 use Carp;
  1         1  
  1         1679  
7              
8             require Exporter;
9             require Treemap::Output;
10             require Imager;
11             require Imager::Font;
12             require Imager::Color;
13              
14             our @ISA = qw( Treemap::Output Exporter );
15             our @EXPORT_OK = ( );
16             our @EXPORT = qw( );
17             our $VERSION = '0.01';
18              
19              
20             # ------------------------------------------
21             # Methods:
22             # ------------------------------------------
23             sub new
24             {
25 1     1 0 12 my $classname = shift;
26 1         12 my $self = $classname->SUPER::new( @_ ); # Call parent constructor
27 1         5 $self->_init( @_ ); # Initialize child variables
28 1         3 return $self;
29             }
30              
31             sub _init
32             {
33 1     1   2 my $self = shift;
34 1   50     10 $self->{WIDTH} = $self->{WIDTH} || 400;
35 1   50     5 $self->{HEIGHT} = $self->{HEIGHT} || 300;
36 1   50     6 $self->{PADDING} = $self->{PADDING} || 5;
37 1   50     6 $self->{SPACING} = $self->{SPACING} || 5;
38 1   50     6 $self->{BORDER_COLOUR} = $self->{BORDER_COLOUR} || "#000000";
39 1   50     9 $self->{FONT_COLOUR} = $self->{FONT_COLOUR} || "#000000";
40 1   50     7 $self->{MIN_FONT_SIZE} = $self->{MIN_FONT_SIZE} || 5;
41 1   50     8 $self->{FONT_FILE} = $self->{FONT_FILE} || "ImUgly.ttf";
42 1   50     6 $self->{TEXT_DEBUG} = $self->{TEXT_DEBUG} || 0;
43 1   50     35 $self->{DEBUG} = $self->{DEBUG} || 0;
44              
45             ## aggregate resource variables:
46 1         11 $self->{IMAGE} = Imager->new( xsize => $self->{WIDTH},
47             ysize => $self->{HEIGHT} );
48 1         82 $self->{ALPHA} = Imager->new();# xsize => $self->{WIDTH},
49             # ysize => $self->{HEIGHT},
50             # channels => 4 );
51              
52 1 50       18 $self->{DEBUG} && print STDERR "Created a new image object.\n";
53              
54             # init cache with border colour and font colours:
55 1         11 $self->{COLOUR_CACHE}->{$self->{BORDER_COLOUR}} = Imager::Color->new(
56             $self->{BORDER_COLOUR} );
57 1         41 $self->{ALPHA_FONT} = Imager::Color->new( 0, 0, 0, 110 );
58 1         24 $self->{SOLID_FONT} = Imager::Color->new( $self->{FONT_COLOUR} );
59              
60 1         25 $self->{FONT} = Imager::Font->new(
61             file => $self->{FONT_FILE},
62             color => $self->{SOLID_FONT},
63             aa => 1,
64             type => 'ft2' );
65              
66             # for profiling:
67 1         585 $self->{font_iters} = 0;
68             }
69              
70             sub save
71             {
72 0     0 0 0 my $self = shift;
73 0         0 my ( $filename ) = @_;
74 0         0 $self->{IMAGE}->write( file=>$filename );
75 0         0 return 1;
76             }
77              
78             sub rect
79             {
80 1     1 1 569 my $self = shift;
81 1         2 my ( $x1, $y1, $x2, $y2, $colour ) = @_;
82 1         2 my $area = ( $x2 - $x1 ) * ( $y2 - $y1 );
83              
84             # cache any colour object that is created; Imager::Color is an expensive
85             # operation
86 1 50       7 if ( ! $self->{COLOUR_CACHE}->{$colour} )
87             {
88 0         0 $self->{COLOUR_CACHE}->{$colour} = Imager::Color->new( $colour );
89             }
90              
91             # draw inner box (filled):
92 1         8 $self->{IMAGE}->box( color => $self->{COLOUR_CACHE}->{$colour},
93             xmin => $x1,
94             ymin => $y1,
95             xmax => $x2,
96             ymax => $y2,
97             filled => 1 );
98            
99 1 50       58 return 1 if ( $area < 3 );
100              
101             # draw outer "outline" box (stroked):
102 1         10 $self->{IMAGE}->box( color => $self->{COLOUR_CACHE}->{$self->{BORDER_COLOUR}},
103             xmin => $x1,
104             ymin => $y1,
105             xmax => $x2,
106             ymax => $y2,
107             filled => 0 );
108              
109 1         32 return 1;
110             }
111              
112             ## text label drawing method:
113             # new "guessing" method
114             # see old method below -- fishy
115             sub text
116             {
117 1     1 1 3 my $self = shift;
118 1         2 my ( $x1, $y1, $x2, $y2, $text, $children ) = @_;
119              
120 1         5 my $x = $x1 + ( $x2 - $x1 ) / 2;
121 1         2 my $y = $y1 + ( $y2 - $y1 ) / 2;
122              
123 1         2 my $width = abs( $x2 - $x1 );
124 1         1 my $height = abs( $y2 - $y1 );
125              
126             # It's not worth trying to print text in here, it's too narrow
127 1 50       8 return 1 if ( $width < 20 );
128              
129             # It's not worth trying to print text in here, it's too short
130 0 0       0 return 1 if ( $height < 10 );
131              
132 0         0 my $size = $self->_font_fit( $width, $height, $text );
133              
134 0 0       0 return 1 if ( ! $size );
135              
136             # write string into image:
137 0         0 my @metrix = $self->{FONT}->bounding_box(
138             string => $text,
139             size => $size,
140             canon => 1 );
141              
142             # alpha transparent fonts, using rub-throughs
143 0 0       0 if( $children )
144             {
145             # $x -= $metrix[2]/2;
146             # $y += $metrix[3]/3;
147             # $self->{ALPHA}->img_set( xsize => $self->{WIDTH},
148             # ysize => $self->{HEIGHT},
149             # channels => 4 );
150             # $self->{ALPHA}->string(
151             # font => $self->{FONT},
152             # text => $text,
153             # x => $x,
154             # y => $y,
155             # color => $self->{ALPHA_FONT},
156             # size => $size );
157             #
158             # $self->{IMAGE}->rubthrough( src => $self->{ALPHA} );
159 0         0 $x -= $metrix[2]/2;
160 0         0 $y -= $metrix[3]/2;
161 0         0 $self->{ALPHA}->img_set( xsize => $metrix[2],
162             ysize => $metrix[3],
163             channels => 4 );
164 0         0 $self->{ALPHA}->string(
165             font => $self->{FONT},
166             text => $text,
167             x => 0,
168             y => $metrix[3]+$metrix[1],
169             color => $self->{ALPHA_FONT},
170             size => $size );
171              
172 0         0 $self->{IMAGE}->rubthrough( src => $self->{ALPHA}, tx=>$x, ty=>$y );
173             }
174             else
175             {
176 0         0 $x -= $metrix[2]/2;
177 0         0 $y += $metrix[3]/3;
178             # position at top, in black:
179              
180             # try to get a reasonable top-padding, if available:
181 0         0 my $top_pad = int(( $height - $metrix[5] ) * 0.1 );
182 0 0       0 $top_pad = ( $top_pad > 5 ) ? 5 : $top_pad;
183 0         0 $y = $y1 + $metrix[5] + $top_pad;
184              
185 0         0 $self->{IMAGE}->string(
186             font => $self->{FONT},
187             text => $text,
188             x => $x,
189             y => $y,
190             color => $self->{SOLID_FONT},
191             size => $size );
192             }
193 0         0 return 1;
194             }
195              
196             ## font fitting algorhythm
197             # moved to seperate function, merged with guessing function
198             sub _font_fit
199             {
200 0     0   0 my $self = shift;
201 0         0 my ( $width, $height, $text ) = @_;
202 0         0 my $DEBUG = $self->{TEXT_DEBUG};
203            
204 0 0 0     0 return unless $text && ( length( $text ) ) && $height && $width;
      0        
      0        
205              
206 0         0 my $local_iters = 0;
207              
208             # Search for suitable font size
209 0 0       0 $self->{TEXT_DEBUG} && print STDERR "$text:\n";
210              
211             # fetch a guess at the starting point:
212             # if not initialized:
213 0 0       0 unless ( $self->{ACWPP} )
214             {
215             # find average character width per point
216 0         0 $self->{ACWPP} = $self->_calc_avg_char_weight_per_pt();
217 0 0       0 croak( "Initialization of font fitting algorhythm failed." )
218             unless ( $self->{ACWPP} );
219             }
220              
221 0         0 my $size = int( ( $width / length( $text ) ) / $self->{ACWPP} );
222            
223             # because it is guaranteed to be not worth it:
224 0 0       0 return if ( $size <= ( $self->{MIN_FONT_SIZE} - 2 ) );
225              
226             # test guess:
227 0         0 my @metrix = $self->{FONT}->bounding_box(
228             string => $text,
229             size => $size,
230             canon => 1 );
231              
232             # two corrective measures:
233            
234             # 1. if the width fits, but not the height, then we have a height
235             # restricted case. These tend to be expensive, so we "correct" our
236             # guess.
237 0 0 0     0 if (( $metrix[2] <= $width ) && ( $metrix[3] > $height ))
    0          
238             {
239             # if there is a major difference in height, correct guess
240 0 0       0 if (( abs( $height - $metrix[3] ) / $height ) * $size >= 3 )
241             {
242 0         0 $self->{font_iters}++; $local_iters++; # track iterations
  0         0  
243            
244 0 0       0 $self->{TEXT_DEBUG} && print STDERR "\tHeight restricted, changing $size =>";
245 0         0 $size = int( $size * ( $height / $metrix[3] ));
246 0 0       0 $self->{TEXT_DEBUG} && print STDERR "$size.\n";
247            
248 0         0 @metrix = $self->{FONT}->bounding_box(
249             string => $text,
250             size => $size,
251             canon => 1 );
252             }
253             }
254             # 2. if our guess is way off width-wise, correct:
255             # if a correction would yeild a size change of more than 3,
256             # it is obviously worth it.
257             elsif ( ( abs( $width - $metrix[2] ) / $width ) * $size >= 3 )
258             {
259 0         0 $self->{font_iters}++; $local_iters++; # track iterations
  0         0  
260 0 0       0 $self->{TEXT_DEBUG} && print STDERR "\tOff by 3pts+, changing $size =>";
261 0         0 $size = int( $size * ( $width / $metrix[2] ));
262 0 0       0 $self->{TEXT_DEBUG} && print STDERR "$size.\n";
263 0         0 @metrix = $self->{FONT}->bounding_box(
264             string => $text,
265             size => $size,
266             canon => 1 );
267             }
268              
269             # if our guess was too large, try smaller values until there is a fit:
270 0 0 0     0 if (( $metrix[2] > $width ) || ( $metrix[3] > $height ))
    0 0        
271             {
272 0 0       0 $self->{TEXT_DEBUG} && print STDERR "\tGuess ($size) too large.\n";
273 0   0     0 while ( ( $metrix[2] > $width ) || ( $metrix[3] > $height ) )
274             {
275 0         0 $self->{font_iters}++; $local_iters++; # track iterations
  0         0  
276 0         0 $size--;
277              
278 0 0       0 return if ( $size < 5 );
279              
280 0         0 @metrix = $self->{FONT}->bounding_box(
281             string => $text,
282             size => $size,
283             canon => 1 );
284             }
285             }
286             # if our guess is too small, try larger values until there is a -no- fit:
287             elsif ( ( $metrix[2] <= $width ) && ( $metrix[3] <= $height ))
288             {
289 0 0       0 $self->{TEXT_DEBUG} && print STDERR "\tGuess ($size) fits, adjusting.\n";
290 0   0     0 while ( ( $metrix[2] <= $width ) && ( $metrix[3] <= $height ) )
291             {
292 0         0 $self->{font_iters}++; $local_iters++; # track iterations
  0         0  
293            
294 0         0 $size++;
295 0 0       0 $size++ if ( $size > 50 ); # grow a bit faster for big fonts
296            
297 0         0 @metrix = $self->{FONT}->bounding_box(
298             string => $text,
299             size => $size,
300             canon => 1 );
301             }
302 0         0 $size--; # because this overshoots
303             }
304              
305 0 0       0 $self->{TEXT_DEBUG} && print STDERR "\t$local_iters :: " . $self->{font_iters} . " => $size\n";
306              
307 0         0 $size = int( $size * 0.9 ); # reduce size to fit comfortably
308              
309 0 0       0 return if ( $size < $self->{MIN_FONT_SIZE} );
310              
311 0         0 return $size;
312             }
313              
314             ###############################################
315             #
316             # private: _calc_avg_char_weight_per_pt
317             # input: none
318             # output: ACWPP
319             #
320             # pardon the size of this function name
321             # it only needs to be called in one place
322             #
323             sub _calc_avg_char_weight_per_pt
324             {
325 0     0   0 my $self = shift;
326 0         0 my $wieghting_string = "rstlnaei0RST.-";
327 0         0 my $sample_size = 50;
328              
329             # get metrix for sample:
330 0         0 my @metrix = $self->{FONT}->bounding_box(
331             string => $wieghting_string,
332             size => $sample_size,
333             canon => 1 );
334              
335 0         0 my $sample_width = $metrix[2];
336 0 0       0 return unless ( $sample_width );
337              
338             # avg width per character per point
339 0         0 return ( $sample_width / length( $wieghting_string ) / $sample_size );
340             }
341              
342              
343             sub width
344             {
345 1     1 0 3 my $self = shift;
346 1         5 return $self->{WIDTH};
347             }
348              
349             sub height
350             {
351 1     1 0 1 my $self = shift;
352 1         4 return $self->{HEIGHT};
353             }
354              
355             sub font_height
356             {
357 0     0 0   my $self = shift;
358 0           return "12";
359             }
360              
361             sub padding
362             {
363 0     0 0   my $self = shift;
364 0           return $self->{PADDING};
365             }
366              
367             sub spacing
368             {
369 0     0 0   my $self = shift;
370 0           return $self->{SPACING};
371             }
372              
373             1;
374              
375             __END__