File Coverage

lib/ChordPro/lib/SVGPDF/Element.pm
Criterion Covered Total %
statement 11 299 3.6
branch 0 152 0.0
condition 0 88 0.0
subroutine 4 33 12.1
pod 0 20 0.0
total 15 592 2.5


line stmt bran cond sub pod time code
1             #! perl
2              
3 1     1   13 use v5.26;
  1         5  
4 1     1   6 use Object::Pad;
  1         3  
  1         7  
5 1     1   98 use utf8;
  1         2  
  1         6  
6             class SVGPDF::Element;
7              
8 1     1   228 use Carp;
  1         3  
  1         9168  
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 0     0 0   method set_graphics () {
  0            
  0            
111              
112 0           my $msg = $name;
113              
114 0 0         if ( defined( my $lw = $style->{'stroke-width'} ) ) {
115             my $w = $self->u( $lw,
116             fontsize => $style->{'font-size'},
117 0           width => $self->root->xoforms->[-1]->{diag});
118 0           $msg .= " stroke-width=$w";
119 0 0         if ( $lw =~ /e[mx]/ ) {
120             $msg .= "($lw@" .
121 0   0       ( $style->{'font-size'}|| $self->root->fontsize) . ")";
122             }
123 0 0         if ( $lw =~ /\%/ ) {
124             $msg .= "($lw@" .
125 0           ( $self->root->xoforms->[-1]->{diag}) . ")";
126             }
127 0           $xo->line_width($w);
128             }
129              
130 0 0         if ( defined( my $linecap = $style->{'stroke-linecap'} ) ) {
131 0           $linecap = lc($linecap);
132 0 0         if ( $linecap eq "round" ) { $linecap = 1 }
  0 0          
    0          
    0          
133 0           elsif ( $linecap eq "r" ) { $linecap = 1 }
134 0           elsif ( $linecap eq "square" ) { $linecap = 2 }
135 0           elsif ( $linecap eq "s" ) { $linecap = 2 }
136 0           else { $linecap = 0 } # b butt
137 0           $msg .= " linecap=$linecap";
138 0           $xo->line_cap($linecap);
139             }
140              
141 0 0         if ( defined( my $linejoin = $style->{'stroke-linejoin'} ) ) {
142 0           $linejoin = lc($linejoin);
143 0 0         if ( $linejoin eq "round" ) { $linejoin = 1 }
  0 0          
    0          
    0          
144 0           elsif ( $linejoin eq "r" ) { $linejoin = 1 }
145 0           elsif ( $linejoin eq "bevel" ) { $linejoin = 2 }
146 0           elsif ( $linejoin eq "b" ) { $linejoin = 2 }
147 0           else { $linejoin = 0 } # m miter
148 0           $msg .= " linejoin=$linejoin";
149 0           $xo->line_join($linejoin);
150             }
151              
152 0           my $color = $style->{color};
153 0           my $stroke = $style->{stroke};
154 0 0         if ( lc($stroke) eq "currentcolor" ) {
155             # Nothing. Use current.
156 0           $msg .= " stroke=(current)";
157 0           $stroke = $color;
158             }
159 0 0         if ( $stroke ne "none" ) {
160 0           $stroke =~ s/\s+//g;
161 0 0         if ( $stroke =~ /rgb\(([\d.]+)%,([\d.]+)%,([\d.]+)%\)/ ) {
    0          
162             $stroke = sprintf("#%02X%02X%02X",
163 0           map { $_*2.55 } $1, $2, $3);
  0            
164             }
165             elsif ( $stroke =~ /rgb\(([\d.]+),([\d.]+),([\d.]+)\)/ ) {
166 0           $stroke = sprintf("#%02X%02X%02X", $1, $2, $3);
167             }
168 0           $xo->stroke_color($stroke);
169 0           $msg .= " stroke=$stroke";
170             }
171             else {
172 0           $msg .= " stroke=none";
173             }
174              
175 0           my $fill = $style->{fill};
176 0 0         if ( lc($fill) eq "currentcolor" ) {
177             # Nothing. Use current.
178 0           $msg .= " fill=(current)";
179 0           $fill = $color;
180             }
181 0 0 0       if ( lc($fill) ne "none" && $fill ne "transparent" ) {
182 0           $fill =~ s/\s+//g;
183 0 0         if ( $fill =~ /rgb\(([\d.]+)%,([\d.]+)%,([\d.]+)%\)/ ) {
    0          
184             $fill = sprintf("#%02X%02X%02X",
185 0           map { $_*2.55 } $1, $2, $3);
  0            
186             }
187             elsif ( $fill =~ /rgb\(([\d.]+),([\d.]+),([\d.]+)\)/ ) {
188 0           $fill = sprintf("#%02X%02X%02X", $1, $2, $3);
189             }
190 0           $xo->fill_color($fill);
191 0           $msg .= " fill=$fill";
192             }
193             else {
194 0           $msg .= " fill=none";
195             }
196              
197 0 0         if ( my $sda = $style->{'stroke-dasharray'} ) {
198 0           my @sda;
199 0 0 0       if ( $sda && $sda ne "none" ) {
200 0           $sda =~ s/,/ /g;
201 0           @sda = split( ' ', $sda );
202             }
203 0           $msg .= " sda=@sda";
204 0           $xo->line_dash_pattern(@sda);
205             }
206              
207 0           $self->_dbg( "%s", $msg );
208 0           return $style;
209             }
210              
211             # Return a stroke/fill/paint sub depending on the fill stroke styles.
212 0     0     method _paintsub () {
  0            
  0            
213 0 0 0       if ( $style->{stroke}
    0 0        
      0        
      0        
214             && $style->{stroke} ne 'none'
215             && $style->{stroke} ne 'transparent'
216             # Hmm. Saw a note somewhere that it defaults to 0 but other notes
217             # say that it should be 1px...
218             && $style->{'stroke-width'}//1 != 0
219             ) {
220 0 0 0       if ( $style->{fill}
      0        
221             && $style->{fill} ne 'none'
222             && $style->{fill} ne 'transparent'
223             ) {
224             return sub {
225             $self->_dbg("xo paint (",
226 0     0     join(" ", $style->{stroke}, $style->{fill} ), ")");
227 0           $xo->paint;
228 0           };
229             }
230             else {
231             return sub {
232 0     0     $self->_dbg("xo stroke (", $style->{stroke}, ")");
233 0           $xo->stroke;
234 0           };
235             }
236             }
237             elsif ( $style->{fill}
238             && $style->{fill} ne 'none'
239             && $style->{fill} ne 'transparent'
240             ) {
241             return sub {
242 0     0     $self->_dbg("xo fill (", $style->{stroke}, ")");
243 0           $xo->fill;
244 0           };
245             }
246             else {
247 0     0     return sub {};
248             }
249             }
250              
251 0     0 0   method process () {
  0            
  0            
252             # Unless overridden in a subclass there's not much we can do.
253 0           state $warned = { desc => 1, title => 1, metadata => 1 };
254             warn("SVG: Skipping element \"$name\" (not implemented)\n")
255 0 0 0       unless $warned->{$name}++ || !$self->root->verbose;
256 0           $self->_dbg("skipping $name (not implemented)");
257             # $self->traverse;
258             }
259              
260 0     0 0   method get_children () {
  0            
  0            
261              
262             # Note: This is the only place where these objects are created.
263              
264 0           my @res;
265 0           for my $e ( @{$self->content} ) {
  0            
266 0 0         if ( $e->{type} eq 'e' ) {
    0          
267 0           my $pkg = "SVGPDF::" . ucfirst(lc $e->{name});
268 0 0         $pkg = "SVGPDF::Element" unless $pkg->can("process");
269             push( @res, $pkg->new
270             ( name => $e->{name},
271 0           atts => { map { lc($_) => $e->{attrib}->{$_} } keys %{$e->{attrib}} },
  0            
272             content => $e->{content},
273 0           root => $self->root,
274             ) );
275             }
276             elsif ( $e->{type} eq 't' ) {
277             push( @res, SVGPDF::TextElement->new
278             ( content => $e->{content},
279 0           ) );
280             }
281             else {
282             # Basically a 'cannot happen',
283 0           croak("Unhandled node type ", $e->{type});
284             }
285             }
286 0           return @res;
287             }
288              
289 0     0 0   method traverse () {
  0            
  0            
290 0           for my $c ( $self->get_children ) {
291 0 0         next if ref($c) eq "SVGPDF::TextElement";
292 0           $self->_dbg("+ start handling ", $c->name, " (", ref($c), ")");
293 0           $c->process;
294 0           $self->_dbg("- end handling ", $c->name);
295             }
296             }
297              
298 0     0 0   method u ( $a, %args ) {
  0            
  0            
  0            
  0            
299 0 0         confess("Undef in units") unless defined $a;
300              
301             # Pixels per point. Usually 96/72.
302 0           my $pxpt = $self->root->pxpi / $self->root->ptpi;
303              
304 0 0         return undef unless $a =~ /^([-+]?[\d.]+)(.*)$/;
305 0 0         return $1*$pxpt if $2 eq "pt";
306              
307             # default is px
308 0 0 0       return $1 if $2 eq "" || $2 eq "px";
309              
310             # 1 inch = pxpi px.
311 0 0         return $1/2.54 * $self->root->pxpi if $2 eq "cm";
312 0 0         return $1/25.4 * $self->root->pxpi if $2 eq "mm";
313 0 0         return $1 * $self->root->pxpi if $2 eq "in";
314              
315 0 0         if ( $2 eq '%' ) {
316 0   0       my $w = $args{width} || $self->root->xoforms->[-1]->{diag};
317 0           return $1/100 * $w * $pxpt;
318             }
319             # Font dependent.
320             # CSS defines em to be the font size.
321 0 0         if ( $2 eq "em" ) {
322             return $1 * ( $args{fontsize}
323 0   0       || $style->{'font-size'}
324             || $self->root->fontsize );
325             }
326             # CSS defines ex to be half the font size.
327 0 0         if ( $2 eq "ex" ) {
328             return $1 * 0.5 * ( $args{fontsize}
329 0   0       || $style->{'font-size'}
330             || $self->root->fontsize );
331             }
332              
333 0           confess("Unhandled units in \"$a\"");
334 0           return $a; # will hopefully crash somewhere...
335             }
336              
337 0     0 0   method getargs ( $a ) {
  0            
  0            
  0            
338 0 0         confess("Null attr?") unless defined $a;
339 0           $a =~ s/^\s+//;
340 0           $a =~ s/\s+$//;
341 0           map { $self->u($_) } split( /\s*[,\s]\s*/, $a );
  0            
342             }
343              
344             # Initial fiddling with entity attributes.
345 0     0 0   method get_params ( @desc ) {
  0            
  0            
  0            
346 0 0         my $atts = shift(@desc) if ref($desc[0]) eq 'HASH';
347 0           my @res;
348 0   0       my %atts = %{ $atts // $self->atts }; # copy
  0            
349              
350             # xlink:href is obsoleted in favour of href.
351 0 0 0       $atts{href} //= delete $atts{"xlink:href"} if exists $atts{"xlink:href"};
352              
353 0           my @todo;
354 0           for my $param ( @desc ) {
355              
356             # Attribute may be followed by ':' and flags.
357             # 0 undef -> 0
358             # h process units, % is viewBox height
359             # s undef -> ""
360             # u process units
361             # v process units, % is viewBox width
362             # U undef -> 0, process units
363             # ! barf if undef
364 0           my $flags = "";
365 0 0         ( $param, $flags ) = ( $1, $2 )
366             if $param =~ /^(.*):(.*)$/;
367 0           $param = lc($param);
368              
369             # Get and remove the attribute.
370 0           my $p = delete( $atts{$param} );
371              
372             # Queue.
373 0           push( @todo, [ $param, $flags, $p ] );
374             }
375              
376             # CSS push with updated attributes.
377 0           $self->css_push( \%atts );
378              
379             # Now we can process the values.
380 0           for ( @todo ) {
381 0           my ( $param, $flags, $p ) = @$_;
382              
383 0 0         unless ( defined $p ) {
384 0 0         if ( $flags =~ /s/ ) { $p = ""; }
  0 0          
385 0           elsif ( $flags =~ /[0HUV]/ ) { $p = 0; }
386             else {
387 0 0         croak("Undefined mandatory attribute: $param")
388             if $flags =~ /\!/;
389 0           push( @res, $p );
390 0           next;
391             }
392             }
393              
394 0           $flags = lc($flags);
395             # Convert units if 'u' flag.
396 0 0         if ( $flags =~ /([huv])/ ) {
397 0           my $flag = $1;
398 0 0         if ( $p =~ /^([\d.]+)\%$/ ) {
399 0           $p = $1/100;
400 0 0 0       if ( $flags eq "w" || $param =~ /^(?:w(?:idth)|x)?$/i ) {
    0 0        
401             # Percentage of viewBox width.
402 0           $p *= $root->xoforms->[-1]->{width};
403             }
404             elsif ( $flag eq "h" || $param =~ /^(?:h(?:eight)?|y)$/i ) {
405             # Percentage of viewBox height.
406 0           $p *= $root->xoforms->[-1]->{height};
407             }
408             else {
409             # Percentage of viewBox diagonal.
410 0           $p *= $root->xoforms->[-1]->{diag};
411             }
412             }
413             else {
414 0           $p = $self->u($p);
415             }
416             }
417              
418 0           push( @res, $p );
419             }
420              
421             # Return param values.
422 0           return @res;
423             }
424              
425 0     0 0   method get_cdata () {
  0            
  0            
426 0           my $res = "";
427 0           for ( $self->get_children ) {
428 0 0         $res .= "\n" . $_->content if ref($_) eq "SVGPDF::TextElement";
429             }
430 0           $res;
431             }
432              
433 0     0 0   method nfi ( $tag ) {
  0            
  0            
  0            
434 0           state $aw = {};
435             warn("SVG: $tag - not fully implemented, expect strange results.\n")
436 0 0 0       unless !$self->root->verbose || $aw->{$tag}++;
437             }
438              
439 0     0 0   method set_font ( $xo, $style ) {
  0            
  0            
  0            
  0            
440 0           my $msg ="";
441 0           my $ret;
442             {
443 0     0     local $SIG{__WARN__} = sub { $msg .= "@_" };
  0            
  0            
444 0           $ret = $self->root->fontmanager->set_font( $xo, $style );
445             }
446 0 0 0       if ( $msg && $self->root->verbose ) {
447 0           warn($msg);
448             }
449 0           $ret;
450             }
451              
452             ################ Bounding Box ################
453              
454             # method bb ( $x, $y, $t = 0 ) {
455             # my $bb = $self->root->xoforms->[-1]->{bb};
456             #
457             # $t = $self->u($t) unless $t =~ /^[-+]?\d*(?:\.\d*)$/;
458             # $t /= 2;
459             # $bb->[0] = $x-$t if $bb->[0] > $x-$t;
460             # $bb->[1] = $y-$t if $bb->[1] > $y-$t;
461             # $bb->[2] = $x+$t if $bb->[2] < $x+$t;
462             # $bb->[3] = $y+$t if $bb->[3] < $y+$t;
463             #
464             # return $bb;
465             # }
466             #
467              
468             ################ TextElement ################
469              
470             class SVGPDF::TextElement;
471              
472 0 0   0     field $content :param :accessor;
  0            
473              
474             # Actually, we should take style->{white-space} into account...
475             BUILD {
476             # Reduce whitespace.
477             $content =~ s/\s+/ /g;
478             }
479              
480 0     0     method process () {
  0            
481             # Nothing to process.
482             }
483              
484             1;