File Coverage

blib/lib/PDF/Builder/Resource/Font/SynFont.pm
Criterion Covered Total %
statement 21 196 10.7
branch 0 94 0.0
condition 0 109 0.0
subroutine 7 8 87.5
pod 1 1 100.0
total 29 408 7.1


line stmt bran cond sub pod time code
1             package PDF::Builder::Resource::Font::SynFont;
2              
3 1     1   1094 use base 'PDF::Builder::Resource::Font';
  1         2  
  1         100  
4              
5 1     1   5 use strict;
  1         2  
  1         17  
6 1     1   2 use warnings;
  1         2  
  1         55  
7              
8             our $VERSION = '3.028'; # VERSION
9             our $LAST_UPDATE = '3.028'; # manually update whenever code is changed
10              
11 1     1   4 use Math::Trig; # CAUTION: deg2rad(0) = deg2rad(360) = 0!
  1         2  
  1         232  
12 1     1   5 use Unicode::UCD 'charinfo';
  1         1  
  1         65  
13              
14 1     1   3 use PDF::Builder::Util;
  1         1  
  1         137  
15 1     1   5 use PDF::Builder::Basic::PDF::Utils;
  1         2  
  1         2248  
16              
17             # for noncompatible options, consider '-entry_point' = 'synfont' or
18             # 'synthetic_font' to be picked up and processed correctly per entry point
19            
20             =head1 NAME
21              
22             PDF::Builder::Resource::Font::SynFont - Module for creating temporary synthetic Fonts
23              
24             Inherits from L<PDF::Builder::Resource::Font>
25              
26             =head1 SYNOPSIS
27              
28             This module permits you to create a "new" font (loaded temporarily, but not
29             permanently stored) based on an existing font, where you can modify certain
30             attributes in the original font, such as:
31              
32             * slant/obliqueness
33             * extra weight/boldness (by drawing glyph outlines at various line
34             thicknesses, rather than just filling enclosed areas)
35             * condense/expand (narrower or wider characters) a.k.a. B<stretch>
36             * extra space between characters
37             * small caps (synthesized, not using any provided with a font)
38             * change the encoding
39              
40             $pdf = PDF::Builder->new();
41             $cft = $pdf->font('Times-Roman'); # corefont, ttfont, etc. also works
42             $sft = $pdf->synfont($cft, 'condense' => .75); # condense by 25%
43              
44             This works for I<corefonts>, I<PS fonts>, and I<TTF/OTF fonts>; but does not
45             work for I<CJK fonts> or I<bitmapped fonts>.
46             See also L<PDF::Builder::Docs/Synthetic Fonts>.
47              
48             B<Alternate name:> C<synthetic_font>
49              
50             This is for compatibility with recent changes to PDF::API2.
51              
52             =head1 METHODS
53              
54             =head2 new
55              
56             $font = PDF::Builder::Resource::Font::SynFont->new($pdf, $fontobj, %opts)
57              
58             =over
59              
60             Returns a synfont object. C<$fontobj> is a normal font object read in from
61             a file, and C<$font> is the modified output.
62              
63             Valid options %opts are:
64              
65             =over
66              
67             =item I<encode>
68              
69             Changes the encoding of the font from its default.
70             See I<Perl's Encode> for the supported values. B<Warning:> only single byte
71             encodings are supported. Multibyte encodings such as UTF-8 are invalid.
72              
73             =item I<pdfname>
74              
75             Changes the reference-name of the font from its default.
76             The reference-name is normally generated automatically and can be
77             retrieved via $pdfname=$font->name().
78              
79             B<Alternate name:> C<name> (for PDF::API2 compatibility)
80              
81             =item I<condense>
82              
83             Character width condense/expand factor (0.1-0.9 = condense, 1 = normal/default,
84             1.1+ = expand). It is the multiplier to apply to the width of each character.
85              
86             In some font systems, this aspect or axis is known as I<stretch>.
87              
88             B<Alternate names:> C<hscale> and C<slant> (for PDF::API2 compatibility)
89              
90             The I<slant> option is a deprecated name in both PDF::Builder and PDF::API2.
91             Its value is the same as I<condense> value (1 = normal, unchanged scale).
92             For the I<hscale> option, the value is percentage (%), with 100 being normal,
93             and other values 100 times the I<condense> value.
94             B<Use only one (at most) of these three option names.>
95              
96             =item I<oblique>
97              
98             Italic or slanted text angle (+/-) in degrees, where the character box is
99             skewed (sheared), top to the right. While it's unlikely that anyone will want
100             to slant characters at +/-360 degrees, they should be aware that these will be
101             treated as an angle of 0 degrees (deg2rad() wraps around). 0 degrees of italic
102             slant (obliqueness) is the default.
103              
104             Note that a font management system may have separate axes for normal/italic
105             fonts, and the degree of I<slant>. Italic is a different (but related) face
106             to the regular (e.g., Roman) face, sometimes made to resemble handwritten
107             characters, and is usually a binary selection (it's either italic or it's not).
108             Slant, on the other hand, can be in arbitrary amounts, and may be applied to either
109             normal (originally Roman posture) or italics (which are often themselves
110             slightly slanted). Finally, what many fonts call "italic" others may call
111             "slanted" or "oblique", and in these cases the coordinate system is merely
112             sheared to slant the character box.
113              
114             B<Alternate name:> C<angle> (for PDF::API2 compatibility)
115              
116             B<Use only one (at most) of these two option names.>
117              
118             =item I<bold>
119              
120             Embolding factor (0.1+, bold=1, heavy=2, ...). It is additional outline
121             B<thickness> (B<linewidth>), which expands the character (glyph) outwards (as
122             well as shrinking unfilled enclosed areas such as bowls and counters).
123             Normally, the glyph's outline is not drawn (it is only filled); this adds
124             a thick outline. The units are in 1/100ths of a text unit.
125              
126             If used with the C<synthetic_font> alternate entry name, the unit is 1/1000th
127             of a text unit, so you will need a value 10 times larger than with the
128             C<synfont> entry to get the same effect.
129              
130             =item I<space>
131              
132             Additional charspacing in thousandths of an em.
133              
134             =item I<caps>
135              
136             Create synthetic small-caps. 0 = no, 1 = yes. These are capitals of
137             lowercase letters, at 80% height and 88% width. Note that this is guaranteed
138             to cover ASCII lowercase letters only -- single byte encoded accented
139             characters I<usually> work, but we can make no promises on accented characters
140             in general, as well as ligatures!
141              
142             B<Alternate name:> C<smallcaps> (for PDF::API2 compatibility)
143              
144             B<Use only one (at most) of these two option names.>
145              
146             Note that only lower case letters which appear in the "standard" font (plane 0
147             for core fonts and PS fonts) will be small-capped. This may include eszett
148             (German sharp s), which becomes SS, and dotless i and j which become I and J
149             respectively. There are many other accented Latin alphabet letters which I<may>
150             show up in planes 1 and higher. Ligatures (e.g., ij and ffl) do not have
151             uppercase equivalents, nor does a long s. If you have text which includes such
152             characters, you may want to consider preprocessing it to replace them with
153             Latin character expansions (e.g., i+j and f+f+l) before small-capping.
154              
155             =back
156              
157             =back
158              
159             =cut
160              
161             # TBD 'name' as alt to 'pdfname'... other font types or just here?
162              
163             sub new {
164 0     0 1   my ($class, $pdf, $font, %opts) = @_;
165             # copy dashed named options to preferred undashed names
166 0 0 0       if (defined $opts{'-encode'} && !defined $opts{'encode'}) { $opts{'encode'} = delete($opts{'-encode'}); }
  0            
167 0 0 0       if (defined $opts{'-pdfname'} && !defined $opts{'pdfname'}) { $opts{'pdfname'} = delete($opts{'-pdfname'}); }
  0            
168 0 0 0       if (defined $opts{'-name'} && !defined $opts{'name'}) { $opts{'name'} = delete($opts{'-name'}); }
  0            
169 0 0 0       if (defined $opts{'-condense'} && !defined $opts{'condense'}) { $opts{'condense'} = delete($opts{'-condense'}); }
  0            
170 0 0 0       if (defined $opts{'-slant'} && !defined $opts{'slant'}) { $opts{'slant'} = delete($opts{'-slant'}); }
  0            
171 0 0 0       if (defined $opts{'-hscale'} && !defined $opts{'hscale'}) { $opts{'hscale'} = delete($opts{'-hscale'}); }
  0            
172 0 0 0       if (defined $opts{'-oblique'} && !defined $opts{'oblique'}) { $opts{'oblique'} = delete($opts{'-oblique'}); }
  0            
173 0 0 0       if (defined $opts{'-angle'} && !defined $opts{'angle'}) { $opts{'angle'} = delete($opts{'-angle'}); }
  0            
174 0 0 0       if (defined $opts{'-bold'} && !defined $opts{'bold'}) { $opts{'bold'} = delete($opts{'-bold'}); }
  0            
175 0 0 0       if (defined $opts{'-space'} && !defined $opts{'space'}) { $opts{'space'} = delete($opts{'-space'}); }
  0            
176 0 0 0       if (defined $opts{'-caps'} && !defined $opts{'caps'}) { $opts{'caps'} = delete($opts{'-caps'}); }
  0            
177 0 0 0       if (defined $opts{'-smallcaps'} && !defined $opts{'smallcaps'}) { $opts{'smallcaps'} = delete($opts{'-smallcaps'}); }
  0            
178              
179 0           my $entry = "synfont"; # synfont or synthetic_font
180 0 0         if (defined $opts{'-entry_point'}) { $entry = $opts{'-entry_point'}; }
  0            
181              
182             # deal with simple aliases
183 0 0 0       if (defined $opts{'slant'} && !defined $opts{'condense'}) { $opts{'condense'} = delete($opts{'slant'}); }
  0            
184 0 0 0       if (defined $opts{'angle'} && !defined $opts{'oblique'}) { $opts{'oblique'} = delete($opts{'angle'}); }
  0            
185 0 0 0       if (defined $opts{'smallcaps'} && !defined $opts{'caps'}) { $opts{'caps'} = delete($opts{'smallcaps'}); }
  0            
186 0 0 0       if (defined $opts{'name'} && !defined $opts{'pdfname'}) { $opts{'pdfname'} = delete($opts{'name'}); }
  0            
187             # deal with semi-aliases
188 0 0 0       if (defined $opts{'hscale'} && !defined $opts{'condense'}) { $opts{'condense'} = delete($opts{'hscale'})/100; }
  0            
189             # deal with entry point differences
190 0 0 0       if (defined $opts{'bold'} && $entry eq 'synthetic_font') { $opts{'bold'} /= 10; }
  0            
191              
192 0           my ($self);
193 0           my $first = 1;
194 0           my $last = 255;
195 0   0       my $cond = $opts{'condense'} || 1;
196 0   0       my $oblique = $opts{'oblique'} || 0;
197 0   0       my $space = $opts{'space'} || '0';
198 0   0       my $bold = ($opts{'bold'} || 0)*10; # convert to em
199             # caps
200              
201             # 5 elements apparently not used anywhere
202             #$self->{' cond'} = $cond;
203             #$self->{' oblique'} = $oblique;
204             #$self->{' bold'} = $bold;
205             #$self->{' boldmove'} = 0.001;
206             #$self->{' space'} = $space;
207             # only available in TT fonts. besides, multibyte encodings not supported
208 0 0         if (defined $opts{'encode'}) {
209 0 0         if ($opts{'encode'} =~ m/^utf/i) {
210 0           die "Invalid multibyte encoding for synfont: $opts{'encode'}\n";
211             # TBD probably more multibyte encodings to check
212             }
213 0           $font->encodeByName($opts{'encode'});
214             }
215              
216 0 0         $class = ref $class if ref $class;
217             $self = $class->SUPER::new($pdf,
218             # pdfkey()
219             # .('+' . $font->name())
220             # .($opts{'caps'} ? '+Caps' : '')
221             # .($opts{'pdfname'} ? '+'.$opts{'pdfname'} : '')
222 0 0         $opts{'pdfname'}? $opts{'pdfname'}: 'Syn' . $font->name() . pdfkey()
223             );
224 0 0         $pdf->new_obj($self) unless $self->is_obj($pdf);
225 0           $self->{' font'} = $font;
226 0   0       $self->{' data'} = {
227             'type' => 'Type3',
228             'ascender' => $font->ascender(),
229             'capheight' => $font->capheight(),
230             'descender' => $font->descender(),
231             'iscore' => '0',
232             'isfixedpitch' => $font->isfixedpitch(),
233             'italicangle' => $font->italicangle() + $oblique,
234             'missingwidth' => ($font->missingwidth()||300) * $cond,
235             'underlineposition' => $font->underlineposition(),
236             'underlinethickness' => $font->underlinethickness(),
237             'xheight' => $font->xheight(),
238             'firstchar' => $first,
239             'lastchar' => $last,
240             'char' => [ '.notdef' ],
241             'uni' => [ 0 ],
242             'u2e' => { 0 => 0 },
243             'fontbbox' => '',
244             'wx' => { 'space' => '600' },
245             };
246              
247 0           my $data = $self->data();
248 0 0         if (ref($font->fontbbox())) {
249 0           $data->{'fontbbox'} = [ @{$font->fontbbox()} ];
  0            
250             } else {
251 0           $data->{'fontbbox'} = [ $font->fontbbox() ];
252             }
253 0           $data->{'fontbbox'}->[0] *= $cond;
254 0           $data->{'fontbbox'}->[2] *= $cond;
255              
256 0           $self->{'Subtype'} = PDFName('Type3');
257 0           $self->{'FirstChar'} = PDFNum($first);
258 0           $self->{'LastChar'} = PDFNum($last);
259 0           $self->{'FontMatrix'} = PDFArray(map { PDFNum($_) } (0.001, 0, 0, 0.001, 0, 0));
  0            
260 0           $self->{'FontBBox'} = PDFArray(map { PDFNum($_) } $self->fontbbox());
  0            
261              
262 0           my $procs = PDFDict();
263 0           $pdf->new_obj($procs);
264 0           $self->{'CharProcs'} = $procs;
265              
266 0           $self->{'Resources'} = PDFDict();
267 0           $self->{'Resources'}->{'ProcSet'} = PDFArray(map { PDFName($_) }
  0            
268             qw(PDF Text ImageB ImageC ImageI));
269 0           my $xo = PDFDict();
270 0           $self->{'Resources'}->{'Font'} = $xo;
271 0           $self->{'Resources'}->{'Font'}->{'FSN'} = $font;
272 0           foreach my $w ($first .. $last) {
273 0           $data->{'char'}->[$w] = $font->glyphByEnc($w);
274             # possible non-standard name... use $w as Unicode value
275 0   0       $data->{'uni'}->[$w] = (uniByName($data->{'char'}->[$w])) || $w;
276 0 0         if (defined $data->{'uni'}->[$w]) {
277 0           $data->{'u2e'}->{$data->{'uni'}->[$w]} = $w;
278             }
279             }
280              
281 0 0         if ($font->isa('PDF::Builder::Resource::CIDFont')) {
282 0           $self->{'Encoding'} = PDFDict();
283 0           $self->{'Encoding'}->{'Type'} = PDFName('Encoding');
284 0           $self->{'Encoding'}->{'Differences'} = PDFArray();
285 0           foreach my $w ($first .. $last) {
286 0           my $char = $data->{'char'}->[$w];
287 0 0 0       if (defined $char && $char ne '.notdef') {
288 0           $self->{'Encoding'}->{'Differences'}->add_elements(PDFNum($w),
289             PDFName($char));
290             }
291             }
292             } else {
293 0           $self->{'Encoding'} = $font->{'Encoding'};
294             }
295              
296 0           my @widths;
297 0           foreach my $w ($first .. $last) {
298             # $w is the "standard encoding" (similar to Windows-1252) PDF
299             # single byte encoding. first 32 .notdef, 255 = U+00FF ydieresis
300 0 0         if ($data->{'char'}->[$w] eq '.notdef') {
301 0           push @widths, $self->missingwidth();
302 0           next;
303             }
304 0           my $char = PDFDict();
305              
306             #my $wth = int($font->width(chr($w)) * 1000 * $cond + 2 * $space);
307 0           my $uni = $data->{'uni'}->[$w];
308 0           my $wth = int($font->width(chr($uni)) * 1000 * $cond + 2*$space);
309              
310 0           $procs->{$font->glyphByEnc($w)} = $char;
311             #$char->{'Filter'} = PDFArray(PDFName('FlateDecode'));
312 0           $char->{' stream'} = $wth." 0 ".join(' ',map { int($_) } $self->fontbbox())." d1\n";
  0            
313 0           $char->{' stream'} .= "BT\n";
314 0 0         $char->{' stream'} .= join(' ', (1, 0, tan(deg2rad($oblique)), 1, 0, 0))." Tm\n" if $oblique;
315 0 0         $char->{' stream'} .= "2 Tr $bold w\n" if $bold;
316             #my $ci = charinfo($data->{'uni'}->[$w]);
317 0           my $ci = {};
318 0 0         if ($data->{'uni'}->[$w] ne '') {
319 0           $ci = charinfo($data->{'uni'}->[$w]);
320             }
321            
322             # Small Caps
323             #
324             # Most Unicode characters simply don't appear in the synthetic
325             # font, which is limited to 255 "standard" encoding points. encode
326             # still will be single byte.
327             #
328             # SynFont seems to have trouble with some accented characters, even
329             # though 'upper' is correct and they are in the standard encoding,
330             # particularly if the string is decoded to UTF-8. Keep in mind that
331             # synfont() only creates a 255 character "standard" encoding font, so
332             # you need to apply it to each "plane" of the original font.
333             #
334             # Some single characters (eszett within the standard encoding, long s
335             # outside it) don't have 'upper' defined and are left as-is (or
336             # skipped entirely, if outside the encoding) unless first replaced by
337             # ASCII lowercase ('ss' and 's' respectively). While we're at it,
338             # replace certain Unicode ligatures with ASCII equivalents so they
339             # will be small-capped correctly instead of ignored. Don't forget to
340             # set proper width for multi-letter replacements.
341             #
342 0           my $hasUpper = 0; # if no small caps, still need to output something
343 0 0         if ($opts{'caps'}) {
344             # not all characters have an 'upper' equivalent code point. Some
345             # have U+0000 (dummy entry).
346 0           my $ch;
347 0           my $multiChar = 0;
348 0 0 0       $hasUpper = 1 if defined $ci->{'upper'} && $ci->{'upper'};
349            
350 0 0         if ($hasUpper) {
351             # standard upper case character and width spec'd by font
352 0           $ch = $self->encByUni(hex($ci->{'upper'}));
353 0           $wth = int($font->width(chr($ch)) * 800 * $cond * 1.1 + 2* $space);
354             }
355             # let's handle some special cases where !$hasUpper
356             # ($hasUpper set to 1)
357             # only characters to be substituted here, unless there is something
358             # in other encodings to deal with
359             # TBD it does not seem to be possible on non-base planes (plane 1+)
360             # to access ASCII letters to build a substitute for ligatures
361             # (e.g., replace U+FB01 fi ligature with F+I)
362 0 0         if ($uni == 0xDF) { # eszett (German sharp s)
    0          
    0          
363 0           $hasUpper = 1;
364 0           $multiChar = 1;
365             # actually, some fonts have a U+1E9E uppercase Eszett, but
366             # since that won't be in any single byte encoding, we use SS
367 0           $wth = 2*(int($font->width('S') * 800 * $cond*1.1 + 2*$space));
368 0           $ch = $font->text('S').$font->text('S');
369             } elsif ($uni == 0x0131) { # dotless i
370             # standard encoding doesn't see Unicode point
371 0           $hasUpper = 1;
372 0           $multiChar = 1;
373 0           $wth = int($font->width('I') * 800 * $cond*1.1 + 2*$space);
374 0           $ch = $font->text('I');
375             } elsif ($uni == 0x0237) { # dotless j
376             # standard encoding doesn't see Unicode point
377 0           $hasUpper = 1;
378 0           $multiChar = 1;
379 0           $wth = int($font->width('J') * 800 * $cond*1.1 + 2*$space);
380 0           $ch = $font->text('J');
381             }
382              
383 0 0         if ($hasUpper) {
384             # this is a lowercase letter, etc. that has an uppercase version
385             # 80% height x 88% (110% aspect ratio @ 80% font size) width.
386             # slightly wider to thicken stems and make look better.
387             # $ch and $wth already set, either default or special case
388 0           $char->{' stream'} .= "/FSN 800 Tf\n";
389 0           $char->{' stream'} .= ($cond * 110)." Tz\n";
390 0 0         $char->{' stream'} .= " [ -$space ] TJ\n" if $space;
391 0 0         if ($multiChar) {
392 0           $ch =~ s/><//g;
393 0           $ch =~ s/\)\(//g;
394 0           $char->{' stream'} .= "$ch";
395             } else {
396 0           $char->{' stream'} .= $font->text(chr($ch));
397             }
398             # uc chr($uni) supposed to be always equivalent to
399             # chr hex($ci->{'upper'}), according to "futuramedium"
400             # HOWEVER, uc doesn't seem to know what to do with non-ASCII chars
401             #$wth = int($font->width(uc chr($uni)) * 800 * $cond * 1.1 + 2* $space);
402             #$char->{' stream'} .= $font->text(uc chr($uni));
403             #$wth = int($font->width(chr(hex($ci->{'upper'}))) * 800 * $cond * 1.1 + 2* $space);
404             #$char->{' stream'} .= $font->text(chr(hex($ci->{'upper'})));
405             } # else fall through to standard handling below
406             } # small caps requested
407              
408 0 0         if (!$hasUpper) {
409             # Applies to all not small-caps too!
410             # does not have an uppercase ('upper') equivalent, so
411             # output at standard height and aspect ratio
412 0           $char->{' stream'} .= "/FSN 1000 Tf\n";
413 0 0         $char->{' stream'} .= ($cond * 100)." Tz\n" if $cond != 1;
414 0 0         $char->{' stream'} .= " [ -$space ] TJ\n" if $space;
415             #$char->{' stream'} .= $font->text(chr($w));
416 0           $char->{' stream'} .= $font->text(chr($uni));
417             }
418              
419             # finale... all modifications to font have been done
420 0           $char->{' stream'} .= " Tj\nET ";
421 0           push @widths, $wth;
422 0           $data->{'wx'}->{$font->glyphByEnc($w)} = $wth;
423 0           $pdf->new_obj($char);
424             } # loop through 255 standard encoding points
425              
426             # the array as 0 elements at this point! 'space' (among others) IS defined,
427             # so copy that, but TBD what kind of fallback if no such element exists?
428             # $procs->{'.notdef'} = $procs->{$font->data()->{'char'}->[32]};
429 0           $procs->{'.notdef'} = $procs->{'space'};
430              
431 0           $self->{'Widths'} = PDFArray(map { PDFNum($_) } @widths);
  0            
432 0           $data->{'e2n'} = $data->{'char'};
433 0           $data->{'e2u'} = $data->{'uni'};
434              
435 0           $data->{'u2c'} = {};
436 0           $data->{'u2e'} = {};
437 0           $data->{'u2n'} = {};
438 0           $data->{'n2c'} = {};
439 0           $data->{'n2e'} = {};
440 0           $data->{'n2u'} = {};
441              
442 0           foreach my $n (reverse 0 .. 255) {
443 0   0       $data->{'n2c'}->{$data->{'char'}->[$n] // '.notdef'} //= $n;
      0        
444 0   0       $data->{'n2e'}->{$data->{'e2n'}->[$n] // '.notdef'} //= $n;
      0        
445              
446 0   0       $data->{'n2u'}->{$data->{'e2n'}->[$n] // '.notdef'} //= $data->{'e2u'}->[$n];
      0        
447 0   0       $data->{'n2u'}->{$data->{'char'}->[$n] // '.notdef'} //= $data->{'uni'}->[$n];
      0        
448              
449 0 0         if (defined $data->{'uni'}->[$n]) {
450 0   0       $data->{'u2c'}->{$data->{'uni'}->[$n]} //= $n
451             }
452 0 0         if (defined $data->{'e2u'}->[$n]) {
453 0   0       $data->{'u2e'}->{$data->{'e2u'}->[$n]} //= $n;
454 0   0       my $value = $data->{'e2n'}->[$n] // '.notdef';
455 0   0       $data->{'u2n'}->{$data->{'e2u'}->[$n]} //= $value;
456             }
457 0 0         if (defined $data->{'uni'}->[$n]) {
458 0   0       my $value = $data->{'char'}->[$n] // '.notdef';
459 0   0       $data->{'u2n'}->{$data->{'uni'}->[$n]} //= $value;
460             }
461             }
462              
463 0           return $self;
464             }
465              
466             1;