File Coverage

lib/ChordPro/lib/SVGPDF/FontManager.pm
Criterion Covered Total %
statement 32 153 20.9
branch 0 98 0.0
condition 0 54 0.0
subroutine 11 18 61.1
pod 0 6 0.0
total 43 329 13.0


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