File Coverage

blib/lib/PDF/Builder/Resource/Font/BdFont.pm
Criterion Covered Total %
statement 15 226 6.6
branch 0 80 0.0
condition 0 57 0.0
subroutine 5 8 62.5
pod 1 3 33.3
total 21 374 5.6


line stmt bran cond sub pod time code
1             package PDF::Builder::Resource::Font::BdFont;
2              
3 1     1   1156 use base 'PDF::Builder::Resource::Font';
  1         2  
  1         106  
4              
5 1     1   6 use strict;
  1         1  
  1         17  
6 1     1   2 use warnings;
  1         2  
  1         81  
7              
8             our $VERSION = '3.028'; # VERSION
9             our $LAST_UPDATE = '3.027'; # manually update whenever code is changed
10              
11 1     1   7 use PDF::Builder::Util;
  1         2  
  1         508  
12 1     1   8 use PDF::Builder::Basic::PDF::Utils;
  1         1  
  1         4155  
13              
14             our $BmpNum = 0;
15              
16             =head1 NAME
17              
18             PDF::Builder::Resource::Font::BdFont - Module for using bitmapped Fonts
19              
20             Inherits from L<PDF::Builder::Resource::Font>
21              
22             =head1 SYNOPSIS
23              
24             #
25             use PDF::Builder;
26             #
27             $pdf = PDF::Builder->new();
28             $sft = $pdf->bdfont($file);
29             #
30              
31             This creates a bitmapped font from a .bdf (bitmap distribution font) file.
32             The default is to use square elements, and the style can be changed to use
33             filled dots (looking more like a dot-matrix printer). The font will be
34             embedded in the PDF file.
35              
36             Bitmapped fonts are quite rough, low resolution, and difficult to read, so
37             unless you're a sadist who wants to force readers back to the good old days of
38             dot-matrix printers and bitmapped X terminals, try to limit the use of such a
39             font to decorative or novelty effects, such as chapter titles and major
40             headings. Have mercy on your readers and use a real font (TrueType, etc.)
41             for body text!
42              
43             =head1 METHODS
44              
45             =head2 new
46              
47             $font = PDF::Builder::Resource::Font::BdFont->new($pdf, $font, %opts)
48              
49             =over
50              
51             Returns a BmpFont object.
52              
53             =back
54              
55             =cut
56              
57             # TBD? not implemented
58             #I<encode>
59             #... changes the encoding of the font from its default.
60             #See I<Perl's Encode> for the supported values.
61             #
62             #I<pdfname> ... changes the reference-name of the font from its default.
63             #The reference-name is normally generated automatically and can be
64             #retrieved via C<$pdfname=$font->name()>.
65              
66             =pod
67              
68             Valid options (%opts) are:
69              
70             =over
71              
72             =item I<style>
73              
74             A value of 'B<block>' (the default) assembles a character from
75             contiguous square blocks. A value of 'B<dot>' assembles a character from
76             overlapping filled circles, in the style of a dot matrix printer.
77              
78             =back
79              
80             =cut
81             # style => 'image' doesn't seem to want to work (see examples/024_bdffonts
82             # for code). it's not clear whether a 1000 x 1000 pixel bitmap needs to be
83             # generated, to be scaled down to the text size. if so, that's very wasteful.
84              
85             sub new {
86 0     0 1   my ($class, $pdf, $file, %opts) = @_;
87             # copy dashed name options to preferred undashed names
88 0 0 0       if (defined $opts{'-style'} && !defined $opts{'style'}) { $opts{'style'} = delete($opts{'-style'}); }
  0            
89             # pdfname doesn't seem to be used here. consider name as alias
90              
91 0           my ($self, $data);
92 0           my $dot_ratio = 1.2; # diameter of a dot (dot style) relative to
93             # a block's side. note that if exceeds 1.0, max
94             # extents of dot will actually slightly exceed
95             # extents of block. TBD might need to have different
96             # calculations of max extents for block and dot.
97              
98 0 0         $class = ref $class if ref $class;
99 0           $self = $class->SUPER::new($pdf,
100             sprintf('%s+Bdf%02i', pdfkey(), ++$BmpNum));
101 0 0         $pdf->new_obj($self) unless $self->is_obj($pdf);
102              
103             # Adobe Bitmap Distribution Format
104 0           $self->{' data'} = $self->readBDF($file);
105              
106             # character coordinate units for block and dots styles (cell sizes)
107 0           my ($csizeH, $csizeV);
108             # at this point we need to find the actual cell bounds (after adding
109             # in right and up offsets), in order to define 1000 units vertical
110             # $self->{' data'}->{'FONTBOUNDINGBOX'} is a string
111 0           my ($minX, $minY, $maxX, $maxY); # for final 'fontbbox' numbers
112 0           $minX = $minY = 10000;
113 0           $maxX = $maxY = -10000;
114 0           foreach my $w (@{$self->data()->{'char2'}}) {
  0            
115 0           my @bbx = @{$w->{'BBX'}};
  0            
116 0           my $LLx = $bbx[2];
117 0           my $LLy = $bbx[3];
118 0           my $URx = $bbx[0]+$bbx[2];
119 0           my $URy = $bbx[1]+$bbx[3];
120 0 0         if ($LLx < $minX) { $minX = $LLx; }
  0            
121 0 0         if ($LLx > $maxX) { $maxX = $LLx; }
  0            
122 0 0         if ($URx < $minX) { $minX = $URx; }
  0            
123 0 0         if ($URx > $maxX) { $maxX = $URx; }
  0            
124 0 0         if ($LLy < $minY) { $minY = $LLy; }
  0            
125 0 0         if ($LLy > $maxY) { $maxY = $LLy; }
  0            
126 0 0         if ($URy < $minY) { $minY = $URy; }
  0            
127 0 0         if ($URy > $maxY) { $maxY = $URy; }
  0            
128             }
129             # for now, same cell dimensions in X and Y
130 0           $csizeH = $csizeV = int(0.5 + 1000/($maxY + 1));
131              
132 0           my $first = 0; # we'll always do the full single byte encoding
133 0           my $last = 255;
134 0 0         $opts{'style'} = 'block' unless defined $opts{'style'};
135              
136 0           $self->{'Subtype'} = PDFName('Type3');
137 0           $self->{'FirstChar'} = PDFNum($first);
138 0           $self->{'LastChar'} = PDFNum($last);
139             # define glyph drawings on 1000x1000 grid, divide by 1000, multiply by
140             # font size in points
141 0           $self->{'FontMatrix'} = PDFArray(map { PDFNum($_) } (0.001, 0, 0, 0.001, 0, 0) );
  0            
142 0 0         if (defined $self->{' data'}->{'FONT'}) {
143 0           $self->{'Comment'} = PDFString("FontName=" . $self->{' data'}->{'FONT'}, 'x');
144             }
145              
146 0           my $xo = PDFDict();
147 0           $self->{'Encoding'} = $xo;
148 0           $xo->{'Type'} = PDFName('Encoding');
149 0           $xo->{'BaseEncoding'} = PDFName('WinAnsiEncoding');
150             # assign .notdef "char" to anything not found in the .bdf file
151 0   0       $xo->{'Differences'} = PDFArray(PDFNum($first), (map { PDFName($_ || '.notdef') } @{$self->data()->{'char'}}));
  0            
  0            
152              
153 0           my $procs = PDFDict();
154 0           $pdf->new_obj($procs);
155 0           $self->{'CharProcs'} = $procs;
156              
157 0           $self->{'Resources'} = PDFDict();
158 0           $self->{'Resources'}->{'ProcSet'} = PDFArray(map { PDFName($_) } qw(PDF Text ImageB ImageC ImageI));
  0            
159 0           foreach my $w ($first .. $last) {
160             # if not a standard glyph name, use $w as the value
161 0   0       $self->data()->{'uni'}->[$w] = (uniByName($self->data()->{'char'}->[$w]))||$w;
162 0           $self->data()->{'u2e'}->{$self->data()->{'uni'}->[$w]} = $w;
163             }
164 0           my @widths = ();
165 0           foreach my $w (@{$self->data()->{'char2'}}) {
  0            
166             # some .bdf files have a grid that is 10000 wide, not 1000. want to
167             # end up with a fraction (typically less than 1.0) of font size
168             # after scaling in FontMatrix (/1000).
169 0 0         if ($self->data()->{'wx'}->{$w->{'NAME'}} > 2500) {
170 0           $self->data()->{'wx'}->{$w->{'NAME'}} /= 10;
171             }
172 0 0         if ($self->data()->{'wx'}->{$w->{'NAME'}} == 0) {
173 0   0       $self->data()->{'wx'}->{$w->{'NAME'}} = $self->{'missingwidth'} || 100;
174             }
175 0           $widths[$w->{'ENCODING'}] = $self->data()->{'wx'}->{$w->{'NAME'}};
176 0           my @bbx = @{ $w->{'BBX'} };
  0            
177 0           my @BBX = @bbx;
178             # if no pattern (e.g., space) give a 0000 pattern to avoid pack problem
179 0 0         $w->{'hex'} = '0000' if !defined $w->{'hex'};
180              
181 0           my $char = PDFDict();
182             # [0] width 1000*fraction wide (approx) = aspect ratio
183             # [1] 0 y +/- move to next character?
184             # [2..3] lower left extent of glyph, can be to left of origin point
185             # [4..5] upper right extent of glyph + trailing space
186             # make sure width is at least 105% of max x
187 0 0         if ($widths[$w->{'ENCODING'}] < 1.05*($bbx[0]+$bbx[2])*$csizeH) {
188 0           $widths[$w->{'ENCODING'}] = int(0.5 + 1.05*($bbx[0]+$bbx[2])*$csizeH);
189             }
190 0           my $LLx = int($bbx[2]*$csizeH + 0.5);
191 0           my $LLy = int($bbx[3]*$csizeV + 0.5);
192 0           my $URx = int(($bbx[0]+$bbx[2])*$csizeH + 0.5);
193 0           my $URy = int(($bbx[1]+$bbx[3])*$csizeV + 0.5);
194 0           $char->{' stream'} = $widths[$w->{'ENCODING'}] . " 0 $LLx $LLy $URx $URy d1\n";
195 0           $char->{'Comment'} = PDFString("N='" . $w->{'NAME'} . "' C=" . $w->{'ENCODING'}, 'x');
196 0           $procs->{$w->{'NAME'}} = $char;
197 0           @bbx = map { $_ * 1000 / $self->data()->{'upm'} } @bbx;
  0            
198              
199             # Reader will save graphics state (q) and restore (Q) around each
200             # glyph's drawing commands
201 0 0         if ($opts{'style'} eq 'image') {
202             # note that each character presented as an image
203             # CAUTION: using this image code for a font doesn't seem to work
204             # well. block and dot look quite nice, so for now, use one of those.
205 0           my $stream = pack('H*', $w->{'hex'});
206 0           my $y = $BBX[1]; # vertical dimension of character (pixels)
207              
208 0 0         if ($y == 0) {
209 0           $char->{' stream'} .= " ";
210             } else {
211 0           my $x = 8 * length($stream) / $y;
212 0           my $img = qq|BI\n/Interpolate false/Decode [1 0]/H $y/W $x/BPC 1/CS/G\nID $stream\nEI|;
213 0           $procs->{$self->data()->{'char'}->[$w]} = $char;
214             # BBX.0 is character width in pixels, BBX.1 is height in pixels
215             # BBX.2 is offset to right in pixels, BBX.3 is offset up in pixels
216 0           $char->{' stream'} .= "q $BBX[0] 0 0 $BBX[1] $BBX[2] $BBX[3] cm\n$img\nQ";
217             }
218             # entered as Type 3 font character
219             # n 0 obj << /Length nn >> stream
220             # width 0 d0 # mils
221             # q xsize 0 0 ysize xoffset yoffset cm
222             # BI
223             # /Interpolate false/Decode[1 0]/H pixh/W pixw/BPC 1/CS/G
224             # ID binary_data stream
225             # EI
226             # Q
227             # endstream endobj
228            
229             } else {
230             # common code for dot and block styles
231 0 0         if ($BBX[1] == 0) {
232             # empty character, such as a space
233 0           $char->{' stream'} .= " ";
234             } else {
235 0           my @dots = (); # rows of pixels [0] at bottom (min y),
236             # each row is array of pixels across
237             # BBX[1] is number of chunks in 'hex', each 2, 4, 6 nybbles
238             # (not sure if can exceed 8 pixels across...)
239             # BBX[0] is width in pixels (8 to a byte) across 'hex'
240 0           my $bytesPerRow = int(($BBX[0]+7)/8); # 2 nybbles each
241 0           for (my $row=0; $row<$BBX[1]; $row++) {
242 0           unshift @dots, [ split //, substr(unpack('B*', pack('H*', substr($w->{'hex'}, $row*2, $bytesPerRow*2))), 0, $BBX[0]) ];
243             }
244             # dots[r][c] is 1 if want pixel there (0,0 at bottom/left)
245              
246 0           for (my $row=0; $row<$BBX[1]; $row++) {
247 0           for (my $col=0; $col<$BBX[0]; $col++) {
248 0 0         if (!$dots[$row][$col]) { next; }
  0            
249              
250 0 0         if ($opts{'style'} eq 'block') {
251             # TBD merge neighbors to form larger rectangles
252 0           $char->{' stream'} .= int(($col+$BBX[2])*$csizeH+0.5).' '.
253             int(($row+$BBX[3])*$csizeV+0.5).' '.
254             int($csizeH+0.5).' '.
255             int($csizeV+0.5).' '.
256             're f ';
257             } else {
258             # dots
259 0           $char->{' stream'} .= filled_circle(
260             ($col+$BBX[2]+0.5)*$csizeH, # Xc
261             ($row+$BBX[3]+0.5)*$csizeV, # Yc
262             $csizeH*$dot_ratio/2 ); # r
263             }
264             }
265 0           $char->{' stream'} .= "\n";
266             }
267             }
268             } # block and dot styles
269              
270 0           $pdf->new_obj($char);
271             # .notdef is treated as a space
272 0           $procs->{'.notdef'} = $procs->{$self->data()->{'char'}->[32]};
273 0           delete $procs->{''};
274             } # loop through all defined characters in BDF file
275              
276             # correct global fontbbox and output after seeing all glyph 'd1' LL UR limits
277 0           my @fbb = ( $self->fontbbox() );
278 0           $fbb[0] = $minX*$csizeH;
279 0           $fbb[1] = $minY*$csizeV;
280 0           $fbb[2] = $maxX*$csizeH;
281 0           $fbb[3] = $maxY*$csizeV;
282 0           $self->{'FontBBox'} = PDFArray(map { PDFNum($_) } @fbb );
  0            
283              
284 0   0       $self->{'Widths'} = PDFArray(map { PDFNum($widths[$_] || $self->{' data'}->{'missingwidth'} || 100) } ($first .. $last));
  0            
285 0           $self->data()->{'e2n'} = $self->data()->{'char'};
286 0           $self->data()->{'e2u'} = $self->data()->{'uni'};
287              
288 0           $self->data()->{'u2c'} = {};
289 0           $self->data()->{'u2e'} = {};
290 0           $self->data()->{'u2n'} = {};
291 0           $self->data()->{'n2c'} = {};
292 0           $self->data()->{'n2e'} = {};
293 0           $self->data()->{'n2u'} = {};
294              
295 0           foreach my $n (reverse 0 .. 255) {
296 0 0 0       $self->data()->{'n2c'}->{$self->data()->{'char'}->[$n] || '.notdef'} = $n unless defined $self->data()->{'n2c'}->{$self->data()->{'char'}->[$n] || '.notdef'};
      0        
297 0 0 0       $self->data()->{'n2e'}->{$self->data()->{'e2n'}->[$n] || '.notdef'} = $n unless defined $self->data()->{'n2e'}->{$self->data()->{'e2n'}->[$n] || '.notdef'};
      0        
298              
299 0 0 0       $self->data()->{'n2u'}->{$self->data()->{'e2n'}->[$n] || '.notdef'} = $self->data()->{'e2u'}->[$n] unless defined $self->data()->{'n2u'}->{$self->data()->{'e2n'}->[$n] || '.notdef'};
      0        
300 0 0 0       $self->data()->{'n2u'}->{$self->data()->{'char'}->[$n] || '.notdef'} = $self->data()->{'uni'}->[$n] unless defined $self->data()->{'n2u'}->{$self->data()->{'char'}->[$n] || '.notdef'};
      0        
301              
302 0 0         $self->data()->{'u2c'}->{$self->data()->{'uni'}->[$n]} = $n unless defined $self->data()->{'u2c'}->{$self->data()->{'uni'}->[$n]};
303 0 0         $self->data()->{'u2e'}->{$self->data()->{'e2u'}->[$n]} = $n unless defined $self->data()->{'u2e'}->{$self->data()->{'e2u'}->[$n]};
304              
305 0 0 0       $self->data()->{'u2n'}->{$self->data()->{'e2u'}->[$n]} = ($self->data()->{'e2n'}->[$n] || '.notdef') unless(defined $self->data()->{'u2n'}->{$self->data()->{'e2u'}->[$n]});
306 0 0 0       $self->data()->{'u2n'}->{$self->data()->{'uni'}->[$n]}=($self->data()->{'char'}->[$n] || '.notdef') unless(defined $self->data()->{'u2n'}->{$self->data()->{'uni'}->[$n]});
307             }
308              
309 0           return $self;
310             }
311              
312             sub readBDF {
313 0     0 0   my ($self, $file) = @_;
314 0           my $data = {};
315 0           $data->{'char'} = []; # ENCODING is NAME from char2
316 0           $data->{'char2'} = []; # NAME from STARTCHAR record, E_encoding# default
317             # hex from BITMAP records as one long string,
318             # can be empty (such as space x20)
319             # BBX arrayref from BBX record
320 0           $data->{'wx'} = {}; # width (by name) from char2 SWIDTH record
321              
322 0 0         if (! -e $file) {
323 0           die "BDF file='$file' not found.";
324             }
325 0 0         open(my $afmf, "<", $file) or die "Can't open the BDF file for $file";
326 0           local($/, $_) = ("\n", undef); # ensure correct $INPUT_RECORD_SEPARATOR
327 0           while ($_ = <$afmf>) {
328 0           chomp($_);
329 0 0         if (/^STARTCHAR/ .. /^ENDCHAR/) {
330 0 0         if (/^STARTCHAR\s+(\S+)/) { # start of one glyph
    0          
331 0           my $name = $1;
332 0           $name =~ s|^(\d+.*)$|X_$1|;
333 0           push @{$data->{'char2'}}, {'NAME' => $name};
  0            
334             } elsif (/^BITMAP/ .. /^ENDCHAR/) { # bitmap itself
335 0 0         next if(/^BITMAP/);
336 0 0         if (/^ENDCHAR/) { # done reading, finalize
337             # fallback NAME is E_<encoding #>
338 0   0       $data->{'char2'}->[-1]->{'NAME'} ||= 'E_'.$data->{'char2'}->[-1]->{'ENCODING'};
339 0           my $charName = $data->{'char2'}->[-1]->{'NAME'};
340             # char ENCODING is char2's NAME
341 0           $data->{'char'}->[$data->{'char2'}->[-1]->{'ENCODING'}] = $charName;
342             # width (2 element vector) from char2 SWIDTH
343 0           ($data->{'wx'}->{$charName}) = split(/\s+/, $data->{'char2'}->[-1]->{'SWIDTH'});
344             # bounding box (4 element vector) from char2 BBX
345 0           $data->{'char2'}->[-1]->{'BBX'} = [split(/\s+/, $data->{'char2'}->[-1]->{'BBX'})];
346             } else { # the bitmap data record appended
347 0           $data->{'char2'}->[-1]->{'hex'} .= $_;
348             }
349             } else {
350             # a few fields (records) here, just grab <name> <value> pairs
351 0 0         if (m|^(\S+)\s+(.+)$|) {
352 0           $data->{'char2'}->[-1]->{uc($1)} .= $2;
353             }
354             }
355             ## } elsif (/^STARTPROPERTIES/ .. /^ENDPROPERTIES/) { not implemented
356             } else {
357             # all sorts of random stuff here, STARTFONT, START/END
358             # PROPERTIES, etc. just grab anything that looks like a
359             # <name> <value> record
360 0 0         if (m|^(\S+)\s+(.+)$|) {
361 0           $data->{uc($1)} .= $2;
362             }
363             }
364             }
365 0           close($afmf);
366 0 0         unless (exists $data->{'wx'}->{'.notdef'}) {
367 0   0       $data->{'wx'}->{'.notdef'} = $data->{'missingwidth'} || 100;
368 0           $data->{'bbox'}{'.notdef'} = [0, 0, 0, 0];
369             }
370              
371 0           $data->{'fontname'} = "BdfF+" . pdfkey();
372 0           $data->{'apiname'} = $data->{'fontname'};
373 0           $data->{'flags'} = 34;
374             # initial value of fontbbox is FONTBOUNDINGBOX entry, e.g., 6 13 0 -3
375             # equals 6 columns, 13 rows, 0 min right offset, -3 min up offset
376 0           $data->{'fontbbox'} = [split(/\s+/, $data->{'FONTBOUNDINGBOX'})];
377             # upm e.g., 15 (if not set, rows-min up offset 13-(-3) = 16)
378             # I don't think this is screen px, but count of vertical elements in glyph
379 0   0       $data->{'upm'} = $data->{'PIXEL_SIZE'} || ($data->{'fontbbox'}->[1] - $data->{'fontbbox'}->[3]);
380             # not sure what this is trying to do. 1000/vertical cell count would be
381             # vertical (and horizontal) cell size, within 1000 grid? multiply cell
382             # locations by cell size to get grid locations?
383 0           @{$data->{'fontbbox'}} = map { int($_*1000/$data->{'upm'}) } @{$data->{'fontbbox'}};
  0            
  0            
  0            
384             # at this point, fontbbox is scaled cols/rows and min right/up. what goes
385             # in /FontBBox should be LL UR scaled values (including offsets)
386              
387 0           foreach my $n (0 .. 255) {
388 0   0       $data->{'char'}->[$n] ||= '.notdef';
389             }
390              
391 0   0       $data->{'uni'} ||= [];
392 0           foreach my $n (0 .. 255) {
393 0   0       $data->{'uni'}->[$n] = uniByName($data->{'char'}->[$n] || '.notdef') || 0;
394             }
395             $data->{'ascender'} = $data->{'RAW_ASCENT'}
396 0   0       || int($data->{'FONT_ASCENT'} * 1000 / $data->{'upm'});
397             $data->{'descender'} = $data->{'RAW_DESCENT'}
398 0   0       || int($data->{'FONT_DESCENT'} * 1000 / $data->{'upm'});
399              
400 0           $data->{'type'} = 'Type3';
401 0           $data->{'capheight'} = 1000;
402 0           $data->{'iscore'} = 0;
403 0           $data->{'issymbol'} = 0;
404 0           $data->{'isfixedpitch'} = 0;
405 0           $data->{'italicangle'} = 0;
406             $data->{'missingwidth'} = $data->{'AVERAGE_WIDTH'}
407             || int($data->{'FONT_AVERAGE_WIDTH'} * 1000 / $data->{'upm'})
408 0   0       || $data->{'RAW_AVERAGE_WIDTH'}
409             || 500;
410 0           $data->{'underlineposition'} = -200;
411 0           $data->{'underlinethickness'} = 10;
412             $data->{'xheight'} = $data->{'RAW_XHEIGHT'}
413             || int(($data->{'FONT_XHEIGHT'}||0) * 1000 / $data->{'upm'})
414 0   0       || int($data->{'ascender'} / 2);
415 0           $data->{'firstchar'} = 0;
416 0           $data->{'lastchar'} = 255;
417              
418 0           delete $data->{'wx'}->{''};
419              
420 0           return $data;
421             }
422              
423             # draw a filled circle centered at $Xc,$Yc, of radius $r
424             # returns string of PDF primitives
425             sub filled_circle {
426             # this algorithm from stackoverflow by Marius (questions/1960786)
427 0     0 0   my ($Xc, $Yc, $r) = @_;
428 0           my $out = ''; # output string
429              
430             # line thickness 2 x radius
431 0           $out .= " " . (2*$r) . " w";
432             # line cap round
433 0           $out .= " 1 J";
434             # draw 0-length line at center
435 0           $out .= " $Xc $Yc m $Xc $Yc l";
436             # stroke line
437 0           $out .= " S";
438              
439 0           return $out;
440             } # end filled_circle()
441              
442             1;
443              
444             __END__
445              
446             =head1 AUTHOR
447              
448             Alfred Reibenschuh, extensively rewritten by Phil Perry
449              
450             =cut