File Coverage

blib/lib/SVGPDF/FontManager.pm
Criterion Covered Total %
statement 73 153 47.7
branch 7 98 7.1
condition 8 54 14.8
subroutine 14 18 77.7
pod 0 6 0.0
total 102 329 31.0


line stmt bran cond sub pod time code
1             #! perl
2              
3 2     2   37 use v5.26;
  2         7  
4 2     2   10 use Object::Pad;
  2         3  
  2         15  
5 2     2   299 use utf8;
  2         4  
  2         14  
6              
7             class SVGPDF::FontManager;
8              
9 2     2   603 use Carp;
  2         4  
  2         169  
10 2     2   15 use List::Util qw( any none uniq );
  2         4  
  2         186  
11 2     2   11 use Text::ParseWords qw( quotewords );
  2         21  
  2         3150  
12              
13 0     0 0 0 field $svg :mutator :param;
  0         0  
14 0     0 0 0 field $fc :mutator;
  0         0  
15 0 0   0 0 0 field $td :accessor;
  0         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 33     33 0 65 method set_font ( $xo, $style ) {
  33         116  
  33         51  
  33         43  
  33         44  
32 33         94 my ( $font, $size, $src ) = $self->find_font($style);
33 33         217 $xo->font( $font, $size );
34             }
35              
36 33     33 0 49 method find_font ( $style ) {
  33         66  
  33         46  
  33         43  
37              
38 33   100     223 my $stl = lc( $style->{'font-style'} // "normal" );
39 33   100     142 my $weight = lc( $style->{'font-weight'} // "normal" );
40             ####TODO: normalize styles and weights.
41              
42             # Process the font-families, if any.
43 33   50     134 for ( ffsplit( $style->{'font-family'} // [] ) ) {
44 33         84 my $family = lc($_);
45              
46             # Check against @font-faces, if any.
47 33   50     52 for ( @{ $style->{'@font-face'}//[] } ) {
  33         172  
48 0         0 my $fam = $_->{'font-family'};
49 0 0 0     0 next unless $fam && lc($fam) eq $family;
50 0 0       0 next unless $_->{src};
51             next if $_->{'font-style'} && $style->{'font-style'}
52 0 0 0     0 && $_->{'font-style'} ne $style->{'font-style'};
      0        
53             next if $_->{'font-weight'} && $style->{'font-weight'}
54 0 0 0     0 && $_->{'font-weight'} ne $style->{'font-weight'};
      0        
55              
56 0         0 $fam = lc($fam);
57 0         0 my $key = join( "|", $fam, $stl, $weight );
58             # Font in cache?
59 0 0       0 if ( my $f = $fc->{$key} ) {
60             return ( $f->{font},
61             $style->{'font-size'} || 12,
62 0   0     0 $f->{src} );
63             }
64              
65             # Fetch font from source.
66 0         0 my $src = $_->{src};
67             ####TODO: Multiple sources
68 0 0       0 if ( $src =~ /^\s*url\s*\((["'])((?:data|https?|file):.*?)\1\s*\)/is ) {
    0          
    0          
69 2     2   17 use File::LoadLines 1.044;
  2         44  
  2         333  
70 0         0 my $opts = {};
71 0         0 my $data = File::LoadLines::loadblob( $2, $opts );
72              
73             # To load font data from net and data urls.
74 2     2   17 use File::Temp qw( tempfile tempdir );
  2         4  
  2         231  
75 2     2   1555 use MIME::Base64 qw( decode_base64 );
  2         2054  
  2         1604  
76 0   0     0 $td //= tempdir( CLEANUP => 1 );
77              
78 0         0 my $sfx; # suffix for font file name
79 0 0       0 if ( $src =~ /\bformat\((["'])(.*?)\1\)/ ) {
    0          
80 0 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         0 $sfx = $2;
87             }
88             # No (or unknown) format, skip.
89 0 0       0 next unless $sfx;
90              
91 0         0 my ($fh,$fn) = tempfile( "${td}SVGXXXX", SUFFIX => $sfx );
92 0         0 binmode( $fh => ':raw' );
93 0         0 print $fh $data;
94 0         0 close($fh);
95 0         0 my $font = eval { $svg->pdf->font($fn) };
  0         0  
96 0 0       0 croak($@) if $@;
97             my $f = $fc->{$key} =
98             { font => $font,
99 0         0 src => $opts->{_filesource} };
100             #warn("SRC: ", $opts->{_filesource}, "\n");
101             return ( $f->{font},
102             $style->{'font-size'} || 12,
103 0   0     0 $f->{src} );
104             }
105             # Temp.
106             elsif ( $src =~ /^\s*url\s*\((["'])data:application\/octet-stream;base64,(.*?)\1\s*\)/is ) {
107              
108 0         0 my $data = $2;
109              
110             # To load font data from net and data urls.
111 2     2   18 use File::Temp qw( tempfile tempdir );
  2         3  
  2         131  
112 2     2   12 use MIME::Base64 qw( decode_base64 );
  2         4  
  2         7449  
113 0   0     0 $td //= tempdir( CLEANUP => 1 );
114              
115 0         0 my $sfx; # suffix for font file name
116 0 0       0 if ( $src =~ /\bformat\((["'])(.*?)\1\)/ ) {
117 0 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       0 next unless $sfx;
124              
125 0         0 my ($fh,$fn) = tempfile( "${td}SVGXXXX", SUFFIX => $sfx );
126 0         0 binmode( $fh => ':raw' );
127 0         0 print $fh decode_base64($data);
128 0         0 close($fh);
129 0         0 my $font = eval { $svg->pdf->font($fn) };
  0         0  
130 0 0       0 croak($@) if $@;
131 0         0 my $f = $fc->{$key} =
132             { font => $font,
133             src => 'data' };
134             return ( $f->{font},
135             $style->{'font-size'} || 12,
136 0   0     0 $f->{src} );
137             }
138             elsif ( $src =~ /^\s*url\s*\((["'])(.*?\.[ot]tf)\1\s*\)/is ) {
139 0         0 my $fn = $2;
140 0         0 my $font = eval { $svg->pdf->font($fn) };
  0         0  
141 0 0       0 croak($@) if $@;
142 0         0 my $f = $fc->{$key} =
143             { font => $font,
144             src => $fn };
145             return ( $f->{font},
146             $style->{'font-size'} || 12,
147 0   0     0 $f->{src} );
148             }
149             else {
150 0         0 croak("\@font-face: Unhandled src \"", substr($src,0,50), "...\"");
151             }
152             }
153             }
154              
155 33         157 my $key = join( "|", $style->{'font-family'}, $stl, $weight );
156             # Font in cache?
157 33 100       121 if ( my $f = $fc->{$key} ) {
158             return ( $f->{font},
159             $style->{'font-size'} || 12,
160 26   50     158 $f->{src} );
161             }
162              
163 7 50       35 if ( my $cb = $svg->fc ) {
164 7         14 my $font;
165 7 50       30 unless ( ref($cb) eq 'ARRAY' ) {
166 7         18 $cb = [ $cb ];
167             }
168             # Run callbacks.
169 7         28 my %args = ( pdf => $svg->pdf, style => $style );
170 7         19 for ( @$cb ) {
171 7         18 eval { $font = $_->( $svg, %args ) };
  7         35  
172 7 50       56633 croak($@) if $@;
173 7 50       32 last if $font;
174             }
175              
176 7 50       19 if ( $font ) {
177 7         21 my $src = "Callback($key)";
178 7         42 $fc->{$key} = { font => $font, src => $src };
179             return ( $font,
180 7   50     58 $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     0 my $fn = $style->{'font-family'} // "Times-Roman";
188 0   0     0 my $sz = $style->{'font-size'} || 12;
189             my $em = $style->{'font-style'}
190 0   0     0 && $style->{'font-style'} =~ /^(italic|oblique)$/ || '';
191             my $bd = $style->{'font-weight'}
192 0   0     0 && $style->{'font-weight'} =~ /^(bold|black)$/ || '';
193              
194 0         0 for ( ffsplit($fn) ) {
195 0         0 $fn = lc($_);
196              
197             # helvetica sans sans-serif text,sans-serif
198 0 0       0 if ( $fn =~ /^(sans|helvetica|(?:text,)?sans-serif)$/ ) {
    0          
    0          
    0          
199 0 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       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       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   0 elsif ( none { $fn eq lc($_) } @corefonts ) {
217 0         0 undef $fn;
218             }
219 0 0       0 last if $fn;
220             # Retry other families, if any.
221             }
222              
223 0 0       0 unless ( $fn ) {
224             # Nothing found...
225 0 0       0 $fn = $bd
    0          
    0          
226             ? $em ? "Times-BoldItalic" : "Times-Bold"
227             : $em ? "Times-Italic" : "Times-Roman";
228             }
229              
230 0   0     0 my $font = $fc->{$fn} //= do {
231 0 0       0 unless ( $fn =~ /\.\w+$/ ) {
232 0         0 my $t = "";
233 0 0       0 $t .= "italic, " if $em;
234 0 0       0 $t .= "bold, " if $bd;
235 0 0       0 $t = " (" . substr($t, 0, length($t)-2) . ")" if $t;
236 0   0     0 warn("SVG: Font ", $style->{'font-family'}//"",
237             "$t - falling back to built-in font $fn with limited glyphs!\n")
238             }
239 0         0 { font => $svg->pdf->font($fn), src => $fn };
240             };
241 0         0 return ( $font->{font}, $sz, $font->{src} );
242             }
243              
244 33     33 0 48 sub ffsplit ( $family ) {
  33         58  
  33         54  
245             # I hope this traps most (ab)uses of quotes and commas.
246 33         118 $family =~ s/^\s+//;
247 33         122 $family =~ s/\s+$//;
248 33         250 map { s/,+$//r } uniq quotewords( qr/(\s+|\s*,\s*)/, 0, $family);
  33         3161  
249             }
250              
251              
252             1;