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.07';
3              
4              
5 5     5   3008 use strict;
  5         8  
  5         126  
6 5     5   15 use warnings;
  5         5  
  5         151  
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.07
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   2425 use Moo;
  5         50254  
  5         30  
42 5     5   8256 use Math::Matrix;
  5         11036  
  5         127  
43 5     5   2395 use Math::Trig qw/deg2rad/;
  5         45353  
  5         348  
44 5     5   2421 use Ouch;
  5         7546  
  5         297  
45              
46             ##
47              
48 5     5   2143 use namespace::clean;
  5         41325  
  5         18  
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 34     34 1 27216 my $self = shift;
233 34         43 my $transform = shift;
234             ##Possible transforms:
235             ## scale (x [y])
236             ## translate (x [y])
237             ## Start with trimming
238 34         115 $transform =~ s/^\s*//;
239 34         67 $transform =~ s/^\s*$//;
240              
241             ##On the empty string, just reset the object
242 34 100       81 if (! $transform) {
243 1         29 $self->clear_transforms;
244 1         28 $self->clear_ctm;
245 1         5 return;
246             }
247 33         43 my @transformers = ();
248 33         901 while ($transform =~ m/\G (\w+) \s* \( \s* ($numbers_re) \s* \) (?:$comma_wsp)? /gx ) {
249 41         302 push @transformers, [$1, $2];
250             }
251              
252 33 50       78 if (! @transformers) {
253 0         0 ouch 'bad_transform_string', "Image::SVG::Transform: Unable to parse the transform string $transform";
254             }
255 33         38 my @transforms = ();
256 33         49 foreach my $transformer (@transformers) {
257 41         37 my ($transform_type, $params) = @{ $transformer };
  41         76  
258 41         225 my @params = split $split_re, $params;
259             ##Global checks
260 41 100       89 ouch 'unknown_type', "Unknown transform $transform_type" unless exists $valid_transforms->{$transform_type};
261 40 100       65 ouch 'no_parameters', "No parameters for transform $transform_type" unless scalar @params;
262 39 100       132 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 36 100 100     159 if ($transform_type eq 'rotate' && @params == 2) {
    100 100        
265 1         3 ouch 'rotate_2', 'rotate transform may not have two parameters';
266             }
267             elsif ($transform_type eq 'matrix' && @params != 6) {
268 1         3 ouch 'matrix_6', 'matrix transform must have exactly six parameters';
269             }
270 34 100 100     71 if ($transform_type eq 'rotate' && @params == 3) {
271             ##Special rotate with pre- and post-translates
272 1         8 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 33         113 push @transforms, {
288             type => $transform_type,
289             params => \@params,
290             }
291             }
292             }
293 26         92 $self->_set_transforms(\@transforms);
294 26         393 $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 9     9 1 18305 my $self = shift;
305 9         10 my $point = shift;
306 9 50       29 return $point unless $self->has_transforms;
307 9         10 push @{ $point }, 0; ##pad with zero to make a 1x3 matrix
  9         13  
308 9         41 my $userspace = Math::Matrix->new(
309             [ $point->[0] ],
310             [ $point->[1] ],
311             [ 1 ],
312             );
313 9         320 my $viewport = $self->ctm->multiply($userspace);
314 9         547 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 637 my $self = shift;
325 2         3 my $point = shift;
326 2 50       8 return $point unless $self->has_transforms;
327 2         3 push @{ $point }, 0; ##pad with zero to make a 1x3 matrix
  2         5  
328 2         8 my $viewport = Math::Matrix->new(
329             [ $point->[0] ],
330             [ $point->[1] ],
331             [ 1 ],
332             );
333 2         66 my $userspace = $self->ctm->invert->multiply($viewport);
334 2         455 return [ $userspace->[0]->[0], $userspace->[1]->[0] ];
335             }
336              
337             sub _generate_matrix {
338 13     13   11 my $self = shift;
339 13         10 my $index = shift;
340 13         18 my $t = $self->transforms->[$index];
341 13         13 my @matrix;
342 13 100       38 if ($t->{type} eq 'translate') {
    100          
    50          
    0          
    0          
    0          
343 7         10 my $tx = $t->{params}->[0];
344 7 100       15 my $ty = defined $t->{params}->[1] ? $t->{params}->[1] : 0;
345 7         20 @matrix = (
346             [ 1, 0, $tx, ],
347             [ 0, 1, $ty, ],
348             [ 0, 0, 1, ],
349             );
350             }
351             elsif ($t->{type} eq 'scale') {
352 4         7 my $sx = $t->{params}->[0];
353 4 100       11 my $sy = defined $t->{params}->[1] ? $t->{params}->[1] : $sx;
354 4         16 @matrix = (
355             [ $sx, 0, 0, ],
356             [ 0, $sy, 0, ],
357             [ 0, 0, 1, ],
358             );
359             }
360             elsif ($t->{type} eq 'rotate') {
361 2         11 my $angle = deg2rad($t->{params}->[0]);
362 2         77 my $cosa = cos $angle;
363 2         6 my $sina = sin $angle;
364 2         10 @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 13         37 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;