File Coverage

blib/lib/Image/SVG/Transform.pm
Criterion Covered Total %
statement 81 90 90.0
branch 26 36 72.2
condition 9 9 100.0
subroutine 11 11 100.0
pod 3 3 100.0
total 130 149 87.2


line stmt bran cond sub pod time code
1             package Image::SVG::Transform;
2             $Image::SVG::Transform::VERSION = '0.06';
3              
4              
5 5     5   2615 use strict;
  5         6  
  5         115  
6 5     5   14 use warnings;
  5         6  
  5         150  
7              
8             =head1 NAME
9              
10             Image::SVG::Transform - read the "transform" attribute of an SVG element
11              
12             =head1 VERSION
13              
14             version 0.06
15              
16             =head1 SYNOPSIS
17              
18             use Image::SVG::Transform;
19             my $transform = Image::SVG::Transform->new();
20             $transform->extract_transforms('scale(0.5)');
21             my $view_point = $transform->transform([5, 10]);
22              
23             =head1 DESCRIPTION
24              
25             This module parses and converts the contents of the transform attribute in SVG into
26             a series of array of hashes, and then provide a convenience method for doing point transformation
27             from the transformed space to the viewpoint space.
28              
29             This is useful if you're doing SVG rendering, or if you are trying to estimate the length of shapes in an SVG file.
30              
31             =head1 METHODS
32              
33             The following methods are available.
34              
35             =head2 new ()
36              
37             Constructor for the class. It takes no arguments.
38              
39             =cut
40              
41 5     5   2393 use Moo;
  5         46941  
  5         24  
42 5     5   7459 use Math::Matrix;
  5         10366  
  5         126  
43 5     5   2305 use Math::Trig qw/deg2rad/;
  5         44293  
  5         331  
44 5     5   2179 use Ouch;
  5         7211  
  5         277  
45              
46 5     5   2047 use namespace::clean;
  5         41155  
  5         17  
