File Coverage

blib/lib/Image/Placeholder.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 Image::Placeholder;
2              
3 4     4   149812 use Modern::Perl;
  4         15542  
  4         39  
4 4     4   10169 use Moose;
  0            
  0            
5             use MooseX::Method::Signatures;
6             use MooseX::FollowPBP;
7              
8             use GD;
9             use version;
10             our $VERSION = qv( 1.0.0 );
11              
12             use constant TRUE_COLOUR => 1;
13             use constant MAX_TRANSPARENCY => 127;
14             use constant MAX_USABLE_RATIO => 0.85;
15              
16             has background_colour => (
17             isa => 'Str',
18             is => 'ro',
19             default => 'ddd',
20             );
21             has font => (
22             isa => 'Str',
23             is => 'ro',
24             default => 'Museo Sans',
25             );
26             has height => (
27             isa => 'Int',
28             is => 'ro',
29             );
30             has line_colour => (
31             isa => 'Str',
32             is => 'ro',
33             default => '444',
34             );
35             has size => (
36             isa => 'Str',
37             is => 'ro',
38             );
39             has text => (
40             isa => 'Str',
41             is => 'rw',
42             );
43             has text_colour => (
44             isa => 'Str',
45             is => 'ro',
46             default => '36f',
47             );
48             has transparent => (
49             isa => 'Bool',
50             is => 'ro',
51             default => 0,
52             );
53             has width => (
54             isa => 'Int',
55             is => 'ro',
56             default => '300',
57             );
58              
59             method BUILD {
60             $self->set_size_from_string( $self->get_size() )
61             if defined $self->get_size();
62            
63             $self->{'height'} = $self->get_width()
64             unless defined $self->get_height();
65            
66             $self->{'width'} = 300
67             unless $self->get_width > 0;
68             $self->{'height'} = $self->get_width
69             unless $self->get_height > 0;
70            
71             $self->set_default_text()
72             unless defined $self->get_text();
73            
74             $self->{'_image'} = GD::Image->new(
75             $self->get_width(),
76             $self->get_height(),
77             TRUE_COLOUR
78             );
79             $self->{'_image'}->saveAlpha(1);
80             $self->{'_image'}->alphaBlending(1);
81             $self->{'_image'}->useFontConfig(1);
82            
83             $self->{'_line'} = $self->allocate_colour( $self->get_line_colour() );
84             $self->{'_background'} = $self->allocate_colour(
85             $self->get_background_colour(),
86             $self->get_transparent() * MAX_TRANSPARENCY
87             );
88             $self->{'_text'} = $self->allocate_colour(
89             $self->get_text_colour(),
90             int( MAX_TRANSPARENCY * 0.6 )
91             );
92             }
93              
94             method generate {
95             my $image = $self->{'_image'};
96             my $line = $self->{'_line'};
97             my $x = $self->get_width() - 1;
98             my $y = $self->get_height() - 1;
99            
100             # draw the border and cross
101             $image->fill( 0, 0, $self->{'_background'} );
102            
103             if ( $self->get_line_colour ne 'none' ) {
104             $image->setAntiAliased( $line );
105             $image->line( 0, 0, $x, $y, gdAntiAliased );
106             $image->line( $x, 0, 0, $y, gdAntiAliased );
107             $image->rectangle( 0, 0, $x, $y, $line );
108             }
109            
110             if ( $self->get_text_colour ne 'none' ) {
111             # work out where (indeed, if) the text fits
112             my( $twidth, $theight, $tdropheight, $point_size )
113             = $self->get_text_offset();
114            
115             if ( $twidth ) {
116             my $baseline = $self->get_height() - $tdropheight;
117             my $text_total = $theight + $tdropheight;
118             my $remainder = $self->get_height() - $text_total;
119            
120             my $tx = ( $self->get_width() - $twidth ) / 2;
121             my $ty = $baseline - int( $remainder / 2 );
122            
123             $image->stringFT(
124             $self->{'_text'},
125             $self->{'font'},
126             $point_size,
127             0, # angle
128             $tx,
129             $ty,
130             $self->get_text()
131             );
132             }
133             }
134            
135             return $image->png;
136             }
137              
138             method set_default_text {
139             my $size = sprintf '%s×%s', $self->get_width(), $self->get_height();
140             $self->set_text( $size );
141             }
142             method set_size_from_string ( Str $size ) {
143             my $width_by_height = qr{
144             ^
145             ( \d+ )
146             x
147             ( \d+ )
148             $
149             }x;
150            
151             if ( $size =~ $width_by_height ) {
152             $self->{'width'} = $1;
153             $self->{'height'} = $2;
154             }
155             }
156              
157             method get_text_offset {
158             my $x = 0;
159             my $y = 0;
160             my $point_size = 10;
161             my $usable_width = int( $self->get_width * MAX_USABLE_RATIO );
162             my $usable_height = int( $self->get_height * MAX_USABLE_RATIO );
163             my @previous = ( 0, 0, 0, 0 );
164            
165             while ( 1 ) {
166             my @bounds = GD::Image->stringFT(
167             $self->{'_line'}, # colour
168             $self->{'font'},
169             $point_size,
170             0, # angle
171             0, # x
172             0, # y
173             $self->get_text(),
174             );
175            
176             if ( @bounds ) {
177             my $text_width = $bounds[2] - $bounds[0];
178             my $text_height = 0 - $bounds[5];
179             my $text_dropheight = $bounds[1];
180             my $text_total_height = $text_height + $text_dropheight;
181            
182             # $text_width = $bounds[2] - $bounds[0];
183             # $text_height = $bounds[1] + ( 0 - $bounds[5]);
184            
185             my $too_big = ( $text_width > $usable_width )
186             || ( $text_total_height > $usable_height );
187            
188             return @previous if $too_big;
189            
190             @previous =
191             ( $text_width, $text_height, $text_dropheight, $point_size );
192             $point_size += 5;
193             }
194             else {
195             return @previous;
196             }
197             }
198             }
199              
200             method allocate_colour ( Str $colour, Int $alpha=0 ) {
201             my @rgb = $self->rgb_to_hex( $colour );
202             my $img = $self->{'_image'};
203            
204             return $img->colorAllocateAlpha( @rgb, $alpha )
205             }
206             method rgb_to_hex ( Str $hex ) {
207             # TODO lookup standard colour values
208            
209             # must be a hex value
210             return( 0, 0, 0 )
211             unless $hex =~ m{^[0-9a-f]+$}i;
212            
213             # allow CSS style shorthands (f60 == ff6600)
214             $hex = "$1$1$2$2$3$3"
215             if $hex =~ m{^([0-9a-f])([0-9a-f])([0-9a-f])$}i;
216            
217             # must be six chars long
218             return( 0, 0, 0 )
219             unless 6 == length $hex;
220            
221             return map { hex($_) } unpack 'a2a2a2', $hex;
222             }
223              
224             1;
225              
226             __END__
227              
228             =head1 NAME
229              
230             Image::Placeholder - generate images for use as placeholders
231              
232             =head1 SYNOPSIS
233              
234             use Image::Placeholder;
235             my $image = Image::Placeholder->new(
236             width => 300,
237             height => 250,
238             background_colour => 'ccc',
239             line_colour => 'none',
240             font => 'Gill Sans',
241             text => 'IAB MRec',
242             );
243             print $image->generate();
244              
245              
246             =head1 OPTIONS
247              
248             The B<new()> method accepts a hash of options to control the size
249             and appearance of the generated image.
250              
251             =over
252              
253             =item background_colour
254              
255             The colour that the background of the image should be painted.
256             Accepts a colour value (see L<Valid colour values>). Defaults to
257             I<ddd>.
258              
259             =item font
260              
261             The font to use for the text in the image. Requires L<fontconfig>
262             support in your L<GD> library. Defaults to I<Museo Sans>, which is
263             available free from
264             L<http://www.josbuivenga.demon.nl/museosans.html>.
265              
266             =item height
267              
268             The height of the image in pixels. Defaults to the same as C<width>.
269              
270             =item line_colour
271              
272             The colour that the border and cross lines should be painted in.
273             Accepts either a colour value or C<none> to suppress them. Defaults
274             to I<444>.
275              
276             =item size
277              
278             A text alternative to supplying C<width> and C<height> separately;
279             of the form '300x250'.
280              
281             =item text
282              
283             The text to use across the image. Defaults to the size of the image,
284             expressed in the form '300x250'.
285              
286             =item text_colour
287              
288             The colour that the text should be painted in. Accepts either a
289             colour value or C<none> to suppress the text. Defaults to I<36f>.
290              
291             =item transparent
292              
293             Makes the background transparent.
294              
295             =item width
296              
297             The width of the image in pixels. Defaults to I<300>.
298              
299             =back
300              
301              
302             =head2 Valid colour values
303              
304             Colour values are specified as the red, green and blue channels in
305             hexadecimal, where C<00> is the least and C<FF> is the most. So black is
306             C<000000> and white is C<FFFFFF>.
307              
308             CSS-style 3-character shorthand is also accepted where the three
309             channels are repeating characters. So black is also C<000> and white
310             C<FFF>. All three values have to be repeating, so a value such as
311             C<080808> cannot be shorted.
312              
313              
314             =head1 SEE ALSO
315              
316             =over
317              
318             =item B<placeholder>
319              
320             command-line generator that uses this module.
321              
322             =item L<http://ima.gs/>
323              
324             hosted version of this module.
325              
326             =back
327              
328             =head1 AUTHOR
329              
330             Mark Norman Francis, L<norm@cackhanded.net>.
331              
332             =head1 COPYRIGHT AND LICENCE
333              
334             Copyright 2010 Mark Norman Francis.
335              
336             This library is free software; you can redistribute it and/or modify it
337             under the same terms as Perl itself.