File Coverage

lib/ChordPro/lib/SVGPDF/Element.pm
Criterion Covered Total %
statement 11 319 3.4
branch 0 152 0.0
condition 0 88 0.0
subroutine 4 35 11.4
pod 0 22 0.0
total 15 616 2.4


line stmt bran cond sub pod time code
1             #! perl
2              
3 10     10   194 use v5.26;
  10         45  
4 10     10   72 use Object::Pad;
  10         41  
  10         89  
5 10     10   1520 use utf8;
  10         24  
  10         74  
6             class SVGPDF::Element;
7              
8 10     10   3383 use Carp;
  10         30  
  10         146103  
9              
10 0     0 0   field $xo :mutator;
  0            
11 0 0   0 0   field $style :accessor;
  0            
12 0 0   0 0   field $name :param :accessor;
  0            
13 0 0   0 0   field $atts :param :accessor;
  0            
14 0 0   0 0   field $css :accessor;
  0            
15 0 0   0 0   field $content :param :accessor; # array of children
  0            
16 0 0   0 0   field $root :param :accessor; # top module
  0            
17              
18             BUILD {
19             $css = $root->css;
20             $xo = $root->xoforms->[-1]->{xo};
21             };
22              
23 0     0     method _dbg (@args) {
  0            
  0            
  0            
24 0           $root->_dbg(@args);
25             }
26              
27 0     0 0   method css_push ( $updated_atts = undef ) {
  0            
  0            
  0            
28 0   0       $style = $css->push( element => $name, %{$updated_atts // $atts} );
  0            
29             }
30              
31 0     0 0   method css_pop () {
  0            
  0            
32 0           $css->pop;
33             }
34              
35 0     0 0   method set_transform ( $tf ) {
  0            
  0            
  0            
36 0 0         return unless $tf;
37              
38 0           my $nooptimize = 1;
39 0           $tf =~ s/\s+/ /g;
40              
41             # The parts of the transform need to be executed in order.
42 0           while ( $tf =~ /\S/ ) {
43 0 0         if ( $tf =~ /^\s*translate\s*\((.*?)\)(.*)/ ) {
    0          
    0          
    0          
    0          
44 0           $tf = $2;
45 0           my ( $x, $y ) = $self->getargs($1);
46 0   0       $y ||= 0;
47 0 0 0       if ( $nooptimize || $x || $y ) {
      0        
48 0           $xo->transform( translate => [ $x, $y ] );
49 0           $self->_dbg( "transform translate(%.2f,%.2f)", $x, $y );
50             }
51             }
52             elsif ( $tf =~ /^\s*rotate\s*\((.*?)\)(.*)/ ) {
53 0           $tf = $2;
54 0           my ( $r, $x, $y ) = $self->getargs($1);
55 0 0 0       if ( $nooptimize || $r ) {
56 0 0 0       if ( $x || $y ) {
57 0           $xo->transform( translate => [ $x, $y ] );
58 0           $self->_dbg( "transform translate(%.2f,%.2f)", $x, $y );
59             }
60 0           $self->_dbg( "transform rotate(%.2f)", $r );
61 0           $xo->transform( rotate => $r );
62 0 0 0       if ( $x || $y ) {
63 0           $xo->transform( translate => [ -$x, -$y ] );
64 0           $self->_dbg( "transform translate(%.2f,%.2f)", -$x, -$y );
65             }
66             }
67             }
68             elsif ( $tf =~ /^\s*scale\s*\((.*?)\)(.*)/ ) {
69 0           $tf = $2;
70 0           my ( $x, $y ) = $self->getargs($1);
71 0   0       $y ||= $x;
72 0 0 0       if ( $nooptimize || $x != 1 && $y != 1 ) {
      0        
73 0           $self->_dbg( "transform scale(%.2f,%.2f)", $x, $y );
74 0           $xo->transform( scale => [ $x, $y ] );
75             }
76             }
77             elsif ( $tf =~ /^\s*matrix\s*\((.*?)\)(.*)/ ) {
78 0           $tf = $2;
79 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           $self->_dbg( "transform matrix(%.2f,%.2f %.2f,%.2f %.2f,%.2f)", @m);
87 0           $xo->matrix(@m);
88             }
89             elsif ( $tf =~ /^\s*skew([XY])\s*\((.*?)\)(.*)/i ) {
90 0           $tf = $3;
91 0           my ( $x ) = $self->getargs($2);
92 0           my $y = 0;
93 0 0         if ( $1 eq "X" ) {
94 0           $y = $x;
95 0           $x = 0;
96             }
97 0           $self->_dbg( "transform skew(%.2f %.2f)", $x, $y );
98 0           $xo->transform( skew => [ $x, $y ] );
99             }
100             else {
101 0 0         warn("Ignoring transform: $tf")
102             if $self->root->verbose;
103 0           $self->_dbg("Ignoring transform: \"$tf\"");
104 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   method multiply_matrices :common (@m) {
  0            
  0            
  0            
116 0           my $i = @m;
117 0           my $m2 = pop(@m);
118 0 0         die("Matrix$i must have 6 elements\n") unless @$m2 == 6;
119              
120 0           while ( --$i > 0 ) {
121 0           my $m1 = pop(@m);
122 0 0         die("Matrix$i must have 6 elements\n") unless @$m1 == 6;
123 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           $m2;
131             }
132              
133 0     0 0   method set_graphics () {
  0            
  0            
134              
135 0           my $msg = $name;
136              
137 0 0         if ( defined( my $lw = $style->{'stroke-width'} ) ) {
138             my $w = $self->u( $lw,
139             fontsize => $style->{'font-size'},
140 0           width => $self->root->xoforms->[-1]->{diag});
141 0           $msg .= sprintf(" stroke-width=%.2f", $w);
142 0 0         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       width => $self->root->xoforms->[-1]->{diag}));
147             }
148 0 0         if ( $lw =~ /\%/ ) {
149             $msg .= sprintf("(%s\@%.2f)", $lw,
150             $self->u( $self->root->xoforms->[-1]->{diag},
151             fontsize => $style->{'font-size'},
152 0           width => $self->root->xoforms->[-1]->{diag}));
153             }
154 0           $xo->line_width($w);
155             }
156              
157 0 0         if ( defined( my $linecap = $style->{'stroke-linecap'} ) ) {
158 0           $linecap = lc($linecap);
159 0 0         if ( $linecap eq "round" ) { $linecap = 1 }
  0 0          
    0          
    0          
160 0           elsif ( $linecap eq "r" ) { $linecap = 1 }
161 0           elsif ( $linecap eq "square" ) { $linecap = 2 }
162 0           elsif ( $linecap eq "s" ) { $linecap = 2 }
163 0           else { $linecap = 0 } # b butt
164 0           $msg .= " linecap=$linecap";
165 0           $xo->line_cap($linecap);
166             }
167              
168 0 0         if ( defined( my $linejoin = $style->{'stroke-linejoin'} ) ) {
169 0           $linejoin = lc($linejoin);
170 0 0         if ( $linejoin eq "round" ) { $linejoin = 1 }
  0 0          
    0          
    0          
171 0           elsif ( $linejoin eq "r" ) { $linejoin = 1 }
172 0           elsif ( $linejoin eq "bevel" ) { $linejoin = 2 }
173 0           elsif ( $linejoin eq "b" ) { $linejoin = 2 }
174 0           else { $linejoin = 0 } # m miter
175 0           $msg .= " linejoin=$linejoin";
176 0           $xo->line_join($linejoin);
177             }
178              
179 0           my $color = $style->{color};
180 0           my $stroke = $style->{stroke};
181 0 0         if ( lc($stroke) eq "currentcolor" ) {
182             # Nothing. Use current.
183 0           $msg .= " stroke=(current)";
184 0           $stroke = $color;
185             }
186 0 0         if ( $stroke ne "none" ) {
187 0           $stroke = SVGPDF::Colour->new( colour => $stroke )->rgb;
188 0           $xo->stroke_color($stroke);
189 0           $msg .= " stroke=$stroke";
190             }
191             else {
192 0           $msg .= " stroke=none";
193             }
194              
195 0           my $fill = $style->{fill};
196 0 0         if ( lc($fill) eq "currentcolor" ) {
197             # Nothing. Use current.
198 0           $msg .= " fill=(current)";
199 0           $fill = $color;
200             }
201 0 0 0       if ( lc($fill) ne "none" && $fill ne "transparent" ) {
202 0           $fill = SVGPDF::Colour->new( colour => $fill )->rgb;
203 0           $xo->fill_color($fill);
204 0           $msg .= " fill=$fill";
205             }
206             else {
207 0           $msg .= " fill=none";
208             }
209              
210 0 0         if ( my $sda = $style->{'stroke-dasharray'} ) {
211 0           my @sda;
212 0 0 0       if ( $sda && $sda ne "none" ) {
213 0           $sda =~ s/,/ /g;
214 0           @sda = split( ' ', $sda );
215             }
216 0           $msg .= " sda=@sda";
217 0           $xo->line_dash_pattern(@sda);
218             }
219              
220 0           $self->_dbg( "%s", $msg );
221 0           return $style;
222             }
223              
224             # Return a stroke/fill/paint sub depending on the fill stroke styles.
225 0     0     method _paintsub () {
  0            
  0            
226 0 0 0       if ( $style->{stroke}
    0 0        
      0        
      0        
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 0 0 0       if ( $style->{fill}
      0        
234             && $style->{fill} ne 'none'
235             && $style->{fill} ne 'transparent'
236             ) {
237             return sub {
238             $self->_dbg("xo paint (",
239 0     0     join(" ", $style->{stroke}, $style->{fill} ), ")");
240 0           $xo->paint;
241 0           };
242             }
243             else {
244             return sub {
245 0     0     $self->_dbg("xo stroke (", $style->{stroke}, ")");
246 0           $xo->stroke;
247 0           };
248             }
249             }
250             elsif ( $style->{fill}
251             && $style->{fill} ne 'none'
252             && $style->{fill} ne 'transparent'
253             ) {
254             return sub {
255 0     0     $self->_dbg("xo fill (", $style->{stroke}, ")");
256 0           $xo->fill;
257 0           };
258             }
259             else {
260             return sub {
261 0     0     $self->_dbg("xo end");
262 0           $xo->end;
263             }
264 0           }
265             }
266              
267 0     0 0   method process () {
  0            
  0            
268             # Unless overridden in a subclass there's not much we can do.
269 0           state $warned = { desc => 1, title => 1, metadata => 1 };
270             warn("SVG: Skipping element \"$name\" (not implemented)\n")
271 0 0 0       unless $warned->{$name}++ || !$self->root->verbose;
272 0           $self->_dbg("skipping $name (not implemented)");
273             # $self->traverse;
274             }
275              
276 0     0 0   method get_children () {
  0            
  0            
277              
278             # Note: This is the only place where these objects are created.
279              
280 0           my @res;
281 0           for my $e ( @{$self->content} ) {
  0            
282 0 0         if ( $e->{type} eq 'e' ) {
    0          
283 0           my $pkg = "SVGPDF::" . ucfirst(lc $e->{name});
284 0 0         $pkg = "SVGPDF::Element" unless $pkg->can("process");
285             push( @res, $pkg->new
286             ( name => $e->{name},
287 0           atts => { map { lc($_) => $e->{attrib}->{$_} } keys %{$e->{attrib}} },
  0            
288             content => $e->{content},
289 0           root => $self->root,
290             ) );
291             }
292             elsif ( $e->{type} eq 't' ) {
293             push( @res, SVGPDF::TextElement->new
294             ( content => $e->{content},
295 0           ) );
296             }
297             else {
298             # Basically a 'cannot happen',
299 0           croak("Unhandled node type ", $e->{type});
300             }
301             }
302 0           return @res;
303             }
304              
305 0     0 0   method traverse () {
  0            
  0            
306 0           for my $c ( $self->get_children ) {
307 0 0         next if ref($c) eq "SVGPDF::TextElement";
308 0           $self->_dbg("+ start handling ", $c->name, " (", ref($c), ")");
309 0           $c->process;
310 0           $self->_dbg("- end handling ", $c->name);
311             }
312             }
313              
314 0     0 0   method u ( $a, %args ) {
  0            
  0            
  0            
  0            
315 0 0         confess("Undef in units") unless defined $a;
316              
317             # Pixels per point. Usually 96/72.
318 0           my $pxpt = $self->root->pxpi / $self->root->ptpi;
319              
320 0 0         return undef unless $a =~ /^([-+]?[\d.]+)(.*)$/;
321 0 0         return $1*$pxpt if $2 eq "pt";
322              
323             # default is px
324 0 0 0       return $1 if $2 eq "" || $2 eq "px";
325              
326             # 1 inch = pxpi px.
327 0 0         return $1/2.54 * $self->root->pxpi if $2 eq "cm";
328 0 0         return $1/25.4 * $self->root->pxpi if $2 eq "mm";
329 0 0         return $1 * $self->root->pxpi if $2 eq "in";
330              
331 0 0         if ( $2 eq '%' ) {
332 0   0       my $w = $args{width} || $self->root->xoforms->[-1]->{diag};
333 0           return $1/100 * $w * $pxpt;
334             }
335             # Font dependent.
336             # CSS defines em to be the font size.
337 0 0         if ( $2 eq "em" ) {
338             return $1 * ( $args{fontsize}
339 0   0       || $style->{'font-size'}
340             || $self->root->fontsize );
341             }
342             # CSS defines ex to be half the font size.
343 0 0         if ( $2 eq "ex" ) {
344             return $1 * 0.5 * ( $args{fontsize}
345 0   0       || $style->{'font-size'}
346             || $self->root->fontsize );
347             }
348              
349 0           confess("Unhandled units in \"$a\"");
350 0           return $a; # will hopefully crash somewhere...
351             }
352              
353 0     0 0   method getargs ( $a ) {
  0            
  0            
  0            
354 0 0         confess("Null attr?") unless defined $a;
355 0           $a =~ s/^\s+//;
356 0           $a =~ s/\s+$//;
357 0           map { $self->u($_) } split( /\s*[,\s]\s*/, $a );
  0            
358             }
359              
360             # Initial fiddling with entity attributes.
361 0     0 0   method get_params ( @desc ) {
  0            
  0            
  0            
362 0 0         my $atts = shift(@desc) if ref($desc[0]) eq 'HASH';
363 0           my @res;
364 0   0       my %atts = %{ $atts // $self->atts }; # copy
  0            
365              
366             # xlink:href is obsoleted in favour of href.
367 0 0 0       $atts{href} //= delete $atts{"xlink:href"} if exists $atts{"xlink:href"};
368              
369 0           my @todo;
370 0           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 0           my $flags = "";
381 0 0         ( $param, $flags ) = ( $1, $2 )
382             if $param =~ /^(.*):(.*)$/;
383 0           $param = lc($param);
384              
385             # Get and remove the attribute.
386 0           my $p = delete( $atts{$param} );
387              
388             # Queue.
389 0           push( @todo, [ $param, $flags, $p ] );
390             }
391              
392             # CSS push with updated attributes.
393 0           $self->css_push( \%atts );
394              
395             # Now we can process the values.
396 0           for ( @todo ) {
397 0           my ( $param, $flags, $p ) = @$_;
398              
399 0 0         unless ( defined $p ) {
400 0 0         if ( $flags =~ /s/ ) { $p = ""; }
  0 0          
401 0           elsif ( $flags =~ /[0HUV]/ ) { $p = 0; }
402             else {
403 0 0         croak("Undefined mandatory attribute: $param")
404             if $flags =~ /\!/;
405 0           push( @res, $p );
406 0           next;
407             }
408             }
409              
410 0           $flags = lc($flags);
411             # Convert units if 'u' flag.
412 0 0         if ( $flags =~ /([huv])/ ) {
413 0           my $flag = $1;
414 0 0         if ( $p =~ /^([\d.]+)\%$/ ) {
415 0           $p = $1/100;
416 0 0 0       if ( $flags eq "w" || $param =~ /^(?:w(?:idth)|x)?$/i ) {
    0 0        
417             # Percentage of viewBox width.
418 0           $p *= $root->xoforms->[-1]->{width};
419             }
420             elsif ( $flag eq "h" || $param =~ /^(?:h(?:eight)?|y)$/i ) {
421             # Percentage of viewBox height.
422 0           $p *= $root->xoforms->[-1]->{height};
423             }
424             else {
425             # Percentage of viewBox diagonal.
426 0           $p *= $root->xoforms->[-1]->{diag};
427             }
428             }
429             else {
430 0           $p = $self->u($p);
431             }
432             }
433              
434 0           push( @res, $p );
435             }
436              
437             # Return param values.
438 0           return @res;
439             }
440              
441 0     0 0   method get_cdata () {
  0            
  0            
442 0           my $res = "";
443 0           for ( $self->get_children ) {
444 0 0         $res .= "\n" . $_->content if ref($_) eq "SVGPDF::TextElement";
445             }
446 0           $res;
447             }
448              
449 0     0 0   method nfi ( $tag ) {
  0            
  0            
  0            
450 0           state $aw = {};
451             warn("SVG: $tag - not fully implemented, expect strange results.\n")
452 0 0 0       unless !$self->root->verbose || $aw->{$tag}++;
453             }
454              
455 0     0 0   method set_font ( $xo, $style ) {
  0            
  0            
  0            
  0            
456 0           my $msg ="";
457 0           my $ret;
458             {
459 0     0     local $SIG{__WARN__} = sub { $msg .= "@_" };
  0            
  0            
460 0           $ret = $self->root->fontmanager->set_font( $xo, $style );
461             }
462 0 0 0       if ( $msg && $self->root->verbose ) {
463 0           warn($msg);
464             }
465 0           $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 0     0 0   method data_inline($src) {
  0            
  0            
  0            
487              
488 0           my %info;
489              
490 0 0         unless ( $src =~ m! ^ data:
491             (? [^/]+ ) /
492             (? [^;]+ ) ;
493             (? [^,]+ ) ,
494             (? . + ) $
495             !sx ) {
496 0           return { error => "Malformed inline data" };
497             }
498              
499 0 0         if ( $+{encoding} eq "base64" ) {
500 0           require MIME::Base64;
501 0           $info{data} = MIME::Base64::decode($+{data});
502             }
503             else {
504 0           return { error => "Unhandled encoding \"$+{encoding}\" in inline data" };
505             }
506              
507 0           $info{mimetype} = $+{mimetype};
508 0           $info{subtype} = $+{subtype};
509              
510 0           return \%info;
511             }
512              
513             ################ TextElement ################
514              
515             class SVGPDF::TextElement;
516              
517 0 0   0     field $content :param :accessor;
  0            
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;