47              
48             =head2 transforms
49              
50             The list of transforms that were extracted from the transform string that submitted to L. Each transform will be a hashref with these keys:
51              
52             =head3 type
53              
54             The type of transformation (scale, translate, skewX, matrix, skewY, rotate).
55              
56             =head3 params
57              
58             An arrayref of hashrefs. Each hashref has key for type (string) and params (arrayref of numeric parameters).
59              
60             =cut
61              
62             has transforms => (
63             is => 'rwp',
64             clearer => 'clear_transforms',
65             predicate => 'has_transforms',
66             );
67              
68             =head2 has_transforms
69              
70             Returns true if the object has any transforms.
71              
72             =head2 clear_transforms
73              
74             Clear the set of transforms
75              
76             =cut
77              
78             =head2 ctm
79              
80             The combined transformation matrix for the set of transforms. This is a C object.
81              
82             =cut
83              
84             has ctm => (
85             is => 'rw',
86             lazy => 1,
87             clearer => 'clear_ctm',
88             default => sub {
89             my $self = shift;
90             my $ctm = $self->_generate_matrix(0);
91             my $idx = 1;
92             while ($idx < scalar @{ $self->transforms }) {
93             my $matrix = $self->_generate_matrix($idx);
94             my $product = $ctm->multiply($matrix);
95             $ctm = $product;
96             $idx++;
97             }
98             return $ctm;
99             },
100             );
101              
102             ##Borrowed parsing code from Image::SVG::Path
103             my $split_re = qr/
104             (?:
105             ,
106             |
107             (?
108             |
109             (?
110             |
111             \s+
112             )
113             /x;
114              
115             my $comma_wsp = qr/ (?: \s+ ,? \s*)|(?: , \s* )/x;
116             my $number_re = qr/[\+\-0-9.,e]+/i;
117             my $numbers_re = qr/(?:$number_re|\s)*/;
118              
119             my $valid_transforms = {
120             scale => 2,
121             translate => 2,
122             rotate => 3,
123             skewX => 1,
124             skewY => 1,
125             matrix => 6,
126             };
127              
128             =head2 extract_transforms ( $svg_transformation )
129              
130             Parses the C<$svg_transformation> string, which is expected to contain a valid set of SVG transformations as described in section 7.6 of the SVG spec: L. Unrecognized transformation types, or valid types with the wrong number of arguments, will cause C to C with an error message.
131              
132             After it is done parsing, it updates the stored C and clears the stored combined transformation matrix.
133              
134             Passing in the empty string will clear the set of transformations.
135              
136             In the following conditions, C will throw an exception using L:
137              
138             =over 4
139              
140             =item The transform string could not be parsed
141              
142             =item The transform contains un unknown type
143              
144             =item The type of transform has the wrong number of arguments
145              
146             =back
147              
148             =cut
149              
150             sub extract_transforms {
151 34     34 1 31947 my $self = shift;
152 34         37 my $transform = shift;
153             ##Possible transforms:
154             ## scale (x [y])
155             ## translate (x [y])
156             ## Start with trimming
157 34         120 $transform =~ s/^\s*//;
158 34         64 $transform =~ s/^\s*$//;
159              
160             ##On the empty string, just reset the object
161 34 100       70 if (! $transform) {
162 1         18 $self->clear_transforms;
163 1         17 $self->clear_ctm;
164 1         4 return;
165             }
166 33         38 my @transformers = ();
167 33         508 while ($transform =~ m/\G (\w+) \s* \( \s* ($numbers_re) \s* \) (?:$comma_wsp)? /gx ) {
168 41         251 push @transformers, [$1, $2];
169             }
170              
171 33 50       60 if (! @transformers) {
172 0         0 ouch 'bad_transform_string', "Image::SVG::Transform: Unable to parse the transform string $transform";
173             }
174 33         39 my @transforms = ();
175 33         41 foreach my $transformer (@transformers) {
176 41         33 my ($transform_type, $params) = @{ $transformer };
  41         73  
177 41         209 my @params = split $split_re, $params;
178             ##Global checks
179 41 100       84 ouch 'unknown_type', "Unknown transform $transform_type" unless exists $valid_transforms->{$transform_type};
180 40 100       61 ouch 'no_parameters', "No parameters for transform $transform_type" unless scalar @params;
181 39 100       78 ouch 'too_many_parameters', "Too many parameters ".scalar(@params). " for transform $transform_type" if scalar(@params) > $valid_transforms->{$transform_type};
182             ##Command specific checks
183 36 100 100     152 if ($transform_type eq 'rotate' && @params == 2) {
    100 100        
184 1         4 ouch 'rotate_2', 'rotate transform may not have two parameters';
185             }
186             elsif ($transform_type eq 'matrix' && @params != 6) {
187 1         3 ouch 'matrix_6', 'matrix transform must have exactly six parameters';
188             }
189 34 100 100     71 if ($transform_type eq 'rotate' && @params == 3) {
190             ##Special rotate with pre- and post-translates
191 1         8 push @transforms,
192             {
193             type => 'translate',
194             params => [ $params[1], $params[2] ],
195             },
196             {
197             type => 'rotate',
198             params => [ $params[0], ],
199             },
200             {
201             type => 'translate',
202             params => [ -1*$params[1], -1*$params[2] ],
203             },
204             }
205             else {
206 33         98 push @transforms, {
207             type => $transform_type,
208             params => \@params,
209             }
210             }
211             }
212 26         82 $self->_set_transforms(\@transforms);
213 26         385 $self->clear_ctm;
214             }
215              
216             =head2 transform ( $point )
217              
218             Using the stored set of one or more C, transform C<$point> from the local coordinate system to viewport coordinate system. The combined transformation matrix is cached so that it isn't recalculated everytime this method is called.
219              
220             =cut
221              
222             sub transform {
223 9     9 1 16989 my $self = shift;
224 9         8 my $point = shift;
225 9 50       24 return $point unless $self->has_transforms;
226 9         8 push @{ $point }, 0; ##pad with zero to make a 1x3 matrix
  9         16  
227 9         38 my $userspace = Math::Matrix->new(
228             [ $point->[0] ],
229             [ $point->[1] ],
230             [ 1 ],
231             );
232 9         283 my $viewport = $self->ctm->multiply($userspace);
233 9         489 return [ $viewport->[0]->[0], $viewport->[1]->[0] ];
234             }
235              
236             =head2 untransform ( $point )
237              
238             The opposite of C. It takes a point from the viewport coordinates and transforms them into the local coordinate system.
239              
240             =cut
241              
242             sub untransform {
243 2     2 1 831 my $self = shift;
244 2         3 my $point = shift;
245 2 50       8 return $point unless $self->has_transforms;
246 2         3 push @{ $point }, 0; ##pad with zero to make a 1x3 matrix
  2         3  
247 2         8 my $viewport = Math::Matrix->new(
248             [ $point->[0] ],
249             [ $point->[1] ],
250             [ 1 ],
251             );
252 2         73 my $userspace = $self->ctm->invert->multiply($viewport);
253 2         466 return [ $userspace->[0]->[0], $userspace->[1]->[0] ];
254             }
255              
256             sub _generate_matrix {
257 13     13   11 my $self = shift;
258 13         10 my $index = shift;
259 13         20 my $t = $self->transforms->[$index];
260 13         10 my @matrix;
261 13 100       59 if ($t->{type} eq 'translate') {
    100          
    50          
    0          
    0          
    0          
262 7         8 my $tx = $t->{params}->[0];
263 7 100       13 my $ty = defined $t->{params}->[1] ? $t->{params}->[1] : 0;
264 7         28 @matrix = (
265             [ 1, 0, $tx, ],
266             [ 0, 1, $ty, ],
267             [ 0, 0, 1, ],
268             );
269             }
270             elsif ($t->{type} eq 'scale') {
271 4         5 my $sx = $t->{params}->[0];
272 4 100       9 my $sy = defined $t->{params}->[1] ? $t->{params}->[1] : $sx;
273 4         11 @matrix = (
274             [ $sx, 0, 0, ],
275             [ 0, $sy, 0, ],
276             [ 0, 0, 1, ],
277             );
278             }
279             elsif ($t->{type} eq 'rotate') {
280 2         11 my $angle = deg2rad($t->{params}->[0]);
281 2         68 my $cosa = cos $angle;
282 2         6 my $sina = sin $angle;
283 2         7 @matrix = (
284             [ $cosa, -1*$sina, 0, ],
285             [ $sina, $cosa, 0, ],
286             [ 0, 0, 1, ],
287             );
288             }
289             elsif ($t->{type} eq 'skewX') {
290 0         0 my $angle = deg2rad($t->{params}->[0]);
291 0         0 my $tana = tan $angle;
292 0         0 @matrix = (
293             [ 1, $tana, 0, ],
294             [ 0, 1, 0, ],
295             [ 0, 0, 1, ],
296             );
297             }
298             elsif ($t->{type} eq 'skewY') {
299 0         0 my $angle = deg2rad($t->{params}->[0]);
300 0         0 my $tana = tan $angle;
301 0         0 @matrix = (
302             [ 1, 0, 0, ],
303             [ $tana, 1, 0, ],
304             [ 0, 0, 1, ],
305             );
306             }
307             elsif ($t->{type} eq 'matrix') {
308 0         0 my $p = $t->{params};
309 0         0 @matrix = (
310             [ $p->[0], $p->[2], $p->[4], ],
311             [ $p->[1], $p->[3], $p->[5], ],
312             [ 0, 0, 1, ],
313             );
314             }
315 13         37 return Math::Matrix->new(@matrix);
316             }
317              
318             =head1 PREREQS
319              
320             L
321             L
322             L
323             L
324             L
325              
326             =head1 SUPPORT
327              
328             =over
329              
330             =item Repository
331              
332             L
333              
334             =item Bug Reports
335              
336             L
337              
338             =back
339              
340             =head1 AUTHOR
341              
342             Colin Kuskie
343              
344             =head1 SEE ALSO
345              
346             L
347             L
348              
349             =head1 THANKS
350              
351             Thank you to Ben Bullock, author of L for the regular expressions for the parser.
352              
353             =head1 LEGAL
354              
355             This module is Copyright 2016 Plain Black Corporation. It is distributed under the same terms as Perl itself.
356              
357             =cut
358              
359             1;