File Coverage

blib/lib/Image/QRCode/Effects.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Image::QRCode::Effects;
2              
3 1     1   24156 use 5.006;
  1         4  
  1         79  
4              
5 1     1   6 use strict;
  1         2  
  1         35  
6 1     1   6 use warnings;
  1         18  
  1         60  
7              
8             our $VERSION = '0.01';
9              
10 1     1   1129 use File::Slurp qw(write_file);
  1         17229  
  1         61  
11 1     1   496 use Image::Magick;
  0            
  0            
12             use Imager;
13             use Imager::QRCode;
14             use File::Temp qw(tempfile);
15             use Params::Validate qw(:all);
16             use Scalar::Util qw(looks_like_number);
17              
18             my $rx_colour = { regex => qr/^#[a-f\d]+$/i };
19             my $valid_size = { regex => qr/^\d+x\d+$/i };
20             my $num = { callbacks => { 'numeric' => sub { looks_like_number(shift) } } };
21             my $optional_num = { %$num, optional => 1 };
22             my $short_enough = { callbacks => { 'under 100 characters' => sub { length(shift) < 100 } } };
23             my $opt_boolean = { type => BOOLEAN, default => 0 };
24             my $file_exists = { callbacks => { 'valid file' => sub { -f shift } } };
25              
26             sub new {
27             my $class = shift;
28             my %args = @_;
29             my $self = bless {}, $class;
30             if (my $qrcode = delete $args{qrcode}) {
31             $self->_set_file_from_imager($qrcode);
32             }
33             elsif (my $file = delete $args{infile}) {
34             $self->_set_file_from_file($file);
35             }
36             else {
37             $self->_set_file_from_args(%args);
38             }
39             return $self;
40             }
41              
42             sub _set_file_from_file {
43             my $self = shift;
44             my ($file) = validate_pos(@_, $file_exists);
45             $self->{file} = $file;
46             }
47              
48             sub _set_file_from_args {
49             my $self = shift;
50             my %args = @_;
51             my $plot = delete $args{plot} or die "Missing 'plot' parameter to new()";
52             my $qrcode = Imager::QRCode->new(%args);
53             my $img = $qrcode->plot($plot);
54             $self->_set_file_from_imager($img);
55             }
56              
57             sub _set_file_from_imager {
58             my $self = shift;
59             my ($qrcode) = validate_pos(@_, { can => 'write' });
60             my $ft = File::Temp->new(TEMPLATE => "qrcode_XXXXXX", TMPDIR => 1, SUFFIX => '.png', UNLINK => 1);
61             $self->{_ft} = $ft;
62             $qrcode->write(file => $ft);
63             close $ft;
64              
65             $self->_set_file_from_file($ft->filename);
66             }
67              
68             sub write {
69             my $self = shift;
70             my %p = validate(@_, {
71             outfile => 1,
72             plasma => $opt_boolean,
73             round_corners => $opt_boolean,
74             wave => $opt_boolean,
75             gradient => $opt_boolean,
76             inner_shadow => $opt_boolean,
77             colour => { %$rx_colour, default => '#000000' },
78             gradient_colour => { %$rx_colour, optional => 1 },
79             size => { %$valid_size, default => '600x600' },
80             wavelength => { %$num, default => 30 },
81             amplitude => { %$num, default => 1.5 },
82             corner_sigma => { %$num, default => 2.2 },
83             corner_threshold => { regex => qr/^\d+%,\d+%$/, default => '42%,58%' },
84             shadow_colour => { %$rx_colour, default => '#000000' },
85             gradient_type => { regex => qr/^(normal|radial|plasma)$/, default => 'normal' },
86             });
87              
88             my $im = Image::Magick->new;
89             my $size = $p{size};
90              
91             my $file = $self->{file};
92             if (!-f "$file") {
93             die "Internal error: file $file has not been set";
94             }
95              
96             # Resize the image, without smoothing
97             $im->read($file);
98             $im->Resize(geometry => $size, filter => 'Point');
99              
100             # Apply the wave, if requested
101             if ($p{wave}) {
102             my $amplitude = $p{amplitude};
103             my $wavelength = $p{wavelength};
104             $im->Wave(amplitude => $amplitude, wavelength => $wavelength);
105             }
106              
107             # Round the corners
108             if ($p{round_corners}) {
109             $im->GaussianBlur(sigma => $p{corner_sigma});
110             }
111              
112             # Get rid of the greyness
113             $im->Level(levels => $p{corner_threshold});
114              
115             # Do the inner shadow
116             my $inner_shadow;
117             if ($p{inner_shadow}) {
118             my $drop = $im->Clone();
119             $drop->Transparent(color => '#FFFFFF', invert => 1);
120             my $stencil = $drop->Clone();
121             $drop->Set(background => $p{shadow_colour});
122             $drop->Shadow(opacity => 80, sigma => 3, x => 3, y => 3);
123             $stencil->Set(background => 'none'); #XXX: this needed?
124             $drop->Composite(image => $stencil);
125             $inner_shadow = $drop;
126             }
127              
128             # fill with a gradient or colour
129             my $fill;
130             my $col;
131             if ($p{gradient}) {
132             my $from = $p{colour};
133             my $to = $p{gradient_colour};
134             my $type = 'gradient';
135             $type = 'radial-gradient' if $p{gradient_type} eq 'radial';
136             $type = 'plasma' if $p{gradient_type} eq 'plasma';
137             if ($type eq 'plasma' && !$to) {
138             $fill = "$type:$from";
139             }
140             else {
141             $to ||= $from;
142             $fill = "$type:$from-$to";
143             }
144             }
145             elsif (($col = $p{colour}) && $p{plasma} ) {
146             if (my $to = $p{gradient_colour}) {
147             $fill = "plasma:$col-$to";
148             }
149             else {
150             $fill = "plasma:$col";
151             }
152             }
153             elsif ($col = $p{colour}) {
154             $fill = "xc:$col";
155             }
156             else {
157             die "Colour required";
158             }
159              
160             # create a blank image to fill
161             my $white = Image::Magick->new;
162             $white->Set(size => $size);
163             $white->ReadImage('xc:white');
164              
165             # fill with the colour, masked with the barcode
166             my $filled = Image::Magick->new;
167             $filled->Set(size => $size);
168             $filled->ReadImage($fill);
169             $im->Negate();
170             $white->Composite(image => $filled, mask => $im, color => 'white');
171              
172             # put the transparent stencil on top if we've got an inner shadow
173             if ($inner_shadow) {
174             $white->Composite(image => $inner_shadow);
175             }
176              
177             # Finally, write the file
178             my $outfile = $p{outfile};
179             $white->write($outfile);
180             }
181              
182             =head1 NAME
183              
184             Image::QRCode::Effects - Create snazzy QRCodes.
185              
186             =head1 SYNOPSIS
187              
188             use Image::QRCode::Effects;
189              
190             my $image = Image::QRCode::Effects->new(
191             level => 'H',
192             plot => 'just another perl hacker',
193             );
194              
195             $image->write(
196             outfile => 'qrcode.jpg',
197             colour => '#1500ff',
198             inner_shadow => 1,
199             round_corners => 1,
200             gradient => 1,
201             gradient_colour => '#ffa200',
202             gradient_type => 'radial',
203             );
204              
205             =cut
206              
207             =head1 DESCRIPTION
208              
209             This module provides a collection of effects commonly used on QRCodes to make them look interesting.
210              
211             It's designed for use with L, although it'll likely work with
212             any barcode images. Providing you don't stray too far from the default parameters,
213             the resulting barcode should be easily readable.
214              
215             =head1 CONSTRUCTOR
216              
217             =head2 new(%args)
218              
219             # Takes same arguments as Imager::QRCode, and additional 'plot' text
220             my $qrcode = Imager::QRCode->new(
221             plot => 'Fire walk with me',
222             size => 2,
223             margin => 2,
224             version => 1,
225             level => 'M',
226             casesensitive => 1,
227             lightcolor => Imager::Color->new( 255, 255, 255 ),
228             darkcolor => Imager::Color->new( 0, 0, 0 ),
229             );
230              
231             # Or from file
232             my $qr = Image::QRCode::Effects->new( infile => '/path/to/barcode.jpg' );
233              
234             # Or from Imager object (eg. Imager::QRCode, after calling ->plot)
235             my $qr = Image::QRCode::Effects->new( qrcode => $qrcode );
236              
237             Returns an C object, ready to call L. For the
238             parameters to L, see that module's documentation.
239              
240             =head1 METHODS
241              
242             =head2 write(%args)
243              
244             $qrcode->write(
245             outfile => '/my/new/barcode.jpg',
246              
247             # dimensions
248             size => '600x600', # optional, default is '600x600'
249              
250             # basic fill colour
251             colour => '#00ff00', # default #000000
252              
253             # extra fill effects
254             gradient => 1, # optional, default 0
255             gradient_colour => '#ff0000',
256             gradient_type => 'normal', # normal|radial|plasma
257              
258             # effects
259             # wave effect
260             wave => 1, # optional, default 0
261             wavelength => 30,
262             amplitude => 1.5,
263              
264             # inner shadow effect
265             inner_shadow => 1, # optional, default 0
266             shadow_colour => '#cccccc', # default #000000
267              
268             # rounded corners effect
269             round_corners => 1, # optional, default 0
270             corner_sigma => 2.2,
271             corner_threshold => '42%,58%',
272             );
273              
274             Writes the barcode with effects to the specified C.
275              
276             There are three main effects: a wave-like effect, rounded corners and an inner
277             shadow. In addition, there are several gradient fill options. These can be
278             combined and each have parameters that can be altered to create unique images.
279              
280             Parameters:
281              
282             =over
283              
284             =item C - File to write to. Required.
285              
286             =item C - Dimensions of new image. Defaults to '600x600'.
287              
288             =item C - Primary fill colour of the barcode
289              
290             =item C - Boolean, whether to fill the barcode with a gradient. Default is 0.
291              
292             =item C - Gradient colour to fill when C.
293              
294             =item C - Type of gradient. Can be C (default), C or C.
295              
296             =item C - Boolean, whether to warp the barcode with a wave effect. Default is 0.
297              
298             =item C - The length of the waves when C.
299              
300             =item C - The amplitude of the waves when C.
301              
302             =item C - Boolean, whether to apply an inner shadow. Default is 0.
303              
304             =item C - Colour of the shadow when C.
305              
306             =item C - Boolean, whether to round the corners of the barcode. Default is 0.
307              
308             =item C - Can be changed to adjust the 'roundedness' of the corners when C. Default is 2.2
309              
310             =item C - Can be changed to adjust the 'sharpness' of the corners when C. Default is '42%,58%'.
311              
312             =back
313              
314             =head1 SEE ALSO
315              
316             L
317              
318             L
319              
320             =head1 AUTHOR
321              
322             Mike Cartmell, C<< >>
323              
324             =head1 LICENSE AND COPYRIGHT
325              
326             Copyright (C) 2013 Mike Cartmell
327              
328             This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
329              
330             See http://dev.perl.org/licenses/ for more information.
331              
332             =cut
333              
334             1;