File Coverage

lib/XML/SAX/SVGTransformer.pm
Criterion Covered Total %
statement 197 235 83.8
branch 66 106 62.2
condition 28 53 52.8
subroutine 24 26 92.3
pod 5 5 100.0
total 320 425 75.2


line stmt bran cond sub pod time code
1             package XML::SAX::SVGTransformer;
2              
3 2     2   211665 use strict;
  2         14  
  2         68  
4 2     2   16 use warnings;
  2         3  
  2         67  
5 2     2   10 use base 'XML::SAX::Base';
  2         3  
  2         1167  
6 2     2   23656 use Math::Matrix;
  2         58853  
  2         89  
7 2     2   1953 use Math::Trig qw/deg2rad/;
  2         32047  
  2         6435  
8              
9             our $VERSION = '0.03';
10             our $GroupId = 'SVGTransformer';
11              
12             my $IdMatrix = Math::Matrix->id(4);
13              
14             sub start_document {
15 3     3 1 55156 my $self = shift;
16 3         29 $self->SUPER::start_document(@_);
17 3         1327 $self->{_stack} = [];
18 3         10 $self->{_ops} = [];
19 3         11 $self->{_stash} = {};
20             }
21              
22             sub start_element {
23 13     13 1 16005 my $self = shift;
24 13         23 my $elem = $_[0];
25 13         30 my $name = lc $elem->{LocalName};
26 13 100 66     71 if ($name eq 'svg' && !$self->_seen($name)) {
    100 66        
27 3         12 $self->_stash(svg => $elem);
28 3         11 return;
29             } elsif ($self->_stash('svg') && !$self->_stash('grouped')) {
30 3         6 my $svg = $self->_stash('svg');
31 3         6 my $group;
32 3 100 100     11 if ($name eq 'g' && (_attr($elem, 'id') || '') eq $self->_group_id) {
      66        
33 1         3 $group = $elem;
34             } else {
35 2 50       5 my $name = $svg->{Prefix} ? "$svg->{Prefix}:g" : "g";
36             $group = {
37             LocalName => 'g',
38             Name => $name,
39             Prefix => $svg->{Prefix},
40             NamespaceURI => $svg->{NamespaceURI},
41 2         10 Attributes => {
42             '{}id' => {
43             Name => 'id',
44             Value => $self->_group_id,
45             },
46             '{}transform' => {
47             Name => 'transform',
48             Value => '',
49             },
50             },
51             };
52              
53 2         6 $self->_stash(added_group => 1);
54             }
55              
56 3         11 $self->_update_tags($svg, $group);
57 3         44 $self->_stash(grouped => 1);
58 3         8 $self->_stash(svg => undef);
59 3 100       7 return unless $self->_stash('added_group');
60             }
61 9         25 $self->_push($name);
62 9         26 $self->SUPER::start_element(@_);
63             }
64              
65             sub comment {
66 1     1 1 160 my ($self, $comment) = @_;
67 1 50       5 if ($self->{_stash}) {
68 1         3 $self->{_comment} = $comment->{Data};
69 1         4 return;
70             }
71 0         0 $self->SUPER::comment($comment);
72             }
73              
74             sub end_element {
75 13     13 1 3250 my $self = shift;
76 13         21 my $elem = $_[0];
77 13         29 my $name = lc $elem->{LocalName};
78 13   50     38 my $prev = $self->{_stack}[-1] || '';
79 13 100       40 if ($name eq 'svg') {
80 3 100 66     7 if ($self->_seen($name) == 1 && $self->_stash('added_group') && $prev eq 'g') {
      66        
81 2         7 $self->_pop($prev);
82 2 50       5 my $name = $elem->{Prefix} ? "$elem->{Prefix}:g" : 'g';
83             $self->SUPER::end_element({
84             LocalName => 'g',
85             Name => $name,
86             Prefix => $elem->{Prefix},
87             NamespaceURI => $elem->{NamespaceURI},
88 2         12 });
89             }
90             }
91 13         266 $self->_pop($name);
92 13         50 $self->SUPER::end_element(@_);
93             }
94              
95 0 0   0 1 0 sub info { shift->{_info} || {} }
96              
97             sub _update_tags {
98 3     3   8 my ($self, $svg, $group) = @_;
99              
100 3         7 my $svg_width = _attr($svg, 'width');
101 3         7 my $svg_height = _attr($svg, 'height');
102 3         6 my $svg_viewbox = _attr($svg, 'viewBox');
103 3         7 my $svg_version = _attr($svg, 'version');
104              
105 3         7 my $transform = _attr($group, 'transform');
106              
107 3         5 my $view;
108 3         13 @$view{qw/min_x min_y max_x max_y tx ty/} = (0, 0, 0, 0, 0, 0);
109              
110 3 100       9 $svg_viewbox = $self->{_comment} if $self->{_comment};
111              
112 3 50       15 if ($svg_viewbox) {
113 3         9 @$view{qw/min_x min_y max_x max_y/} = _split($svg_viewbox);
114             } else {
115 0         0 $view->{max_x} = _numify($svg_width);
116 0         0 $view->{max_y} = _numify($svg_height);
117             }
118 3         14 _translate($view);
119              
120 3         11 my $scale = _scale($view, @$self{qw/Width Height/});
121 3         6 push @{$self->{_ops}}, ['scale', @$scale];
  3         16  
122              
123 3         12 $self->_parse_transform($transform);
124              
125 3         10 my $matrix = $self->_parse_transform($self->{Transform});
126              
127 3         8 $view = _to_hash($matrix * _to_matrix($view));
128 3         16 _translate($view);
129              
130 3         6 push @{$self->{_ops}}, ['translate', @$view{qw/tx ty/}];
  3         11  
131              
132 3         11 _attr($svg, 'width', $view->{max_x});
133 3         10 _attr($svg, 'height', $view->{max_y});
134 3         20 _attr($svg, 'viewBox', "0 0 $view->{max_x} $view->{max_y}");
135              
136 3         12 _attr($group, 'transform', $self->_ops_to_transform);
137              
138 3         9 $self->_push('svg');
139 3         8 $self->_push('g');
140 3         22 $self->SUPER::start_element($svg);
141 3         954 $self->SUPER::comment({Data => $svg_viewbox});
142 3         343 $self->SUPER::start_element($group);
143              
144             $self->{_info} = {
145             width => $view->{max_x},
146             height => $view->{max_y},
147 3         357 version => $svg_version,
148             };
149             }
150              
151             sub _parse_transform {
152 6     6   14 my ($self, $transform) = @_;
153              
154 6         8 my @ops = @{$self->{_ops}};
  6         15  
155 6 100       15 if ($transform) {
156 3         27 my @parts = (lc $transform) =~ /(\w+(?:\([^)]*\))?)/g;
157 3         7 for my $op (reverse @parts) {
158 4         23 my ($name, $arg) = $op =~ /^(\w+)(?:\(([^)]*)\))?$/;
159 4         11 my @args = _split($arg);
160 4 100       16 if ($name eq 'rotate') {
    50          
    50          
    50          
161 3 100 66     19 if (@ops && $ops[-1][0] eq 'rotate') {
162 1         3 $ops[-1][1] += $args[0];
163 1         4 $ops[-1][1] %= 360;
164             } else {
165 2         8 push @ops, ['rotate', @args];
166             }
167             } elsif ($name eq 'flipx') {
168 0         0 my $m = Math::Matrix->diagonal(-1, 1, -1, 1);
169 0 0 0     0 if (@ops && $ops[-1][0] eq 'matrix') {
170 0         0 $ops[-1][1] *= $m;
171             } else {
172 0         0 push @ops, ['matrix', $m];
173             }
174             } elsif ($name eq 'flipy') {
175 0         0 my $m = Math::Matrix->diagonal(1, -1, 1, -1);
176 0 0 0     0 if (@ops && $ops[-1][0] eq 'matrix') {
177 0         0 $ops[-1][1] *= $m;
178             } else {
179 0         0 push @ops, ['matrix', $m];
180             }
181             } elsif ($name eq 'matrix') {
182 0         0 my $m = Math::Matrix->new([
183             [$args[0], $args[2], 0, 0],
184             [$args[1], $args[3], 0, 0],
185             [0, 0, $args[0], $args[2]],
186             [0, 0, $args[1], $args[3]],
187             ]);
188 0 0 0     0 if (@ops && $ops[-1] eq 'matrix') {
189 0         0 $ops[-1][1] *= $m;
190             } else {
191 0         0 push @ops, ['matrix', $m];
192             }
193             }
194             }
195 3         9 $self->{_ops} = \@ops;
196             }
197              
198 6         23 my $matrix = $IdMatrix->clone;
199 6         140 for my $op (@ops) {
200 9         1624 my ($name, @args) = @$op;
201 9 100       38 if ($name eq 'rotate') {
    50          
    50          
202 3   50     30 my $angle = deg2rad($args[0] || 0);
203 3         64 my $sin = sin $angle;
204 3         20 my $cos = cos $angle;
205 3         17 my $m = Math::Matrix->new([
206             [$cos, -$sin, 0, 0],
207             [$sin, $cos, 0, 0],
208             [0, 0, $cos, -$sin],
209             [0, 0, $sin, $cos],
210             ]);
211 3         143 $matrix *= $m;
212 3 50       1471 if ($matrix->equal($IdMatrix)) {
213 0         0 $matrix = $IdMatrix->clone;
214             }
215             } elsif ($name eq 'matrix') {
216 0         0 $matrix *= $args[0];
217             } elsif ($name eq 'scale') {
218 6         59 $matrix *= Math::Matrix->new(
219             [$args[0], 0, 0, 0],
220             [0, $args[1], 0, 0],
221             [0, 0, $args[0], 0],
222             [0, 0, 0, $args[1]],
223             );
224             }
225             }
226 6         2079 return $matrix;
227             }
228              
229             sub _ops_to_transform {
230 3     3   4 my $self = shift;
231 3         16 my @transform;
232 3         5 for my $op (@{$self->{_ops}}) {
  3         8  
233 8         30 my ($name, @args) = @$op;
234 8 100       36 if ($name eq 'rotate') {
    50          
    100          
    50          
235 2 50       5 next if !$args[0];
236 2         8 push @transform, "rotate($args[0])";
237             } elsif ($name eq 'matrix') {
238 0 0       0 next if $args[0]->equal($IdMatrix);
239 0         0 my $flatten = join ' ', _flatten($args[0]);
240 0         0 push @transform, "matrix($flatten)";
241             } elsif ($name eq 'scale') {
242 3 100 66     16 next if $args[0] == 1 && $args[1] == 1;
243 1         8 push @transform, "scale($args[0] $args[1])";
244             } elsif ($name eq 'translate') {
245 3 0 33     8 next if !$args[0] && !$args[1];
246 3         18 push @transform, "translate($args[0] $args[1])";
247             }
248             }
249 3         17 join ' ', reverse @transform;
250             }
251              
252             sub _numify {
253 17 100   17   35 my $number = shift or return 0;
254 14         25 $number =~ tr/0-9.eE\-//cd;
255 14         28 $number += 0;
256 14         28 $number =~ s/\.0+$//;
257 14 50       48 $number || 0;
258             }
259              
260             sub _split {
261 7     7   12 my $value = shift;
262 7 50       25 return unless defined $value;
263 7         44 map { _numify($_) } split /(?:\s+|\s*,\s*)/, $value;
  17         34  
264             }
265              
266             sub _flatten {
267 0     0   0 my $matrix = shift;
268 0         0 my $array = $matrix->as_array;
269 0         0 my @values = map { _numify($_) } (
  0         0  
270             $array->[0][0],
271             $array->[1][0],
272             $array->[0][1],
273             $array->[1][1],
274             $array->[0][2] + $array->[0][3],
275             $array->[1][2] + $array->[1][3],
276             );
277 0         0 @values;
278             }
279              
280             sub _translate {
281 6     6   11 my $set = shift;
282              
283 6 100 66     27 if ($set->{min_x} && $set->{min_x} < 0) {
284 5         9 $set->{max_x} -= $set->{min_x};
285 5         10 $set->{tx} -= $set->{min_x};
286 5         8 $set->{min_x} = 0;
287             }
288 6 100 66     20 if ($set->{min_y} && $set->{min_y} < 0) {
289 1         5 $set->{max_y} -= $set->{min_y};
290 1         5 $set->{ty} -= $set->{min_y};
291 1         2 $set->{min_y} = 0;
292             }
293             }
294              
295             sub _to_matrix {
296 3     3   7 my $set = shift;
297             Math::Matrix->new([
298             [@$set{qw/min_x max_x min_x max_x/}],
299             [@$set{qw/min_y min_y max_y max_y/}],
300             [@$set{qw/tx tx tx tx/}],
301 3         20 [@$set{qw/ty ty ty ty/}],
302             ]);
303             }
304              
305             sub _to_hash {
306 3     3   1661 my $matrix = shift;
307 3         6 my %hash;
308 3         13 @hash{qw/min_x min_y max_x max_y tx ty/} = (0, 0, 0, 0, 0, 0);
309 3         12 my $x = $matrix->getrow(0);
310 3         110 my $y = $matrix->getrow(1);
311 3         104 my $tx = $matrix->getrow(2);
312 3         91 my $ty = $matrix->getrow(3);
313 3         107 $hash{min_x} = $x->min->as_array->[0][0];
314 3         189 $hash{max_x} = $x->max->as_array->[0][0];
315 3         170 $hash{min_y} = $y->min->as_array->[0][0];
316 3         152 $hash{max_y} = $y->max->as_array->[0][0];
317 3         147 $hash{tx} = $tx->min->as_array->[0][0];
318 3         145 $hash{ty} = $ty->min->as_array->[0][0];
319 3         151 \%hash;
320             }
321              
322             sub _scale {
323 3     3   8 my ($set, $x, $y) = @_;
324              
325 3         8 my ($scale_x, $scale_y) = (1, 1);
326 3 50 66     30 if ($x && $y) {
    100          
    50          
327 0 0       0 if ($set->{max_x}) {
328 0         0 $scale_x = $x / $set->{max_x};
329             }
330 0 0       0 if ($set->{max_y}) {
331 0         0 $scale_y = $y / $set->{max_y};
332             }
333             } elsif ($x) {
334 1 50       4 if ($set->{max_x}) {
335 1         4 $scale_x = $x / $set->{max_x};
336             }
337 1         3 $scale_y = $scale_x;
338             } elsif ($y) {
339 0 0       0 if ($set->{max_y}) {
340 0         0 $scale_y = $y / $set->{max_y};
341             }
342 0         0 $scale_x = $scale_y;
343             }
344 3         9 return [$scale_x, $scale_y];
345             }
346              
347             sub _push {
348 15     15   29 my ($self, $name) = @_;
349 15         25 push @{$self->{_stack}}, $name;
  15         36  
350             }
351              
352             sub _pop {
353 15     15   47 my ($self, $name) = @_;
354 15         23 my $popped = pop @{$self->{_stack}};
  15         33  
355 15 50       34 if ($name ne $popped) {
356 0         0 die "Broken! expected: $name got: $popped left:" . join(",", @{$self->{_stack}});
  0         0  
357             }
358 15         27 $popped;
359             }
360              
361             sub _seen {
362 6     6   16 my ($self, $name) = @_;
363 6         12 my $count = grep { $_ eq $name } @{$self->{_stack}};
  5         14  
  6         14  
364 6   100     35 return $count || 0;
365             }
366              
367             sub _group_id {
368 5     5   8 my $self = shift;
369 5 50       12 if ($self->{SessionId}) {
370 0         0 join '-', $GroupId, $self->{SessionId};
371             } else {
372 5         28 $GroupId;
373             }
374             }
375              
376             sub _stash {
377 33     33   55 my $self = shift;
378 33         49 my $key = shift;
379 33 100       67 if (@_) {
380 11         26 $self->{_stash}{$key} = shift;
381             }
382 33         130 $self->{_stash}{$key};
383             }
384              
385             sub _attr {
386 30     30   42 my $elem = shift;
387 30         41 my $name = shift;
388 30 100       56 if (@_) {
389 12         20 my $value = shift;
390 12 50 33     57 if (defined $value && $value ne '') {
391 12 100       75 if (!exists $elem->{Attributes}{"{}$name"}{Name}) {
392 4         13 $elem->{Attributes}{"{}$name"}{Name} = $name;
393             }
394 12         34 return $elem->{Attributes}{"{}$name"}{Value} = $value;
395             } else {
396 0         0 delete $elem->{Attributes}{"{}$name"};
397 0         0 return;
398             }
399             } else {
400 18 50       39 return unless exists $elem->{Attributes};
401 18 100       63 return unless exists $elem->{Attributes}{"{}$name"};
402 9         32 return $elem->{Attributes}{"{}$name"}{Value};
403             }
404             }
405              
406             1;
407              
408             __END__