File Coverage

blib/lib/PDF/Builder/Resource/Font/Postscript.pm
Criterion Covered Total %
statement 21 269 7.8
branch 0 118 0.0
condition 0 66 0.0
subroutine 7 12 58.3
pod 1 5 20.0
total 29 470 6.1


line stmt bran cond sub pod time code
1             package PDF::Builder::Resource::Font::Postscript;
2              
3 1     1   1782 use base 'PDF::Builder::Resource::Font';
  1         7  
  1         110  
4              
5 1     1   7 use strict;
  1         3  
  1         20  
6 1     1   6 use warnings;
  1         2  
  1         51  
7              
8             our $VERSION = '3.025'; # VERSION
9             our $LAST_UPDATE = '3.024'; # manually update whenever code is changed
10              
11 1     1   6 use Encode qw(:all);
  1         2  
  1         231  
12 1     1   7 use IO::File qw();
  1         2  
  1         18  
13              
14 1     1   5 use PDF::Builder::Util;
  1         2  
  1         153  
15 1     1   33 use PDF::Builder::Basic::PDF::Utils;
  1         7  
  1         3744  
16              
17             =head1 NAME
18              
19             PDF::Builder::Resource::Font::Postscript - support routines for using PostScript fonts. Inherits from L
20              
21             =cut
22              
23             sub new {
24 0     0 1   my ($class, $pdf, $psfile, %opts) = @_;
25             # copy dashed option names to preferred undashed names
26 0 0 0       if (defined $opts{'-encode'} && !defined $opts{'encode'}) { $opts{'encode'} = delete($opts{'-encode'}); }
  0            
27 0 0 0       if (defined $opts{'-afmfile'} && !defined $opts{'afmfile'}) { $opts{'afmfile'} = delete($opts{'-afmfile'}); }
  0            
28 0 0 0       if (defined $opts{'-pfmfile'} && !defined $opts{'pfmfile'}) { $opts{'pfmfile'} = delete($opts{'-pfmfile'}); }
  0            
29 0 0 0       if (defined $opts{'-xfmfile'} && !defined $opts{'xfmfile'}) { $opts{'xfmfile'} = delete($opts{'-xfmfile'}); }
  0            
30 0 0 0       if (defined $opts{'-pdfname'} && !defined $opts{'pdfname'}) { $opts{'pdfname'} = delete($opts{'-pdfname'}); }
  0            
31 0 0 0       if (defined $opts{'-nocomps'} && !defined $opts{'nocomps'}) { $opts{'nocomps'} = delete($opts{'-nocomps'}); }
  0            
32 0 0 0       if (defined $opts{'-dokern'} && !defined $opts{'dokern'}) { $opts{'dokern'} = delete($opts{'-dokern'}); }
  0            
33              
34 0           my ($self);
35             my ($data);
36              
37 0 0         if (defined $opts{'afmfile'}) {
    0          
    0          
38 0           $data = $class->readAFM($opts{'afmfile'});
39             } elsif (defined $opts{'pfmfile'}) {
40 0           $data = $class->readPFM($opts{'pfmfile'});
41             } elsif (defined $opts{'xfmfile'}) {
42 0           $data = $class->readXFM($opts{'xfmfile'});
43             } else {
44 0           die "No proper font-metrics file specified for PostScript file '$psfile'.";
45             }
46              
47 0 0         $class = ref $class if ref $class;
48             # $self = $class->SUPER::new($pdf, $data->{'apiname'}.pdfkey().'~'.time());
49 0           $self = $class->SUPER::new($pdf, $data->{'apiname'}.'PST1f'.pdfkey());
50 0 0         $pdf->new_obj($self) unless $self->is_obj($pdf);
51 0           $self->{' data'} = $data;
52              
53 0 0         if ($opts{'pdfname'}) {
54 0           $self->name($opts{'pdfname'});
55             }
56              
57 0           $self->{'Subtype'} = PDFName("Type1");
58 0           $self->{'FontDescriptor'} = $self->descrByData();
59 0 0         if (-f $psfile) {
60             # $self->{'BaseFont'} = PDFName(pdfkey().'+'.$self->fontname().'~'.time());
61 0           $self->{'BaseFont'} = PDFName(pdfkey().'+'.$self->fontname());
62              
63 0           my ($l1,$l2,$l3, $stream) = $self->readPFAPFB($psfile);
64              
65 0           my $s = PDFDict();
66 0           $self->{'FontDescriptor'}->{'FontFile'} = $s;
67 0           $s->{'Length1'} = PDFNum($l1);
68 0           $s->{'Length2'} = PDFNum($l2);
69 0           $s->{'Length3'} = PDFNum($l3);
70 0           $s->{'Filter'} = PDFArray(PDFName("FlateDecode"));
71 0           $s->{' stream'} = $stream;
72 0 0         if (defined $pdf) {
73 0           $pdf->new_obj($s);
74             }
75             } else {
76 0           $self->{'BaseFont'} = PDFName($self->fontname());
77             }
78              
79 0 0 0       if (defined $opts{'encode'} && $opts{'encode'} =~ m/^utf/i) {
80 0           die "Invalid multibyte encoding for psfont: $opts{'encode'}\n";
81             # probably more encodings to check
82             }
83 0           $self->encodeByData($opts{'encode'}); # undef arg OK
84              
85 0 0         $self->{'-nocomps'} = 1 if $opts{'nocomps'};
86 0 0         $self->{'-dokern'} = 1 if $opts{'dokern'};
87              
88 0           return $self;
89             } # end of new()
90              
91             sub readPFAPFB {
92 0     0 0   my ($self, $file) = @_;
93 0           my ($l1,$l2,$l3, $stream, $t1stream, @lines, $line, $head, $body, $tail);
94              
95 0 0         die "Cannot find PFA/PFB font file '$file' ..." unless -f $file;
96              
97 0           my $l = -s $file;
98              
99 0 0         open(my $inf, "<", $file) or die "$!: $file";
100 0           binmode($inf,':raw');
101 0           read($inf, $line, 2);
102 0           @lines = unpack('C*', $line);
103 0 0 0       if ($lines[0] == 0x80 && $lines[1] == 1) {
    0          
104 0           read($inf, $line, 4);
105 0           $l1 = unpack('V', $line);
106 0           seek($inf, $l1, 1);
107 0           read($inf, $line, 2);
108 0           @lines = unpack('C*', $line);
109 0 0 0       if ($lines[0] == 0x80 && $lines[1] == 2) {
110 0           read($inf, $line, 4);
111 0           $l2 = unpack('V', $line);
112             } else {
113 0           die "Corrupt PFB in file '$file' at marker='2'.";
114             }
115 0           seek($inf, $l2, 1);
116 0           read($inf, $line, 2);
117 0           @lines = unpack('C*', $line);
118 0 0 0       if ($lines[0] == 0x80 && $lines[1] == 1) {
119 0           read($inf, $line, 4);
120 0           $l3 = unpack('V', $line);
121             } else {
122 0           die "Corrupt PFB in file '$file' at marker='3'.";
123             }
124 0           seek($inf, 0, 0);
125 0           @lines = <$inf>;
126 0           $stream = join('', @lines);
127 0           $t1stream = substr($stream, 6, $l1);
128 0           $t1stream .= substr($stream, 12+$l1, $l2);
129 0           $t1stream .= substr($stream, 18+$l1+$l2, $l3);
130             } elsif ($line eq '%!') {
131 0           seek($inf, 0, 0);
132 0           while ($line = <$inf>) {
133 0 0         if (!$l1) {
    0          
134 0           $head .= $line;
135 0 0         if ($line=~/eexec$/) {
136 0           chomp($head);
137 0           $head .= "\x0d";
138 0           $l1 = length($head);
139             }
140             } elsif (!$l2) {
141 0 0         if ($line =~ /^0+$/) {
142 0           $l2 = length($body);
143 0           $tail = $line;
144             } else {
145 0           chomp($line);
146 0           $body .= pack('H*', $line);
147             }
148             } else {
149 0           $tail .= $line;
150             }
151             }
152 0           $l3 = length($tail);
153 0           $t1stream = "$head$body$tail";
154             } else {
155 0           die "Unsupported font-format in file '$file' at marker='1'.";
156             }
157 0           close($inf);
158              
159 0           return($l1,$l2,$l3, $t1stream);
160             } # end of readPFAPFB()
161              
162             # $datahashref = $self->readAFM( $afmfile );
163              
164             sub readAFM {
165 0     0 0   my ($self, $file) = @_;
166              
167 0           my $data = {};
168 0           $data->{'wx'} = {};
169 0           $data->{'bbox'} = {};
170 0           $data->{'char'} = [];
171 0           $data->{'firstchar'} = 255;
172 0           $data->{'lastchar'} = 0;
173              
174 0 0         if (! -e $file) {
175 0           die "File='$file' not found.";
176             }
177 0 0         open(my $afmf, "<", $file) or die "Can't find the AFM file for $file";
178 0           local($/, $_) = ("\n", undef); # ensure correct $INPUT_RECORD_SEPARATOR
179 0           while ($_ = <$afmf>) {
180 0 0         if (/^StartCharMetrics/ .. /^EndCharMetrics/) {
    0          
    0          
181             # only lines that start with "C" or "CH" are parsed
182 0 0         next unless $_ =~ /^CH?\s/;
183 0           my ($ch) = $_ =~ /^CH?\s+(\d+)\s*;/;
184 0   0       $ch = $ch || 0;
185 0           my ($name) = $_ =~ /\bN\s+(\.?\w+)\s*;/;
186 0           my ($wx) = $_ =~ /\bWX\s+(\d+)\s*;/;
187 0           my ($bbox) = $_ =~ /\bB\s+([^;]+);/;
188 0           $bbox =~ s/\s+$//;
189             # Should also parse ligature data (format: L successor ligature)
190 0           $data->{'avgwidth2'} += $wx ;
191 0 0 0       $data->{'maxwidth'} = ($data->{'maxwidth'}||0) < $wx? $wx: $data->{'maxwidth'}||0;
      0        
192 0           $data->{'wx'}->{$name} = $wx;
193 0           $data->{'bbox'}->{$name} = [split(/\s+/,$bbox)];
194 0 0         if ($ch > 0) {
195 0           $data->{'char'}->[$ch] = $name;
196             }
197 0 0         $data->{'lastchar'} = $data->{'lastchar'} < $ch? $ch: $data->{'lastchar'};
198 0 0         $data->{'firstchar'} = $data->{'firstchar'} > $ch? $ch: $data->{'firstchar'};
199 0           next;
200             } elsif (/^StartKernData/ .. /^EndKernData/) {
201 0   0       $data->{'kern'} ||= {};
202 0 0         if ($_ =~ m|^KPX\s+(\S+)\s+(\S+)\s+(\S+)\s*$|i) {
203 0           $data->{'kern'}->{"$1:$2"} = $3;
204             }
205             } elsif (/^StartComposites/ .. /^EndComposites/) {
206 0   0       $data->{'comps'} ||= {};
207 0 0         if ($_ =~ m|^CC\s+(\S+)\s+(\S+)\s+;|i) {
208 0           my ($name, $comp) = ($1, $2);
209 0           my @cv = split(/;/, $_);
210 0           shift @cv;
211 0           my $rng = [];
212 0           foreach (1..$comp) {
213 0           my @c1 = split(/\s+/,shift @cv);
214 0           push @{$rng}, $c1[1],$c1[2],$c1[3];
  0            
215             }
216 0           $data->{'comps'}->{$name} = $rng;
217             }
218             }
219 0 0         last if $_ =~ /^EndFontMetrics/;
220 0 0         if (/(^\w+)\s+(.*)/) {
221 0           my($key, $val) = ($1, $2);
222 0           $key = lc($key);
223 0 0         if (defined $data->{$key}) {
224             # $data->{$key} = [ $data->{$key} ] unless ref $data->{$key};
225             # push(@{$data->{$key}}, $val);
226             } else {
227 0           $val =~ s/[\x00\x1f]+//g;
228 0           $data->{$key} = $val;
229             }
230             } else {
231             ## print STDERR "Can't parse: $_";
232             }
233             }
234 0           close($afmf);
235 0 0         unless (exists $data->{'wx'}->{'.notdef'}) {
236 0           $data->{'wx'}->{'.notdef'} = 0;
237 0           $data->{'bbox'}{'.notdef'} = [0, 0, 0, 0];
238             }
239              
240 0           $data->{'avgwidth2'} /= scalar keys %{$data->{'bbox'}} ;
  0            
241 0           $data->{'avgwidth2'} = int($data->{'avgwidth2'});
242              
243 0           $data->{'fontname'} =~ s/[\x00-\x20]+//og;
244             ## $data->{'fontname'} =~ s/[^A-Za-z0-9]+//og;
245              
246 0 0         if (defined $data->{'fullname'}) {
247 0           $data->{'altname'} = $data->{'fullname'};
248             } else {
249 0           $data->{'altname'} = $data->{'familyname'};
250 0 0         $data->{'altname'} .= ' Italic' if $data->{'italicangle'} < 0;
251 0 0         $data->{'altname'} .= ' Oblique' if $data->{'italicangle'} > 0;
252 0           $data->{'altname'} .= ' '.$data->{'weight'};
253             }
254 0           $data->{'apiname'} = $data->{'altname'};
255 0           $data->{'altname'} =~ s/[^A-Za-z0-9]+//og;
256              
257 0           $data->{'subname'} = $data->{'weight'};
258 0 0         $data->{'subname'} .= ' Italic' if $data->{'italicangle'} < 0;
259 0 0         $data->{'subname'} .= ' Oblique' if $data->{'italicangle'} > 0;
260 0           $data->{'subname'} =~ s/[^A-Za-z0-9]+//og;
261              
262 0   0       $data->{'missingwidth'} ||= $data->{'avgwidth2'};
263              
264 0           $data->{'issymbol'} = 0;
265 0           $data->{'fontbbox'} = [ split(/\s+/,$data->{'fontbbox'}) ];
266              
267 0           $data->{'apiname'} = join('', map { ucfirst(lc(substr($_, 0, 2))) } split m/[^A-Za-z0-9\s]+/, $data->{'apiname'});
  0            
268              
269 0           $data->{'flags'} = 34;
270              
271 0   0       $data->{'uni'} ||= [];
272 0           foreach my $n (0..255) {
273 0   0       $data->{'uni'}->[$n] = uniByName($data->{'char'}->[$n] || '.notdef') || 0;
274             }
275 0           delete $data->{'bbox'};
276              
277 0           return $data;
278             } # end of readAFM()
279              
280             sub readPFM {
281 0     0 0   my ($self, $file) = @_;
282              
283 0 0         if (! -e $file) {
284 0           die "pfmfile='$file' not found.";
285             }
286 0           my $fh = IO::File->new();
287 0           my $data = {};
288              
289 0           $data->{'issymbol'} = 0;
290              
291 0           $data->{'wx'} = {};
292 0           $data->{'bbox'} = {};
293 0           $data->{'kern'} = {};
294 0           $data->{'char'} = [];
295              
296 0           my $buf;
297 0 0         open($fh, "<", $file) || return;
298 0           binmode($fh, ':raw');
299 0           read($fh, $buf, 117 + 30);
300              
301 0           my %df;
302             # Packing structure for PFM Header
303             ( $df{'Version'},
304             $df{'Size'},
305             $df{'Copyright'},
306             $df{'Type'},
307             $df{'Point'},
308             $df{'VertRes'},
309             $df{'HorizRes'},
310             $df{'Ascent'},
311             $df{'InternalLeading'},
312             $df{'ExternalLeading'},
313             $df{'Italic'},
314             $df{'Underline'},
315             $df{'StrikeOut'},
316             $df{'Weight'},
317             #define FW_DONTCARE 0
318             #define FW_THIN 100
319             #define FW_EXTRALIGHT 200
320             #define FW_ULTRALIGHT FW_EXTRALIGHT
321             #define FW_LIGHT 300
322             #define FW_NORMAL 400
323             #define FW_REGULAR 400
324             #define FW_MEDIUM 500
325             #define FW_SEMIBOLD 600
326             #define FW_DEMIBOLD FW_SEMIBOLD
327             #define FW_BOLD 700
328             #define FW_EXTRABOLD 800
329             #define FW_ULTRABOLD FW_EXTRABOLD
330             #define FW_HEAVY 900
331             #define FW_BLACK FW_HEAVY
332             $df{'CharSet'},
333             #define ANSI_CHARSET 0
334             #define DEFAULT_CHARSET 1
335             #define SYMBOL_CHARSET 2
336             #define SHIFTJIS_CHARSET 128
337             #define HANGEUL_CHARSET 129
338             #define HANGUL_CHARSET 129
339             #define GB2312_CHARSET 134
340             #define CHINESEBIG5_CHARSET 136
341             #define GREEK_CHARSET 161
342             #define TURKISH_CHARSET 162
343             #define HEBREW_CHARSET 177
344             #define ARABIC_CHARSET 178
345             #define BALTIC_CHARSET 186
346             #define RUSSIAN_CHARSET 204
347             #define THAI_CHARSET 222
348             #define EASTEUROPE_CHARSET 238
349             #define OEM_CHARSET 255
350             #define JOHAB_CHARSET 130
351             #define VIETNAMESE_CHARSET 163
352             #define MAC_CHARSET 77
353             #define BALTIC_CHARSET 186
354             #define JOHAB_CHARSET 130
355             #define VIETNAMESE_CHARSET 163
356             $df{'PixWidth'},
357             $df{'PixHeight'},
358             $df{'PitchAndFamily'},
359             #define DEFAULT_PITCH 0
360             #define FIXED_PITCH 1
361             #define VARIABLE_PITCH 2
362             #define MONO_FONT 8
363             #define FF_DECORATIVE 80
364             #define FF_DONTCARE 0
365             #define FF_MODERN 48
366             #define FF_ROMAN 16
367             #define FF_SCRIPT 64
368             #define FF_SWISS 32
369             $df{'AvgWidth'},
370             $df{'MaxWidth'},
371             $df{'FirstChar'},
372             $df{'LastChar'},
373             $df{'DefaultChar'},
374             $df{'BreakChar'},
375             $df{'WidthBytes'},
376             $df{'Device'},
377             $df{'Face'},
378             $df{'BitsPointer'},
379             $df{'BitsOffset'},
380             $df{'SizeFields'}, # Two bytes, the size of extension section
381             $df{'ExtMetricsOffset'}, # Four bytes, offset value to the 'Extended Text Metrics' section
382             $df{'ExtentTable'}, # Four bytes Offset value to the Extent Table
383             $df{'OriginTable'}, # Four bytes 0
384             $df{'PairKernTable'}, # Four bytes 0
385             $df{'TrackKernTable'}, # Four bytes 0
386             $df{'DriverInfo'}, # Four bytes Offset value to the PostScript font name string
387 0           $df{'Reserved'}, # Four bytes 0
388             ) = unpack("vVa60vvvvvvvCCCvCvvCvvCCCCvVVVV vVVVVVVV",$buf); # PFM Header + Ext
389              
390 0           seek($fh, $df{Device}, 0);
391 0           read($fh, $buf, 250);
392              
393 0           ($df{'postScript'}) = unpack("Z*", $buf);
394 0           $buf = substr($buf, length($df{'postScript'})+1, 250);
395 0           ($df{'windowsName'}) = unpack("Z*", $buf);
396 0           $buf = substr($buf, length($df{'windowsName'})+1, 250);
397 0           ($df{'psName'}) = unpack("Z*", $buf);
398              
399 0           seek($fh, $df{'ExtMetricsOffset'}, 0);
400 0           read($fh, $buf, 52);
401              
402             ( $df{'etmSize'},
403             $df{'PointSize'},
404             $df{'Orientation'},
405             $df{'MasterHeight'},
406             $df{'MinScale'},
407             $df{'MaxScale'},
408             $df{'MasterUnits'},
409             $df{'CapHeight'},
410             $df{'xHeight'},
411             $df{'LowerCaseAscent'},
412             $df{'LowerCaseDescent'},
413             $df{'Slant'},
414             $df{'SuperScript'},
415             $df{'SubScript'},
416             $df{'SuperScriptSize'},
417             $df{'SubScriptSize'},
418             $df{'UnderlineOffset'},
419             $df{'UnderlineWidth'},
420             $df{'DoubleUpperUnderlineOffset'},
421             $df{'DoubleLowerUnderlineOffset'},
422             $df{'DoubleUpperUnderlineWidth'},
423             $df{'DoubleLowerUnderlineWidth'},
424             $df{'StrikeOutOffset'},
425             $df{'StrikeOutWidth'},
426             $df{'KernPairs'},
427 0           $df{'KernTracks'} ) = unpack('v*', $buf);
428              
429 0           $data->{'fontname'} = $df{'psName'};
430 0           $data->{'fontname'} =~ s/[^A-Za-z0-9]+//og;
431 0           $data->{'apiname'} = join('', map { ucfirst(lc(substr($_, 0, 2))) } split m/[^A-Za-z0-9\s]+/, $df{'windowsName'});
  0            
432              
433 0           $data->{'upem'} = 1000;
434              
435 0           $data->{'fontbbox'} = [-100,-100, $df{'MaxWidth'},$df{'Ascent'}];
436              
437 0           $data->{'stemv'} = 0;
438 0           $data->{'stemh'} = 0;
439              
440 0   0       $data->{'lastchar'} = $df{'LastChar'}||0; # running max
441 0   0       $data->{'firstchar'} = $df{'FirstChar'}||255; # running min
442              
443 0           $data->{'missingwidth'} = $df{'AvgWidth'};
444 0           $data->{'maxwidth'} = $df{'MaxWidth'};
445 0           $data->{'ascender'} = $df{'Ascent'};
446 0           $data->{'descender'} = -$df{'LowerCaseDescent'};
447              
448 0           $data->{'flags'} = 0;
449             # FixedPitch 1
450 0 0 0       $data->{'flags'} |= 1 if (($df{'PitchAndFamily'} & 1) || ($df{'PitchAndFamily'} & 8)) && !($df{'PitchAndFamily'} & 2);
      0        
451             # Serif 2
452 0 0 0       $data->{'flags'} |= 2 if ($df{'PitchAndFamily'} & 16) && !($df{'PitchAndFamily'} & 32);
453             # Symbolic 4
454 0 0         $data->{'flags'} |= 4 if $df{'PitchAndFamily'} & 80;
455             # Script 8
456 0 0         $data->{'flags'} |= 8 if $df{'PitchAndFamily'} & 64;
457             # Nonsymbolic 32
458 0 0         $data->{'flags'} |= 32 unless $df{'PitchAndFamily'} & 80;
459             # Italic 64
460 0 0         $data->{'flags'} |= 64 if $df{'Italic'};
461              
462             #bit 17 AllCap
463             #bit 18 SmallCap
464             #bit 19 ForceBold
465              
466 0           $data->{'capheight'} = $df{'CapHeight'};
467 0           $data->{'xheight'} = $df{'xHeight'};
468              
469 0           $data->{'uni'} = [ unpack('U*', decode('cp1252', pack('C*',(0..255)))) ];
470 0 0         $data->{'char'} = [ map { nameByUni($_) || '.notdef' } @{$data->{'uni'}} ];
  0            
  0            
471              
472 0           $data->{'italicangle'} = -12*$df{'Italic'};
473 0   0       $data->{'isfixedpitch'} = ($df{'PitchAndFamily'} & 8) || ($df{'PitchAndFamily'} & 1);
474 0           $data->{'underlineposition'} = -$df{'UnderlineOffset'};
475 0           $data->{'underlinethickness'} = $df{'UnderlineWidth'};
476              
477 0           seek($fh, $df{'ExtentTable'}, 0);
478              
479 0           foreach my $k ($df{'FirstChar'} .. $df{'LastChar'}) {
480 0           read($fh, $buf, 2);
481 0           my ($wx) = unpack('v', $buf);
482 0           $data->{'wx'}->{$data->{'char'}->[$k]} = $wx;
483             # print STDERR "e: c=$k n='".$data->{'char'}->[$k]."' wx='$wx'\n";
484             }
485 0           $data->{'pfm'} = \%df;
486 0           close($fh);
487              
488 0           return $data;
489             } # end of readPFM()
490              
491             sub readXFM {
492 0     0 0   my ($class, $xfmfile) = @_;
493              
494 0 0         die "Cannot find font '$xfmfile' ..." unless -f $xfmfile;
495              
496 0           my $data = {};
497              
498 0           return $data;
499             }
500              
501             1;