File Coverage

blib/lib/SVGPDF/Element.pm
Criterion Covered Total %
statement 240 319 75.2
branch 85 152 55.9
condition 35 88 39.7
subroutine 29 35 82.8
pod 0 22 0.0
total 389 616 63.1


line stmt bran cond sub pod time code
1             #! perl
2              
3 2     2   26 use v5.26;
  2         8  
4 2     2   16 use Object::Pad;
  2         3  
  2         14  
5 2     2   270 use utf8;
  2         4  
  2         12  
6             class SVGPDF::Element;
7              
8 2     2   651 use Carp;
  2         5  
  2         26926  
9              
10 310     310 0 911 field $xo :mutator;
  310         1214  
11 30 50   30 0 99 field $style :accessor;
  30         98  
12 450 50   450 0 1125 field $name :param :accessor;
  450         2770  
13 192 50   192 0 497 field $atts :param :accessor;
  192         694  
14 0 0   0 0 0 field $css :accessor;
  0         0  
15 112 50   112 0 235 field $content :param :accessor; # array of children
  112         464  
16 1828 50   1828 0 3631 field $root :param :accessor; # top module
  1828         8593  
17              
18             BUILD {
19             $css = $root->css;
20             $xo = $root->xoforms->[-1]->{xo};
21             };
22              
23 1481     1481   2436 method _dbg (@args) {
  1481         3979  
  1481         3406  
  1481         2007  
24 1481         4915 $root->_dbg(@args);
25             }
26              
27 181     181 0 388 method css_push ( $updated_atts = undef ) {
  181         441  
  181         315  
  181         322  
28 181   33     279 $style = $css->push( element => $name, %{$updated_atts // $atts} );
  181         1093  
29             }
30              
31 181     181 0 318 method css_pop () {
  181         604  
  181         282  
32 181         743 $css->pop;
33             }
34              
35 53     53 0 97 method set_transform ( $tf ) {
  53         201  
  53         113  
  53         66  
36 53 100       165 return unless $tf;
37              
38 8         18 my $nooptimize = 1;
39 8         48 $tf =~ s/\s+/ /g;
40              
41             # The parts of the transform need to be executed in order.
42 8         44 while ( $tf =~ /\S/ ) {
43 9 100       121 if ( $tf =~ /^\s*translate\s*\((.*?)\)(.*)/ ) {
    50          
    0          
    0          
    0          
44 7         20 $tf = $2;
45 7         41 my ( $x, $y ) = $self->getargs($1);
46 7   100     54 $y ||= 0;
47 7 0 33     29 if ( $nooptimize || $x || $y ) {
      33        
48 7         49 $xo->transform( translate => [ $x, $y ] );
49 7         3979 $self->_dbg( "transform translate(%.2f,%.2f)", $x, $y );
50             }
51             }
52             elsif ( $tf =~ /^\s*rotate\s*\((.*?)\)(.*)/ ) {
53 2         9 $tf = $2;
54 2         10 my ( $r, $x, $y ) = $self->getargs($1);
55 2 50 33     12 if ( $nooptimize || $r ) {
56 2 50 33     29 if ( $x || $y ) {
57 0         0 $xo->transform( translate => [ $x, $y ] );
58 0         0 $self->_dbg( "transform translate(%.2f,%.2f)", $x, $y );
59             }
60 2         10 $self->_dbg( "transform rotate(%.2f)", $r );
61 2         18 $xo->transform( rotate => $r );
62 2 50 33     1355 if ( $x || $y ) {
63 0         0 $xo->transform( translate => [ -$x, -$y ] );
64 0         0 $self->_dbg( "transform translate(%.2f,%.2f)", -$x, -$y );
65             }
66             }
67             }
68             elsif ( $tf =~ /^\s*scale\s*\((.*?)\)(.*)/ ) {
69 0         0 $tf = $2;
70 0         0 my ( $x, $y ) = $self->getargs($1);
71 0   0     0 $y ||= $x;
72 0 0 0     0 if ( $nooptimize || $x != 1 && $y != 1 ) {
      0        
73 0         0 $self->_dbg( "transform scale(%.2f,%.2f)", $x, $y );
74 0         0 $xo->transform( scale => [ $x, $y ] );
75             }
76             }
77             elsif ( $tf =~ /^\s*matrix\s*\((.*?)\)(.*)/ ) {
78 0         0 $tf = $2;
79 0         0 my ( @m ) = $self->getargs($1);
80              
81             # 1 0 0 1 dx dy translate
82             # sx 0 0 sy 0 0 scale
83             # c s -s c 0 0 rotate (s = sin, c = cos)
84             # 1 a b 1 0 0 skew (a = tan a, b = tan b)
85              
86 0         0 $self->_dbg( "transform matrix(%.2f,%.2f %.2f,%.2f %.2f,%.2f)", @m);
87 0         0 $xo->matrix(@m);
88             }
89             elsif ( $tf =~ /^\s*skew([XY])\s*\((.*?)\)(.*)/i ) {
90 0         0 $tf = $3;
91 0         0 my ( $x ) = $self->getargs($2);
92 0         0 my $y = 0;
93 0 0       0 if ( $1 eq "X" ) {
94 0         0 $y = $x;
95 0         0 $x = 0;
96             }
97 0         0 $self->_dbg( "transform skew(%.2f %.2f)", $x, $y );
98 0         0 $xo->transform( skew => [ $x, $y ] );
99             }
100             else {
101 0 0       0 warn("Ignoring transform: $tf")
102             if $self->root->verbose;
103 0         0 $self->_dbg("Ignoring transform: \"$tf\"");
104 0         0 $tf = "";
105             }
106             # %rel = ( relative => 1 );
107             }
108             }
109              
110             #
111             #
112             #
113             # result = multiply_matrices( $m1, $m2, $m3 );
114              
115 0     0 0 0 method multiply_matrices :common (@m) {
  0         0  
  0         0  
  0         0  
116 0         0 my $i = @m;
117 0         0 my $m2 = pop(@m);
118 0 0       0 die("Matrix$i must have 6 elements\n") unless @$m2 == 6;
119              
120 0         0 while ( --$i > 0 ) {
121 0         0 my $m1 = pop(@m);
122 0 0       0 die("Matrix$i must have 6 elements\n") unless @$m1 == 6;
123 0         0 $m2 = [ $m1->[0] * $m2->[0] + $m1->[2] * $m2->[1],
124             $m1->[1] * $m2->[0] + $m1->[3] * $m2->[1],
125             $m1->[0] * $m2->[2] + $m1->[2] * $m2->[3],
126             $m1->[1] * $m2->[2] + $m1->[3] * $m2->[3],
127             $m1->[0] * $m2->[4] + $m1->[2] * $m2->[5] + $m1->[4],
128             $m1->[1] * $m2->[4] + $m1->[3] * $m2->[5] + $m1->[5] ];
129             }
130 0         0 $m2;
131             }
132              
133 116     116 0 243 method set_graphics () {
  116         339  
  116         208  
134              
135 116         260 my $msg = $name;
136              
137 116 100       407 if ( defined( my $lw = $style->{'stroke-width'} ) ) {
138             my $w = $self->u( $lw,
139             fontsize => $style->{'font-size'},
140 46         167 width => $self->root->xoforms->[-1]->{diag});
141 46         412 $msg .= sprintf(" stroke-width=%.2f", $w);
142 46 50       121 if ( $lw =~ /e[mx]/ ) {
143             $msg .= sprintf("(%s\@%.2f)", $lw,
144             $self->u( $style->{'font-size'}|| $self->root->fontsize,
145             fontsize => $style->{'font-size'},
146 0   0     0 width => $self->root->xoforms->[-1]->{diag}));
147             }
148 46 50       127 if ( $lw =~ /\%/ ) {
149             $msg .= sprintf("(%s\@%.2f)", $lw,
150             $self->u( $self->root->xoforms->[-1]->{diag},
151             fontsize => $style->{'font-size'},
152 0         0 width => $self->root->xoforms->[-1]->{diag}));
153             }
154 46         231 $xo->line_width($w);
155             }
156              
157 116 100       2442 if ( defined( my $linecap = $style->{'stroke-linecap'} ) ) {
158 8         49 $linecap = lc($linecap);
159 8 100       41 if ( $linecap eq "round" ) { $linecap = 1 }
  4 50       10  
    100          
    50          
160 0         0 elsif ( $linecap eq "r" ) { $linecap = 1 }
161 2         5 elsif ( $linecap eq "square" ) { $linecap = 2 }
162 0         0 elsif ( $linecap eq "s" ) { $linecap = 2 }
163 2         5 else { $linecap = 0 } # b butt
164 8         20 $msg .= " linecap=$linecap";
165 8         45 $xo->line_cap($linecap);
166             }
167              
168 116 100       819 if ( defined( my $linejoin = $style->{'stroke-linejoin'} ) ) {
169 3         10 $linejoin = lc($linejoin);
170 3 100       35 if ( $linejoin eq "round" ) { $linejoin = 1 }
  1 50       3  
    100          
    50          
171 0         0 elsif ( $linejoin eq "r" ) { $linejoin = 1 }
172 1         3 elsif ( $linejoin eq "bevel" ) { $linejoin = 2 }
173 0         0 elsif ( $linejoin eq "b" ) { $linejoin = 2 }
174 1         2 else { $linejoin = 0 } # m miter
175 3         8 $msg .= " linejoin=$linejoin";
176 3         27 $xo->line_join($linejoin);
177             }
178              
179 116         427 my $color = $style->{color};
180 116         280 my $stroke = $style->{stroke};
181 116 50       347 if ( lc($stroke) eq "currentcolor" ) {
182             # Nothing. Use current.
183 0         0 $msg .= " stroke=(current)";
184 0         0 $stroke = $color;
185             }
186 116 100       274 if ( $stroke ne "none" ) {
187 65         824 $stroke = SVGPDF::Colour->new( colour => $stroke )->rgb;
188 65         426 $xo->stroke_color($stroke);
189 65         10149 $msg .= " stroke=$stroke";
190             }
191             else {
192 51         121 $msg .= " stroke=none";
193             }
194              
195 116         326 my $fill = $style->{fill};
196 116 100       441 if ( lc($fill) eq "currentcolor" ) {
197             # Nothing. Use current.
198 44         93 $msg .= " fill=(current)";
199 44         82 $fill = $color;
200             }
201 116 100 100     560 if ( lc($fill) ne "none" && $fill ne "transparent" ) {
202 63         738 $fill = SVGPDF::Colour->new( colour => $fill )->rgb;
203 63         394 $xo->fill_color($fill);
204 63         10541 $msg .= " fill=$fill";
205             }
206             else {
207 53         97 $msg .= " fill=none";
208             }
209              
210 116 100       417 if ( my $sda = $style->{'stroke-dasharray'} ) {
211 2         4 my @sda;
212 2 50 33     13 if ( $sda && $sda ne "none" ) {
213 2         14 $sda =~ s/,/ /g;
214 2         10 @sda = split( ' ', $sda );
215             }
216 2         8 $msg .= " sda=@sda";
217 2         20 $xo->line_dash_pattern(@sda);
218             }
219              
220 116         712 $self->_dbg( "%s", $msg );
221 116         541 return $style;
222             }
223              
224             # Return a stroke/fill/paint sub depending on the fill stroke styles.
225 82     82   155 method _paintsub () {
  82         447  
  82         118  
226 82 100 100     1096 if ( $style->{stroke}
    50 100        
      33        
      33        
227             && $style->{stroke} ne 'none'
228             && $style->{stroke} ne 'transparent'
229             # Hmm. Saw a note somewhere that it defaults to 0 but other notes
230             # say that it should be 1px...
231             && $style->{'stroke-width'}//1 != 0
232             ) {
233 65 100 66     370 if ( $style->{fill}
      100        
234             && $style->{fill} ne 'none'
235             && $style->{fill} ne 'transparent'
236             ) {
237             return sub {
238             $self->_dbg("xo paint (",
239 12     12   75 join(" ", $style->{stroke}, $style->{fill} ), ")");
240 12         74 $xo->paint;
241 12         93 };
242             }
243             else {
244             return sub {
245 53     53   213 $self->_dbg("xo stroke (", $style->{stroke}, ")");
246 53         307 $xo->stroke;
247 53         403 };
248             }
249             }
250             elsif ( $style->{fill}
251             && $style->{fill} ne 'none'
252             && $style->{fill} ne 'transparent'
253             ) {
254             return sub {
255 17     17   76 $self->_dbg("xo fill (", $style->{stroke}, ")");
256 17         109 $xo->fill;
257 17         147 };
258             }
259             else {
260             return sub {
261 0     0   0 $self->_dbg("xo end");
262 0         0 $xo->end;
263             }
264 0         0 }
265             }
266              
267 5     5 0 7 method process () {
  5         18  
  5         8  
268             # Unless overridden in a subclass there's not much we can do.
269 5         16 state $warned = { desc => 1, title => 1, metadata => 1 };
270             warn("SVG: Skipping element \"$name\" (not implemented)\n")
271 5 50 33     26 unless $warned->{$name}++ || !$self->root->verbose;
272 5         18 $self->_dbg("skipping $name (not implemented)");
273             # $self->traverse;
274             }
275              
276 112     112 0 186 method get_children () {
  112         236  
  112         145  
277              
278             # Note: This is the only place where these objects are created.
279              
280 112         190 my @res;
281 112         207 for my $e ( @{$self->content} ) {
  112         463  
282 603 100       1674 if ( $e->{type} eq 'e' ) {
    50          
283 246         667 my $pkg = "SVGPDF::" . ucfirst(lc $e->{name});
284 246 100       1735 $pkg = "SVGPDF::Element" unless $pkg->can("process");
285             push( @res, $pkg->new
286             ( name => $e->{name},
287 954         3858 atts => { map { lc($_) => $e->{attrib}->{$_} } keys %{$e->{attrib}} },
  246         973  
288             content => $e->{content},
289 246         551 root => $self->root,
290             ) );
291             }
292             elsif ( $e->{type} eq 't' ) {
293             push( @res, SVGPDF::TextElement->new
294             ( content => $e->{content},
295 357         2188 ) );
296             }
297             else {
298             # Basically a 'cannot happen',
299 0         0 croak("Unhandled node type ", $e->{type});
300             }
301             }
302 112         441 return @res;
303             }
304              
305 47     47 0 90 method traverse () {
  47         125  
  47         71  
306 47         157 for my $c ( $self->get_children ) {
307 339 100       1093 next if ref($c) eq "SVGPDF::TextElement";
308 146         488 $self->_dbg("+ start handling ", $c->name, " (", ref($c), ")");
309 146         881 $c->process;
310 146         496 $self->_dbg("- end handling ", $c->name);
311             }
312             }
313              
314 703     703 0 1068 method u ( $a, %args ) {
  703         1646  
  703         1281  
  703         1378  
  703         1067  
315 703 50       1773 confess("Undef in units") unless defined $a;
316              
317             # Pixels per point. Usually 96/72.
318 703         1585 my $pxpt = $self->root->pxpi / $self->root->ptpi;
319              
320 703 50       3395 return undef unless $a =~ /^([-+]?[\d.]+)(.*)$/;
321 703 50       1918 return $1*$pxpt if $2 eq "pt";
322              
323             # default is px
324 703 100 100     3401 return $1 if $2 eq "" || $2 eq "px";
325              
326             # 1 inch = pxpi px.
327 8 50       58 return $1/2.54 * $self->root->pxpi if $2 eq "cm";
328 0 0       0 return $1/25.4 * $self->root->pxpi if $2 eq "mm";
329 0 0       0 return $1 * $self->root->pxpi if $2 eq "in";
330              
331 0 0       0 if ( $2 eq '%' ) {
332 0   0     0 my $w = $args{width} || $self->root->xoforms->[-1]->{diag};
333 0         0 return $1/100 * $w * $pxpt;
334             }
335             # Font dependent.
336             # CSS defines em to be the font size.
337 0 0       0 if ( $2 eq "em" ) {
338             return $1 * ( $args{fontsize}
339 0   0     0 || $style->{'font-size'}
340             || $self->root->fontsize );
341             }
342             # CSS defines ex to be half the font size.
343 0 0       0 if ( $2 eq "ex" ) {
344             return $1 * 0.5 * ( $args{fontsize}
345 0   0     0 || $style->{'font-size'}
346             || $self->root->fontsize );
347             }
348              
349 0         0 confess("Unhandled units in \"$a\"");
350 0         0 return $a; # will hopefully crash somewhere...
351             }
352              
353 20     20 0 44 method getargs ( $a ) {
  20         67  
  20         40  
  20         31  
354 20 50       58 confess("Null attr?") unless defined $a;
355 20         75 $a =~ s/^\s+//;
356 20         100 $a =~ s/\s+$//;
357 20         203 map { $self->u($_) } split( /\s*[,\s]\s*/, $a );
  126         352  
358             }
359              
360             # Initial fiddling with entity attributes.
361 149     149 0 285 method get_params ( @desc ) {
  149         322  
  149         579  
  149         225  
362 149 50       538 my $atts = shift(@desc) if ref($desc[0]) eq 'HASH';
363 149         279 my @res;
364 149   33     257 my %atts = %{ $atts // $self->atts }; # copy
  149         1335  
365              
366             # xlink:href is obsoleted in favour of href.
367 149 100 33     578 $atts{href} //= delete $atts{"xlink:href"} if exists $atts{"xlink:href"};
368              
369 149         232 my @todo;
370 149         312 for my $param ( @desc ) {
371              
372             # Attribute may be followed by ':' and flags.
373             # 0 undef -> 0
374             # h process units, % is viewBox height
375             # s undef -> ""
376             # u process units
377             # v process units, % is viewBox width
378             # U undef -> 0, process units
379             # ! barf if undef
380 586         877 my $flags = "";
381 586 100       3364 ( $param, $flags ) = ( $1, $2 )
382             if $param =~ /^(.*):(.*)$/;
383 586         1101 $param = lc($param);
384              
385             # Get and remove the attribute.
386 586         992 my $p = delete( $atts{$param} );
387              
388             # Queue.
389 586         1785 push( @todo, [ $param, $flags, $p ] );
390             }
391              
392             # CSS push with updated attributes.
393 149         636 $self->css_push( \%atts );
394              
395             # Now we can process the values.
396 149         542 for ( @todo ) {
397 586         1686 my ( $param, $flags, $p ) = @$_;
398              
399 586 100       1161 unless ( defined $p ) {
400 236 100       817 if ( $flags =~ /s/ ) { $p = ""; }
  145 50       258  
401 91         142 elsif ( $flags =~ /[0HUV]/ ) { $p = 0; }
402             else {
403 0 0       0 croak("Undefined mandatory attribute: $param")
404             if $flags =~ /\!/;
405 0         0 push( @res, $p );
406 0         0 next;
407             }
408             }
409              
410 586         980 $flags = lc($flags);
411             # Convert units if 'u' flag.
412 586 100       1767 if ( $flags =~ /([huv])/ ) {
413 314         624 my $flag = $1;
414 314 100       716 if ( $p =~ /^([\d.]+)\%$/ ) {
415 2         8 $p = $1/100;
416 2 100 66     27 if ( $flags eq "w" || $param =~ /^(?:w(?:idth)|x)?$/i ) {
    50 33        
417             # Percentage of viewBox width.
418 1         6 $p *= $root->xoforms->[-1]->{width};
419             }
420             elsif ( $flag eq "h" || $param =~ /^(?:h(?:eight)?|y)$/i ) {
421             # Percentage of viewBox height.
422 1         6 $p *= $root->xoforms->[-1]->{height};
423             }
424             else {
425             # Percentage of viewBox diagonal.
426 0         0 $p *= $root->xoforms->[-1]->{diag};
427             }
428             }
429             else {
430 312         771 $p = $self->u($p);
431             }
432             }
433              
434 586         1324 push( @res, $p );
435             }
436              
437             # Return param values.
438 149         1609 return @res;
439             }
440              
441 0     0 0 0 method get_cdata () {
  0         0  
  0         0  
442 0         0 my $res = "";
443 0         0 for ( $self->get_children ) {
444 0 0       0 $res .= "\n" . $_->content if ref($_) eq "SVGPDF::TextElement";
445             }
446 0         0 $res;
447             }
448              
449 1     1 0 3 method nfi ( $tag ) {
  1         4  
  1         2  
  1         3  
450 1         3 state $aw = {};
451             warn("SVG: $tag - not fully implemented, expect strange results.\n")
452 1 50 33     3 unless !$self->root->verbose || $aw->{$tag}++;
453             }
454              
455 33     33 0 55 method set_font ( $xo, $style ) {
  33         142  
  33         53  
  33         49  
  33         45  
456 33         58 my $msg ="";
457 33         46 my $ret;
458             {
459 33     0   46 local $SIG{__WARN__} = sub { $msg .= "@_" };
  33         369  
  0         0  
460 33         129 $ret = $self->root->fontmanager->set_font( $xo, $style );
461             }
462 33 50 33     5356 if ( $msg && $self->root->verbose ) {
463 0         0 warn($msg);
464             }
465 33         117 $ret;
466             }
467              
468             ################ Bounding Box ################
469              
470             # method bb ( $x, $y, $t = 0 ) {
471             # my $bb = $self->root->xoforms->[-1]->{bb};
472             #
473             # $t = $self->u($t) unless $t =~ /^[-+]?\d*(?:\.\d*)$/;
474             # $t /= 2;
475             # $bb->[0] = $x-$t if $bb->[0] > $x-$t;
476             # $bb->[1] = $y-$t if $bb->[1] > $y-$t;
477             # $bb->[2] = $x+$t if $bb->[2] < $x+$t;
478             # $bb->[3] = $y+$t if $bb->[3] < $y+$t;
479             #
480             # return $bb;
481             # }
482             #
483              
484             ################ Utility ################
485              
486 8     8 0 16 method data_inline($src) {
  8         27  
  8         16  
  8         11  
487              
488 8         40 my %info;
489              
490 8 50       456 unless ( $src =~ m! ^ data:
491             (? [^/]+ ) /
492             (? [^;]+ ) ;
493             (? [^,]+ ) ,
494             (? . + ) $
495             !sx ) {
496 0         0 return { error => "Malformed inline data" };
497             }
498              
499 8 50       150 if ( $+{encoding} eq "base64" ) {
500 8         94 require MIME::Base64;
501 8         533 $info{data} = MIME::Base64::decode($+{data});
502             }
503             else {
504 0         0 return { error => "Unhandled encoding \"$+{encoding}\" in inline data" };
505             }
506              
507 8         66 $info{mimetype} = $+{mimetype};
508 8         44 $info{subtype} = $+{subtype};
509              
510 8         36 return \%info;
511             }
512              
513             ################ TextElement ################
514              
515             class SVGPDF::TextElement;
516              
517 35 50   35   133 field $content :param :accessor;
  35         269  
518              
519             # Actually, we should take style->{white-space} into account...
520             BUILD {
521             # Reduce whitespace.
522             $content =~ s/\s+/ /g;
523             }
524              
525 0     0     method process () {
  0            
526             # Nothing to process.
527             }
528              
529             1;