File Coverage

lib/ChordPro/lib/SVGPDF/FontManager.pm
Criterion Covered Total %
statement 23 128 17.9
branch 0 88 0.0
condition 0 52 0.0
subroutine 8 14 57.1
pod 0 6 0.0
total 31 288 10.7


line stmt bran cond sub pod time code
1             #! perl
2              
3 1     1   13 use v5.26;
  1         4  
4 1     1   7 use Object::Pad;
  1         2  
  1         7  
5 1     1   100 use utf8;
  1         2  
  1         8  
6              
7             class SVGPDF::FontManager;
8              
9 1     1   232 use Carp;
  1         17  
  1         76  
10 1     1   7 use List::Util qw( any uniq );
  1         8  
  1         81  
11 1     1   7 use Text::ParseWords qw( quotewords );
  1         2  
  1         899  
12              
13 0     0 0   field $svg :mutator :param;
  0            
14 0     0 0   field $fc :mutator;
  0            
15 0 0   0 0   field $td :accessor;
  0            
16              
17             # Set a font according to the style.
18             #
19             # Strategy: First see if there was a @font-face defined. If so, use it.
20             # Then dispatch to user callback, if specified.
21             # Otherwise, try builtin fonts.
22              
23 0     0 0   method set_font ( $xo, $style ) {
  0            
  0            
  0            
  0            
24 0           my ( $font, $size, $src ) = $self->find_font($style);
25 0           $xo->font( $font, $size );
26             }
27              
28 0     0 0   method find_font ( $style ) {
  0            
  0            
  0            
29              
30 0   0       my $stl = lc( $style->{'font-style'} // "normal" );
31 0   0       my $weight = lc( $style->{'font-weight'} // "normal" );
32             ####TODO: normalize styles and weights.
33              
34             # Process the font-families, if any.
35 0   0       for ( ffsplit( $style->{'font-family'} // [] ) ) {
36 0           my $family = lc($_);
37              
38             # Check against @font-faces, if any.
39 0   0       for ( @{ $style->{'@font-face'}//[] } ) {
  0            
40 0 0         next unless $_->{'font-family'};
41 0 0         next unless $_->{src};
42             next if $_->{'font-style'} && $style->{'font-style'}
43 0 0 0       && $_->{'font-style'} ne $style->{'font-style'};
      0        
44             next if $_->{'font-weight'} && $style->{'font-weight'}
45 0 0 0       && $_->{'font-weight'} ne $style->{'font-weight'};
      0        
46              
47             # Matching style and weight, check against families.
48 0           for my $fam ( ffsplit($_->{'font-family'}) ) {
49 0           $fam = lc($fam);
50 0 0         next unless $family eq $fam;
51              
52             # Yeah! We have a match!
53 0           my $key = join( "|", $fam, $stl, $weight );
54             # Font in cache?
55 0 0         if ( my $f = $fc->{$key} ) {
56             return ( $f->{font},
57             $style->{'font-size'} || 12,
58 0   0       $f->{src} );
59             }
60              
61             # Fetch font from source.
62 0           my $src = $_->{src};
63             ####TODO: Multiple sources
64 0 0         if ( $src =~ /^\s*url\s*\((["'])data:application\/octet-stream;base64,(.*?)\1\s*\)/is ) {
    0          
65              
66 0           my $data = $2;
67              
68             # To load font data from net and data urls.
69 1     1   9 use File::Temp qw( tempfile tempdir );
  1         2  
  1         120  
70 1     1   668 use MIME::Base64 qw( decode_base64 );
  1         834  
  1         2313  
71 0   0       $td //= tempdir( CLEANUP => 1 );
72              
73 0           my $sfx; # suffix for font file name
74 0 0         if ( $src =~ /\bformat\((["'])(.*?)\1\)/ ) {
75 0 0         $sfx =
    0          
76             lc($2) eq "truetype" ? ".ttf" :
77             lc($2) eq "opentype" ? ".otf" :
78             '';
79             }
80             # No (or unknown) format, skip.
81 0 0         next unless $sfx;
82              
83 0           my ($fh,$fn) = tempfile( "${td}SVGXXXX", SUFFIX => $sfx );
84 0           binmode( $fh => ':raw' );
85 0           print $fh decode_base64($data);
86 0           close($fh);
87 0           my $font = eval { $svg->pdf->font($fn) };
  0            
88 0 0         croak($@) if $@;
89 0           my $f = $fc->{$key} =
90             { font => $font,
91             src => 'data' };
92             return ( $f->{font},
93             $style->{'font-size'} || 12,
94 0   0       $f->{src} );
95             }
96             elsif ( $src =~ /^\s*url\s*\((["'])(.*?\.[ot]tf)\1\s*\)/is ) {
97 0           my $fn = $2;
98 0           my $font = eval { $svg->pdf->font($fn) };
  0            
99 0 0         croak($@) if $@;
100 0           my $f = $fc->{$key} =
101             { font => $font,
102             src => $fn };
103             return ( $f->{font},
104             $style->{'font-size'} || 12,
105 0   0       $f->{src} );
106             }
107             else {
108 0           croak("\@font-face: Unhandled src \"", substr($src,0,50), "...\"");
109             }
110             }
111             }
112             }
113              
114 0           my $key = join( "|", $style->{'font-family'}, $stl, $weight );
115             # Font in cache?
116 0 0         if ( my $f = $fc->{$key} ) {
117             return ( $f->{font},
118             $style->{'font-size'} || 12,
119 0   0       $f->{src} );
120             }
121              
122 0 0         if ( my $cb = $svg->fc ) {
123 0           my $font;
124 0 0         unless ( ref($cb) eq 'ARRAY' ) {
125 0           $cb = [ $cb ];
126             }
127             # Run callbacks.
128 0           for ( @$cb ) {
129 0           eval { $font = $_->( $svg, $svg->pdf, $style ) };
  0            
130 0 0         croak($@) if $@;
131 0 0         last if $font;
132             }
133              
134 0 0         if ( $font ) {
135 0           my $src = "Callback($key)";
136 0           $fc->{$key} = { font => $font, src => $src };
137             return ( $font,
138 0   0       $style->{'font-size'} || 12,
139             $src );
140             }
141             }
142              
143             # No @font-face, no (or failed) callbacks, we're on our own.
144              
145 0   0       my $fn = $style->{'font-family'} // "Times-Roman";
146 0   0       my $sz = $style->{'font-size'} || 12;
147             my $em = $style->{'font-style'}
148 0   0       && $style->{'font-style'} =~ /^(italic|oblique)$/ || '';
149             my $bd = $style->{'font-weight'}
150 0   0       && $style->{'font-weight'} =~ /^(bold|black)$/ || '';
151              
152 0           for ( ffsplit($fn) ) {
153 0           $fn = lc($_);
154              
155 0 0 0       if ( $fn =~ /^(sans|helvetica|(?:text,)?sans-serif)$/ ) {
    0 0        
    0          
    0          
    0          
156 0 0         $fn = $bd
    0          
    0          
157             ? $em ? "Helvetica-BoldOblique" : "Helvetica-Bold"
158             : $em ? "Helvetica-Oblique" : "Helvetica";
159             }
160             elsif ( $fn eq "text" || $fn =~ /^mono(?:-?space)?$/ ) {
161 0 0         $fn = $bd
    0          
    0          
162             ? $em ? "Courier-BoldOblique" : "Courier-Bold"
163             : $em ? "Courier-Oblique" : "Courier";
164             }
165             elsif ( $fn =~ /^abc2svg(?:\.ttf)?/ or $fn eq "music" ) {
166 0           $fn = "abc2svg.ttf";
167             }
168             elsif ( $fn =~ /^musejazz\s*text$/ ) {
169 0           $fn = "MuseJazzText.otf";
170             }
171             elsif ( $fn =~ /^(serif|times|(?:text,)?serif)$/ ) {
172 0 0         $fn = $bd
    0          
    0          
173             ? $em ? "Times-BoldItalic" : "Times-Bold"
174             : $em ? "Times-Italic" : "Times-Roman";
175             }
176             else {
177 0           undef $fn;
178             }
179 0 0         last if $fn;
180             # Retry other families, if any.
181             }
182              
183 0 0         unless ( $fn ) {
184             # Nothing found...
185 0 0         $fn = $bd
    0          
    0          
186             ? $em ? "Times-BoldItalic" : "Times-Bold"
187             : $em ? "Times-Italic" : "Times-Roman";
188             }
189              
190 0   0       my $font = $fc->{$fn} //= do {
191 0 0         unless ( $fn =~ /\.\w+$/ ) {
192 0           my $t = "";
193 0 0         $t .= "italic, " if $em;
194 0 0         $t .= "bold, " if $bd;
195 0 0         $t = " (" . substr($t, 0, length($t)-2) . ")" if $t;
196 0   0       warn("SVG: Font ", $style->{'font-family'}//"",
197             "$t - falling back to built-in font $fn with limited glyphs!\n")
198             }
199 0           { font => $svg->pdf->font($fn), src => $fn };
200             };
201 0           return ( $font->{font}, $sz, $font->{src} );
202             }
203              
204 0     0 0   sub ffsplit ( $family ) {
  0            
  0            
205             # I hope this traps most (ab)uses of quotes and commas.
206 0           $family =~ s/^\s+//;
207 0           $family =~ s/\s+$//;
208 0           map { s/,+$//r } uniq quotewords( qr/(\s+|\s*,\s*)/, 0, $family);
  0            
209             }
210              
211              
212             1;