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.08';
3              
4              
5 5     5   5683 use strict;
  5         15  
  5         200  
6 5     5   40 use warnings;
  5         13  
  5         242  
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.08
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   4278 use Moo;
  5         84050  
  5         39  
42 5     5   12455 use Math::Matrix;
  5         30669  
  5         246  
43 5     5   4982 use Math::Trig qw/deg2rad/;
  5         87177  
  5         628  
44 5     5   3997 use Ouch;
  5         14410  
  5         31  
45              
46             ##
47              
48 5     5   3617 use namespace::clean;
  5         77258  
  5         44  
49              
50             =head2 transforms
51              
52             The list of transforms that were extracted from the transform string that submitted to L. Each transform will be a hashref with these keys:
53              
54             =head3 type
55              
56             The type of transformation (scale, translate, skewX, matrix, skewY, rotate).
57              
58             =head3 params
59              
60             An arrayref of hashrefs. Each hashref has key for type (string) and params (arrayref of numeric parameters).
61              
62             =cut
63              
64             has transforms => (
65             is => 'rwp',
66             clearer => 'clear_transforms',
67             predicate => 'has_transforms',
68             );
69              
70             =head2 has_transforms
71              
72             Returns true if the object has any transforms.
73              
74             =head2 clear_transforms
75              
76             Clear the set of transforms
77              
78             =cut
79              
80             =head2 ctm
81              
82             The combined transformation matrix for the set of transforms. This is a C object.
83              
84             =cut
85              
86             has ctm => (
87             is => 'rw',
88             lazy => 1,
89             clearer => 'clear_ctm',
90             default => sub {
91             my $self = shift;
92             my $ctm = $self->_generate_matrix(0);
93             my $idx = 1;
94             while ($idx < scalar @{ $self->transforms }) {
95             my $matrix = $self->_generate_matrix($idx);
96             my $product = $ctm->multiply($matrix);
97             $ctm = $product;
98             $idx++;
99             }
100             return $ctm;
101             },
102             );
103              
104             ##Blatantly stolen from Image::SVG::Path
105              
106             # Match the e or E in an exponent.
107              
108             my $e = qr/[eE]/;
109              
110             # These regular expressions are directly taken from the SVG grammar,
111             # https://www.w3.org/TR/SVG/paths.html#PathDataBNF
112              
113             our $sign = qr/\+|\-/;
114              
115             our $wsp = qr/[\x20\x09\x0D\x0A]/;
116              
117             our $comma_wsp = qr/(?:$wsp+,?$wsp*|,$wsp*)/;
118              
119             # The following regular expression splits the path into pieces Note we
120             # only split on '-' or '+' when not preceeded by 'e'. This regular
121             # expression is not following the SVG grammar, it is going our own
122             # way.
123              
124             my $split_re = qr/
125             (?:
126             $wsp*,$wsp*
127             |
128             (?
129             |
130             (?
131             |
132             $wsp+
133             )
134             /x;
135              
136             # Match a number
137              
138             # From SVG grammar, https://www.w3.org/TR/SVG/paths.html#PathDataBNF
139              
140             # $ds is "digit sequence", and it accounts for all the uses of "digit"
141             # in the SVG path grammar, so there is no "digit" here.
142              
143              
144             my $ds = qr/[0-9]+/;
145             our $digit_sequence = $ds;
146              
147             # From SVG grammar, https://www.w3.org/TR/SVG/paths.html#PathDataBNF
148              
149             # Aside to whoever wrote the SVG standards: this is not an integer,
150             # it's a whole number!
151              
152             our $integer_constant = qr/$ds/;
153              
154             # From SVG grammar, https://www.w3.org/TR/SVG/paths.html#PathDataBNF
155              
156             our $fractional_constant = qr/$ds? \. $ds/x;
157              
158             # From SVG grammar, https://www.w3.org/TR/SVG/paths.html#PathDataBNF
159              
160             our $exponent = qr/
161             $e
162             $sign?
163             $ds
164             /x;
165              
166             # From SVG grammar, https://www.w3.org/TR/SVG/paths.html#PathDataBNF
167              
168             our $floating_point_constant = qr/
169             $fractional_constant
170             $exponent?
171             |
172             $ds
173             $exponent
174             /x;
175              
176              
177             # From SVG grammar, https://www.w3.org/TR/SVG/paths.html#PathDataBNF
178              
179             # $floating_point_constant needs to go before $integer_constant,
180             # otherwise it matches the shorter $integer_constant every time.
181              
182             our $number = qr/
183             $sign?
184             $floating_point_constant
185             |
186             $sign?
187             $integer_constant
188             /x;
189              
190             my $pair = qr/$number $comma_wsp? $number/x;
191              
192             my $pairs = qr/(?:$pair $wsp)* $pair/x;
193              
194             my $numbers = qr/(?:$number $wsp)* $number/x;
195              
196             # This is where we depart from the SVG grammar and go our own way.
197              
198             my $numbers_re = qr/(?:$number|$comma_wsp+)*/;
199              
200             my $valid_transforms = {
201             scale => 2,
202             translate => 2,
203             rotate => 3,
204             skewX => 1,
205             skewY => 1,
206             matrix => 6,
207             };
208              
209             =head2 extract_transforms ( $svg_transformation )
210              
211             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.
212              
213             After it is done parsing, it updates the stored C and clears the stored combined transformation matrix.
214              
215             Passing in the empty string will clear the set of transformations.
216              
217             In the following conditions, C will throw an exception using L:
218              
219             =over 4
220              
221             =item The transform string could not be parsed
222              
223             =item The transform contains un unknown type
224              
225             =item The type of transform has the wrong number of arguments
226              
227             =back
228              
229             =cut
230              
231             sub extract_transforms {
232 38     38 1 75452 my $self = shift;
233 38         114 my $transform = shift;
234             ##Possible transforms:
235             ## scale (x [y])
236             ## translate (x [y])
237             ## Start with trimming
238 38         259 $transform =~ s/^\s*//;
239 38         171 $transform =~ s/^\s*$//;
240              
241             ##On the empty string, just reset the object
242 38 100       159 if (! $transform) {
243 1         31 $self->clear_transforms;
244 1         36 $self->clear_ctm;
245 1         8 return;
246             }
247 37         107 my @transformers = ();
248 37         1625 while ($transform =~ m/\G (\w+) \s* \( \s* ($numbers_re) \s* \) (?:$comma_wsp)? /gx ) {
249 46         579 push @transformers, [$1, $2];
250             }
251              
252 37 50       146 if (! @transformers) {
253 0         0 ouch 'bad_transform_string', "Image::SVG::Transform: Unable to parse the transform string $transform";
254             }
255 37         100 my @transforms = ();
256 37         113 foreach my $transformer (@transformers) {
257 46         96 my ($transform_type, $params) = @{ $transformer };
  46         154  
258 46         478 my @params = split $split_re, $params;
259             ##Global checks
260 46 100       209 ouch 'unknown_type', "Unknown transform $transform_type" unless exists $valid_transforms->{$transform_type};
261 45 100       147 ouch 'no_parameters', "No parameters for transform $transform_type" unless scalar @params;
262 44 100       186 ouch 'too_many_parameters', "Too many parameters ".scalar(@params). " for transform $transform_type" if scalar(@params) > $valid_transforms->{$transform_type};
263             ##Command specific checks
264 41 100 100     289 if ($transform_type eq 'rotate' && @params == 2) {
    100 100        
265 1         6 ouch 'rotate_2', 'rotate transform may not have two parameters';
266             }
267             elsif ($transform_type eq 'matrix' && @params != 6) {
268 1         8 ouch 'matrix_6', 'matrix transform must have exactly six parameters';
269             }
270 39 100 100     159 if ($transform_type eq 'rotate' && @params == 3) {
271             ##Special rotate with pre- and post-translates
272 1         15 push @transforms,
273             {
274             type => 'translate',
275             params => [ $params[1], $params[2] ],
276             },
277             {
278             type => 'rotate',
279             params => [ $params[0], ],
280             },
281             {
282             type => 'translate',
283             params => [ -1*$params[1], -1*$params[2] ],
284             },
285             }
286             else {
287 38         262 push @transforms, {
288             type => $transform_type,
289             params => \@params,
290             }
291             }
292             }
293 30         231 $self->_set_transforms(\@transforms);
294 30         1058 $self->clear_ctm;
295             }
296              
297             =head2 transform ( $point )
298              
299             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.
300              
301             =cut
302              
303             sub transform {
304 10     10 1 32785 my $self = shift;
305 10         28 my $point = shift;
306 10 50       59 return $point unless $self->has_transforms;
307 10         29 push @{ $point }, 0; ##pad with zero to make a 1x3 matrix
  10         37  
308 10         91 my $userspace = Math::Matrix->new(
309             [ $point->[0] ],
310             [ $point->[1] ],
311             [ 1 ],
312             );
313 10         778 my $viewport = $self->ctm->multiply($userspace);
314 10         1792 return [ $viewport->[0]->[0], $viewport->[1]->[0] ];
315             }
316              
317             =head2 untransform ( $point )
318              
319             The opposite of C. It takes a point from the viewport coordinates and transforms them into the local coordinate system.
320              
321             =cut
322              
323             sub untransform {
324 2     2 1 2056 my $self = shift;
325 2         9 my $point = shift;
326 2 50       19 return $point unless $self->has_transforms;
327 2         7 push @{ $point }, 0; ##pad with zero to make a 1x3 matrix
  2         10  
328 2         20 my $viewport = Math::Matrix->new(
329             [ $point->[0] ],
330             [ $point->[1] ],
331             [ 1 ],
332             );
333 2         161 my $userspace = $self->ctm->invert->multiply($viewport);
334 2         1415 return [ $userspace->[0]->[0], $userspace->[1]->[0] ];
335             }
336              
337             sub _generate_matrix {
338 14     14   32 my $self = shift;
339 14         32 my $index = shift;
340 14         57 my $t = $self->transforms->[$index];
341 14         32 my @matrix;
342 14 100       83 if ($t->{type} eq 'translate') {
    100          
    50          
    0          
    0          
    0          
343 8         28 my $tx = $t->{params}->[0];
344 8 100       35 my $ty = defined $t->{params}->[1] ? $t->{params}->[1] : 0;
345 8         46 @matrix = (
346             [ 1, 0, $tx, ],
347             [ 0, 1, $ty, ],
348             [ 0, 0, 1, ],
349             );
350             }
351             elsif ($t->{type} eq 'scale') {
352 4         17 my $sx = $t->{params}->[0];
353 4 100       18 my $sy = defined $t->{params}->[1] ? $t->{params}->[1] : $sx;
354 4         27 @matrix = (
355             [ $sx, 0, 0, ],
356             [ 0, $sy, 0, ],
357             [ 0, 0, 1, ],
358             );
359             }
360             elsif ($t->{type} eq 'rotate') {
361 2         22 my $angle = deg2rad($t->{params}->[0]);
362 2         101 my $cosa = cos $angle;
363 2         11 my $sina = sin $angle;
364 2         16 @matrix = (
365             [ $cosa, -1*$sina, 0, ],
366             [ $sina, $cosa, 0, ],
367             [ 0, 0, 1, ],
368             );
369             }
370             elsif ($t->{type} eq 'skewX') {
371 0         0 my $angle = deg2rad($t->{params}->[0]);
372 0         0 my $tana = tan $angle;
373 0         0 @matrix = (
374             [ 1, $tana, 0, ],
375             [ 0, 1, 0, ],
376             [ 0, 0, 1, ],
377             );
378             }
379             elsif ($t->{type} eq 'skewY') {
380 0         0 my $angle = deg2rad($t->{params}->[0]);
381 0         0 my $tana = tan $angle;
382 0         0 @matrix = (
383             [ 1, 0, 0, ],
384             [ $tana, 1, 0, ],
385             [ 0, 0, 1, ],
386             );
387             }
388             elsif ($t->{type} eq 'matrix') {
389 0         0 my $p = $t->{params};
390 0         0 @matrix = (
391             [ $p->[0], $p->[2], $p->[4], ],
392             [ $p->[1], $p->[3], $p->[5], ],
393             [ 0, 0, 1, ],
394             );
395             }
396 14         79 return Math::Matrix->new(@matrix);
397             }
398              
399             =head1 PREREQS
400              
401             L
402             L
403             L
404             L
405             L
406              
407             =head1 SUPPORT
408              
409             =over
410              
411             =item Repository
412              
413             L
414              
415             =item Bug Reports
416              
417             L
418              
419             =back
420              
421             =head1 AUTHOR
422              
423             Colin Kuskie
424              
425             =head1 SEE ALSO
426              
427             L
428             L
429              
430             =head1 THANKS
431              
432             Thank you to Ben Bullock, author of L for the regular expressions for the parser.
433              
434             =head1 LEGAL
435              
436             This module is Copyright 2016 Plain Black Corporation. It is distributed under the same terms as Perl itself.
437              
438             =cut
439              
440             1;