File Coverage

blib/lib/Font/TFM.pm
Criterion Covered Total %
statement 170 271 62.7
branch 80 156 51.2
condition 13 24 54.1
subroutine 13 22 59.0
pod 6 17 35.2
total 282 490 57.5


line stmt bran cond sub pod time code
1              
2             =head1 NAME
3              
4             Font::TFM -- read information from TeX font metric files
5              
6             =cut
7              
8              
9             package Font::TFM;
10 1     1   940 use strict;
  1         1  
  1         39  
11 1         3508 use vars qw( $VERSION $DEBUG $TEXFONTSDIR $TEXFONTSUSELS
12 1     1   5 $LSFILENAME $MULTIPLY $errstr );
  1         2  
13              
14             # ################
15             # Global variables
16             #
17             $VERSION = '1.01';
18             $errstr = '';
19             $DEBUG = 0;
20 50     50 0 104 sub DEBUG () { $DEBUG; }
21             sub Error {
22 1     1 0 5 $errstr = join '', @_;
23 1 50       3 print STDERR @_, "\n" if $DEBUG;
24             }
25              
26             $TEXFONTSDIR = '.';
27             $TEXFONTSUSELS = 1;
28             $LSFILENAME = "ls-R";
29             $MULTIPLY = 65536;
30              
31             # #####################
32             # Load new font at size
33             #
34             sub new_at {
35 0     0 0 0 my ($class, $fontname, $size) = @_;
36 0         0 new($class, 'name' => $fontname, 'at' => $size);
37             }
38              
39             # ########################
40             # Load new font with scale
41             #
42             sub new {
43 6     6 0 181 my $class = shift;
44 6         13 my %opts = ();
45 6 100 33     35 if (@_ > 2 or (@_ == 2 and $_[1] !~ /^(\d*\.)?\d+$/)) {
      66        
46 2 50       7 if (@_ % 2) {
47 0         0 $opts{name} = shift;
48             }
49 2         7 %opts = @_;
50             } else {
51 4         13 @opts{'name', 'scale'} = @_;
52             }
53            
54             # make the object
55 6         20 my $self = bless {}, $class;
56              
57 6         7 my ($file_handle);
58 6         8 eval {
59 6         22 $file_handle = $self->open_tfm(%opts);
60             };
61 6 100       14 if ($@) {
62 1         4 Error($@); return;
  1         7  
63             }
64              
65             # read header
66 5         8 my $buffer = '';
67 5 50       117 if (read($file_handle, $buffer, 24) != 24) {
68 0         0 Error("Error reading TFM header: $!"); return;
  0         0  
69             }
70              
71             # get 12 fields of the header
72 5         21 @{$self}{ qw(length headerlength smallest largest numwidth
  5         45  
73             numheight numdepth numic numligkern numkern numext numparam) }
74             = unpack "n12", $buffer;
75 5         18 $self->{numofchars} = $self->{largest} - $self->{smallest} + 1;
76 5         9 $self->{'length'} = $self->{'length'} * 4 - 24;
77 5         7 $self->{headerlength} *= 4;
78              
79             # read rest of the file
80 5 50       18 if (read($file_handle, $buffer, $self->{'length'})
81             != $self->{'length'}) {
82 0         0 Error("Error reading body: $!"); return;
  0         0  
83             }
84 5         50 close $file_handle;
85              
86 5         20 my $headerrestlength = $self->{headerlength} - 18 * 4;
87              
88 5         6 my ($face, $headerrest, @charinfo, @width, @height, @depth,
89             @italic, @ligkern, @kern, @exten);
90              
91             # split the file into various arrays
92 5         212 (@{$self}{ qw(checksum designsize codingschemestringlength
  5         329  
93             codingscheme familystringlength family sevenbitsafe ) },
94             undef, undef, $face,
95             $headerrest,
96             @charinfo[0 .. $self->{numofchars} - 1],
97             @width[0 .. $self->{numwidth} - 1],
98             @height[0 .. $self->{numheight} - 1],
99             @depth[0 .. $self->{numdepth} - 1],
100             @italic[0 .. $self->{numic} - 1],
101             @ligkern[0 .. $self->{numligkern} - 1],
102             @kern[0 .. $self->{numkern} - 1],
103             @exten[0 .. $self->{numext} - 1],
104 5         245 @{$self->{param}}[1 .. $self->{numparam}],
105             )
106             = unpack "NNCA39CA19C4a$headerrestlength" .
107             "a4" x $self->{numofchars} .
108             "N$self->{numwidth}N$self->{numheight}N$self->{numdepth}N$self->{numic}" .
109             "a4" x $self->{numligkern} .
110             "N$self->{numkern}" .
111             "a4" x $self->{numext} .
112             "N$self->{numparam}", $buffer;
113             # the unpack above does all the work, isn't it neat?
114            
115 5         122 $self->{designsize} = getfixword($self->{designsize});
116              
117 5         7 my $fontsize = $self->{designsize};
118 5 100       17 if (defined $opts{at}) {
    50          
119 2         3 $fontsize = $opts{at};
120             } elsif (defined $opts{scale}) {
121 0         0 $fontsize *= $opts{scale};
122             }
123 5         8 $self->{fontsize} = $fontsize;
124 5 50       13 my $multiplysize = $fontsize *
125             ( defined $opts{'multiply'} ? $opts{'multiply'} : $MULTIPLY );
126              
127 5 50       13 if ($self->{sevenbitsafe}) {
128 0         0 $self->{sevenbitsafe} = 1;
129             }
130              
131 5         7 $self->{face} = ""; # computation of face seems useless
132 5 50       12 if ($face < 18) {
133 0         0 my ($weight, $slope, $expansion) = ("M", "R", "R");
134 0 0       0 if ($face - 12 >= 0) { $expansion = "E"; $face -= 12; }
  0 0       0  
  0         0  
135 0         0 elsif ($face - 6 >= 0) { $expansion = "C"; $face -= 6; }
  0         0  
136 0 0       0 if ($face - 4 >= 0) { $weight = "L"; $face -= 4; }
  0 0       0  
  0         0  
137 0         0 elsif ($face - 2 >= 0) { $weight = "B"; $face -= 2; }
  0         0  
138 0 0       0 if ($face > 0) { $slope = "I"; }
  0         0  
139 0         0 $self->{face} = "$weight$slope$expansion";
140             }
141              
142 5         12 @{$self->{headerrest}}[0 .. $headerrestlength] = split //, $headerrest;
  5         14  
143              
144 5         9 @width = map { getfixword($_) * $multiplysize } @width;
  184         209  
145 5         17 @height = map { getfixword($_) * $multiplysize } @height;
  69         83  
146 5         11 @depth = map { getfixword($_) * $multiplysize } @depth;
  60         77  
147 5         12 @italic = map { getfixword($_) * $multiplysize } @italic;
  36         48  
148 5         10 @kern = map { getfixword($_) * $multiplysize } @kern;
  37         48  
149 5         14 $self->{param}[1] = getfixword($self->{param}[1]);
150 5         16 @{$self->{param}}[2 .. $self->{numparam}] =
  51         67  
151 5         10 map { getfixword($_) * $multiplysize }
152 5         13 @{$self->{param}}[2 .. $self->{numparam}];
153             # compute the actual dimensions
154              
155 5 100       17 if (@ligkern) { # check for boundary char
156 4         5 my ($skip, $next, $opbyte, $remainder);
157 4         14 ($skip, $next) = unpack "Ca1", $ligkern[0];
158 4 50       8 if ($skip == 255) {
159 0         0 $self->{"boundary"} = $next;
160             }
161 4         13 ($skip, $next, $opbyte, $remainder) = unpack
162             "Ca1CC", $ligkern[$#ligkern];
163 4 50       12 if ($skip == 255) {
164 0         0 process_lig_kern($self, "boundary", \@ligkern,
165             256 * $opbyte + $remainder, \@kern);
166             }
167             }
168              
169 5         13 for (0 .. $self->{numofchars} - 1) {
170 640         1007 my $char = pack "C", $_ + $self->{smallest};
171 640         1115 my ($wid, $heidep, $italtag, $remainder)
172             = unpack "C4", $charinfo[$_];
173 640 50       1087 next if ($wid == 0);
174             # set up dimensions of the character
175 640         3423 ($self->{width}{$char}, $self->{height}{$char},
176             $self->{depth}{$char}, $self->{italic}{$char})
177             = ($width[$wid], $height[$heidep >> 4],
178             $depth[$heidep & 0x0f], $italic[$italtag >> 2]);
179            
180 640         774 my $tag = $italtag & 0x03; # other info
181 640 100       1586 if ($tag == 1) { # lig/kern program
    100          
    100          
182 140         269 process_lig_kern($self, $char, \@ligkern, $remainder, \@kern);
183             }
184             elsif ($tag == 2) { # larger character
185 74         184 $self->{larger}{$char} = pack "C", $remainder;
186             }
187             elsif ($tag == 3) { # extensible character
188 28         52 my ($top, $mid, $bot, $rep) = unpack "C4", $exten[$remainder];
189 28 100       85 $self->{extentop}{$char} = pack "C", $top if $top;
190 28 100       49 $self->{extenmid}{$char} = pack "C", $mid if $mid;
191 28 100       69 $self->{extenbot}{$char} = pack "C", $bot if $bot;
192 28         65 $self->{extenrep}{$char} = $rep;
193             }
194             }
195 5         96 $self;
196             }
197              
198             # ###################################
199             # Open the file and return filehandle
200             #
201             sub open_tfm {
202 6     6 0 12 my ($self, %opts) = @_;
203 6         5 my $filename;
204 6 100       21 if (defined $opts{file}) {
    50          
205 1         3 $filename = $opts{file}
206             } elsif (defined $opts{name}) {
207 5 100       17 $filename = find_tfm_file(%opts) or
208             die "No tfm file found for font $opts{name}";
209             } else {
210 0         0 die "Either font name or file name has to be specified";
211             }
212              
213             # try to open the file
214 5 50       11 print STDERR "Loading $filename\n" if DEBUG;
215 5 50       176 open TFMFILE, $filename or
216             die "Error opening $filename: $!";
217 5         13 binmode TFMFILE;
218              
219 5         23 $self->{name} = $filename;
220 5         23 $self->{name} =~ s/\.tfm$//i;
221 5         24 $self->{name} =~ s!^.*/!!;
222              
223 5         17 return(\*TFMFILE);
224             }
225              
226             # ###################################################
227             # Process the ligature/kerning program for a character
228             #
229             sub process_lig_kern {
230 140     140 0 191 my ($self, $char, $ligkernref, $prognum, $kernref) = @_;
231 140         121 my $firstinstr = 1;
232 140         108 while (1) {
233 608         1269 my ($skipbyte, $nextchar, $opbyte, $remainder)
234             = unpack "Ca1CC", $ligkernref->[$prognum];
235 608 100       1024 if ($firstinstr) {
236 140 50       207 if ($skipbyte > 128) {
237 0         0 $prognum = 256 * $opbyte + $remainder;
238 0         0 ($skipbyte, $nextchar, $opbyte, $remainder)
239             = unpack "Ca1CC", $ligkernref->[$prognum];
240             }
241             }
242 608 100 100     2171 if ($opbyte >= 128
243             and not exists $self->{kern}{$char . $nextchar}) {
244 569         1256 $self->{kern}{$char . $nextchar}
245             = $kernref->[ 256 * ($opbyte - 128)
246             + $remainder];
247             }
248 608 100 66     1229 if ($opbyte < 128
249             and not exists $self->{lig}{$char . $nextchar}) {
250 33         48 my ($a, $b, $c) = ($opbyte >> 2, ($opbyte >> 1) & 0x01, $opbyte & 0x01);
251 33         33 my $out = "";
252 33 50       47 $out .= $char if $b;
253 33         43 $out .= pack "C", $remainder;
254 33 50       43 $out .= $nextchar if $c;
255 33         97 $self->{lig}{$char . $nextchar} = $out;
256 33         59 $self->{ligpassover}{$char . $nextchar} = $a;
257             }
258 608 100       1057 last if ($skipbyte >= 128);
259 468         398 $prognum += $skipbyte + 1;
260 468         455 $firstinstr = 0;
261             }
262             }
263              
264             # #################
265             # Find the TFM file
266             #
267             sub find_tfm_file {
268 5     5 0 8 my %opts = @_;
269 5         11 my $fontname = $opts{name};
270 5 50       25 $fontname .= ".tfm" unless $fontname =~ /\.tfm$/i;
271 5 50       11 print STDERR "Font::TFM::find_tfm_file: \$fontname = $fontname\n" if DEBUG;
272 5         8 my $directory;
273 5 50       23 for $directory (split /:/,
274             (defined $opts{path} ? $opts{path} : $TEXFONTSDIR)) {
275 5 50       8 print STDERR "Font::TFM::find_tfm_file: \$directory = $directory\n" if DEBUG;
276 5         16 my $file = find_tfm_file_in_directory(%opts,
277             name => $fontname, dir => $directory);
278 5 100       26 return $file if defined $file;
279             }
280 1         20 return;
281             }
282             sub find_tfm_file_in_directory {
283 22     22 0 68 my %opts = @_;
284 22         33 my ($fontname, $directory) = ($opts{name}, $opts{dir});
285 22 50       51 $directory .= '/' unless $directory eq '';
286 22         30 my $tfmfile = "$directory$fontname";
287 22         32 my $lsfile = "$directory$LSFILENAME";
288 22 50       30 print STDERR "Font::TFM::find_tfm_file_in_directory: \$directory = $directory\n" if DEBUG;
289 22 100       271 if (-f $tfmfile) {
290 4         15 return $tfmfile;
291             }
292 18         19 my $usels = $TEXFONTSUSELS;
293 18 50       30 if (defined $opts{usels}) {
294 0         0 $usels = 1;
295 0 0 0     0 $usels = 0 if $opts{usels} eq '0' or $opts{usels} eq 'no';
296             }
297 18 50 33     163 if (-f $lsfile and $usels) {
298 0         0 my $file = find_tfm_file_in_ls($fontname, $lsfile);
299 0 0       0 return $file if defined $file;
300             }
301             else {
302 18         17 my $subdir;
303 18         1074 for $subdir (<$directory/*>) {
304 35 100       353 next unless -d $subdir;
305 17         68 my $file = find_tfm_file_in_directory(%opts,
306             dir => $subdir);
307 17 50       34 return $file if defined $file;
308             }
309             }
310 18         46 return;
311             }
312             sub find_tfm_file_in_ls {
313 0     0 0 0 my ($fontname, $lsfile) = @_;
314 0         0 my $lsdir = $lsfile;
315 0         0 $lsdir =~ s!/$LSFILENAME$!!;
316 0 0       0 print STDERR "Font::TFM::find_tfm_file_in_ls: \$lsfile = $lsfile\n" if DEBUG;
317 0 0       0 print STDERR "Font::TFM::find_tfm_file_in_ls: \$lsdir = $lsdir\n" if DEBUG;
318 0 0       0 if (not open LSFILE, $lsfile) {
319 0         0 Error("Error opening $lsfile: $!"); return;
  0         0  
320             }
321 0         0 local ($/) = "\n";
322 0         0 while () {
323 0         0 chomp;
324 0 0       0 if (/:$/) {
    0          
325 0         0 $lsdir = $_;
326 0         0 $lsdir =~ s!:$!!;
327 0 0       0 print STDERR "Font::TFM::find_tfm_file_in_ls: \$lsdir = $lsdir\n" if (DEBUG > 10);
328             }
329             elsif ($_ eq $fontname) {
330 0         0 my $file = "$lsdir/$fontname";
331 0 0       0 if (-f $file) {
332 0         0 close LSFILE;
333 0 0       0 print STDERR "file $fontname found in $lsfile\n" if DEBUG;
334 0         0 return $file;
335             }
336             }
337             }
338 0 0       0 print STDERR "file $fontname not found in $lsfile\n" if DEBUG;
339 0         0 return;
340             }
341             sub getfixword {
342 447     447 0 409 my $val = $_[0];
343 447         488 my $p = pack "L", $val;
344 447 100       655 if ($val & 0x80000000) {
345 26         33 $val = unpack "l", $p;
346             }
347 447         855 return ($val / (1 << 20));
348             }
349             sub kern {
350 2     2 1 71 my ($self, $double, $second) = @_;
351 2 50       6 $double .= $second if (defined $second);
352 2 50       6 if (defined $self->{kern}{$double}) {
353 2         7 return $self->{kern}{$double};
354             }
355 0         0 return 0;
356             }
357             sub lig {
358 0     0 1 0 my ($self, $double, $second) = @_;
359 0 0       0 $double .= $second if (defined $second);
360 0 0       0 if (defined $self->{lig}{$double}) {
361 0         0 return $self->{lig}{$double};
362             }
363 0         0 return undef;
364             }
365             sub ligpassover {
366 0     0 1 0 my ($self, $double) = @_;
367 0         0 $self->{ligpassover}{$double};
368             }
369             sub param {
370 0     0 1 0 my ($self, $param) = @_;
371 0         0 $self->{param}[$param];
372             }
373             sub word_dimensions {
374 0     0 1 0 my ($self, $text) = @_;
375 0         0 my @expanded = $self->expand($text);
376 0         0 my ($width, $height, $depth) = (0, 0, 0);
377 0         0 while (@expanded) {
378 0         0 my $word = shift @expanded;
379 0         0 while ($word =~ /./sg) {
380 0         0 my $char = $&;
381 0         0 $width += $self->width($char);
382 0         0 my $newval = $self->height($char);
383 0 0       0 $height = $newval if ($newval > $height);
384 0         0 $newval = $self->depth($char);
385 0 0       0 $depth = $newval if ($newval > $depth);
386             }
387 0 0       0 last if (not @expanded);
388 0         0 $width += shift @expanded;
389             }
390 0         0 ($width, $height, $depth);
391             }
392             sub expand {
393 0     0 1 0 my ($self, $text) = @_;
394 0         0 my $pos = 0;
395            
396             # ligature substitutions
397 0         0 while ($pos < (length $text) - 1) {
398 0         0 my $found;
399 0         0 my $substr = substr $text, $pos, 2;
400 0         0 $found = $self->lig(substr $text, $pos, 2);
401 0 0       0 if (defined $found) {
402 0         0 substr($text, $pos, 2) = $found;
403 0         0 $pos += $self->ligpassover($substr);
404             }
405             else
406 0         0 { $pos++ }
407             }
408              
409             # kerning processing
410 0         0 my @out = ();
411 0         0 my $currentstring = substr $text, 0, 1;
412 0         0 while ($text =~ /(.)(?=(.))/gs) {
413 0 0       0 if ($self->kern($1 . $2)) {
414 0         0 push @out, $currentstring;
415 0         0 $currentstring = '';
416 0         0 push @out, $self->kern($1 . $2);
417             }
418 0         0 $currentstring .= $2
419             }
420 0         0 push @out, $currentstring;
421 0         0 @out;
422             }
423              
424              
425             my %PARAM_NAMES = ( slant => 1, space => 2, spacestretch => 3,
426             spaceshrink => 4, xheight => 5, emwidth => 6, quad => 6,
427             extraspace => 7,
428             );
429             my @GENERAL_NAMES = qw( fontsize designsize name checksum codingscheme );
430             my @CHAR_NAMES = qw( width height depth italic );
431             my %WORD_NAMES = ( word_width => 0, word_height => 1, word_depth => 2 );
432             my %MATH_SY_NAMES = ( num1 => 8, num2 => 9, num3 => 10,
433             denom1 => 11, denom2 => 12,
434             sup1 => 13, sup2 => 14, sup3 => 15,
435             sub1 => 16, sub2 => 17,
436             supdrop => 18,
437             subdrop => 19,
438             delim1 => 20, delim2 => 21,
439             axisheight => 22,
440             );
441             my %MATH_EX_NAMES = (
442             defaultrulethickness => 8,
443             bigopspacing1 => 9,
444             bigopspacing2 => 10,
445             bigopspacing3 => 11,
446             bigopspacing4 => 12,
447             bigopspacing5 => 13,
448             );
449              
450 1     1   11 use vars qw( $AUTOLOAD );
  1         7  
  1         393  
451             sub AUTOLOAD {
452 13     13   441 my $self = shift;
453 13         16 my $function = $AUTOLOAD;
454 13         30 local ($_);
455 13         75 $function =~ s/^.*:://;
456 13         20 $function =~ s/_//g;
457 13 50       30 print STDERR "Autoloading $function\n" if DEBUG;
458 13 50       28 return ($self->word_dimensions(shift))[$WORD_NAMES{$function}]
459             if defined $WORD_NAMES{$function};
460 13 100       28 return $self->{'param'}[$PARAM_NAMES{$function}]
461             if (defined $PARAM_NAMES{$function});
462 44         89 return $self->{$function}{$_[0]}
463 11 100       20 if grep { $_ eq $function } @CHAR_NAMES;
464 40         69 return $self->{$function}
465 8 100       9 if grep { $_ eq $function } @GENERAL_NAMES;
466 6 100 66     27 if ($self->{codingscheme} eq 'TeX math symbols'
467             and defined $MATH_SY_NAMES{$function}) {
468 2         11 return $self->{'param'}[$MATH_SY_NAMES{$function}]
469             }
470 4 100 66     19 if ($self->{codingscheme} eq 'TeX math extension'
471             and defined $MATH_EX_NAMES{$function}) {
472 2         10 return $self->{'param'}[$MATH_EX_NAMES{$function}]
473             }
474 2         27 die "Method $function not defined for $self";
475             }
476              
477 0     0 0   sub Version { $VERSION; }
478 0     0     sub DESTROY {}
479             1;
480              
481             __END__