File Coverage

blib/lib/Font/PCF.pm
Criterion Covered Total %
statement 21 122 17.2
branch 0 56 0.0
condition 0 9 0.0
subroutine 7 16 43.7
pod 2 8 25.0
total 30 211 14.2


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2019 -- leonerd@leonerd.org.uk
5              
6             package Font::PCF;
7              
8 2     2   70782 use strict;
  2         12  
  2         51  
9 2     2   9 use warnings;
  2         3  
  2         48  
10 2     2   9 use base qw( IO::Handle::Packable );
  2         4  
  2         1069  
11              
12             our $VERSION = '0.01';
13              
14 2     2   20699 use List::Util qw( any first );
  2         3  
  2         185  
15 2     2   797 use PerlIO::gzip;
  2         1366  
  2         55  
16              
17 2     2   1012 use Struct::Dumb;
  2         2926  
  2         8  
18              
19             =head1 NAME
20              
21             C - read an X11 PCF font file
22              
23             =head1 SYNOPSIS
24              
25             use Font::PCF;
26              
27             my $font = Font::PCF->open( "/usr/share/fonts/X11/misc/9x15.pcf.gz" );
28              
29             my $glyph = $font->get_glyph_for_char( "A" );
30              
31             sub printbits {
32             my ( $bits ) = @_;
33             while( $bits ) {
34             print +( $bits & (1<<31) ) ? '#' : ' ';
35             $bits <<= 1;
36             }
37             print "\n";
38             }
39              
40             printbits $_ for $glyph->bitmap->@*;
41              
42             =head1 DESCRIPTION
43              
44             Instances of this class provide read access to the "PCF" format font files
45             that are typically found as part of an X11 installation.
46              
47             This module was written just to be sufficient for generating font bitmaps to
48             encode in microcontroller programs for display on OLED panels. It is possibly
49             useful for other use-cases as well, but may required more methods adding.
50              
51             =cut
52              
53             # See also
54             # http://fileformats.archiveteam.org/wiki/PCF
55             # https://fontforge.github.io/en-US/documentation/reference/pcf-format/
56              
57             struct Table => [qw( type format size offset )];
58              
59             struct Glyph => [qw( bitmap left_side_bearing right_side_bearing width ascent descent attrs name )];
60              
61             use constant {
62             # Table types
63 2         2953 PCF_PROPERTIES => (1<<0),
64             PCF_ACCELERATORS => (1<<1),
65             PCF_METRICS => (1<<2),
66             PCF_BITMAPS => (1<<3),
67             PCF_INK_METRICS => (1<<4),
68             PCF_BDF_ENCODINGS => (1<<5),
69             PCF_SWIDTHS => (1<<6),
70             PCF_GLYPH_NAMES => (1<<7),
71             PCF_BDF_ACCELERATORS => (1<<8),
72              
73             # Format types
74             PCF_DEFAULT_FORMAT => 0x00000000,
75             PCF_INKBOUNDS => 0x00000200,
76             PCF_ACCEL_W_INKBOUNDS => 0x00000100,
77             PCF_COMPRESSED_METRICS => 0x00000100,
78              
79             PCF_FORMAT_MASK => 0xFFFFFF00,
80              
81             # Format modifiers
82             PCF_GLYPH_PAD_MASK => (3<<0), # See the bitmap table for explanation
83             PCF_BYTE_MASK => (1<<2), # If set then Most Sig Byte First
84             PCF_BIT_MASK => (1<<3), # If set then Most Sig Bit First
85             PCF_SCAN_UNIT_MASK => (3<<4), # See the bitmap table for explanation
86 2     2   414 };
  2         4  
87              
88             =head1 CONSTRUCTOR
89              
90             =cut
91              
92             =head2 open
93              
94             $font = Font::PCF->open( $path )
95              
96             Opens the PCF file from the given path, and returns a new instance containing
97             the data from it. Throws an exception if an error occurs.
98              
99             =cut
100              
101             sub open
102             {
103 0     0 1   my $class = shift;
104 0           my ( $path, %opts ) = @_;
105              
106 0 0         $opts{gzip} = 1 if $path =~ m/\.gz$/;
107              
108 0 0         open my $self, $opts{gzip} ? "<:gzip" : "<", $path or
    0          
109             die "Cannot open font at $path - $!";
110              
111 0           bless $self, $class;
112              
113 0           $self->read_data;
114              
115 0           return $self;
116             }
117              
118             =head1 METHODS
119              
120             =cut
121              
122             sub read_data
123             {
124 0     0 0   my $self = shift;
125              
126 0           my ( $signature, $table_count ) = $self->unpack( "a4 i<" );
127 0 0         $signature eq "\x01fcp" or die "Invalid signature";
128              
129             my @tables = map {
130 0           Table( $self->unpack( "i< i< i< i<" ) )
  0            
131             } 1 .. $table_count;
132              
133 0           foreach my $table ( @tables ) {
134 0           my $type = $table->type;
135 0 0         if( $type == PCF_METRICS ) {
    0          
    0          
    0          
136 0           $self->read_metrics_table( $table );
137             }
138             elsif( $type == PCF_BITMAPS ) {
139 0           $self->read_bitmaps_table( $table );
140             }
141             elsif( $type == PCF_BDF_ENCODINGS ) {
142 0           $self->read_encodings_table( $table );
143             }
144             elsif( $type == PCF_GLYPH_NAMES ) {
145 0           $self->read_glyph_names_table( $table );
146             }
147             else {
148 0           my $size = 4 * int( ( $table->size + 3 ) / 4 );
149             print STDERR "TODO: Skipping table type $type of $size bytes\n" unless
150 0 0   0     any { $type == $_ } PCF_PROPERTIES, PCF_ACCELERATORS, PCF_INK_METRICS,
  0            
151             PCF_SWIDTHS, PCF_BDF_ACCELERATORS;
152 0           $self->read( my $tmp, $table->size );
153             }
154             }
155             }
156              
157             sub read_metrics_table
158             {
159 0     0 0   my $self = shift;
160 0           my ( $table ) = @_;
161              
162 0           my ( $format ) = $self->unpack( "i<" );
163 0 0         $format == $table->format or die "Expected format repeated\n";
164              
165 0 0         my $end = $table->format & PCF_BYTE_MASK ? ">" : "<";
166 0           my $compressed = ( $format & PCF_COMPRESSED_METRICS );
167              
168 0 0         my $count = $self->unpack( $compressed ? "s${end}" : "i${end}" );
169              
170 0           foreach my $index ( 0 .. $count-1 ) {
171 0           my @fields;
172 0 0         if( $compressed ) {
173 0           @fields = $self->unpack( "C5" );
174 0           $_ -= 0x80 for @fields;
175 0           push @fields, 0;
176             }
177             else {
178 0           @fields = $self->unpack( "s${end}5 S${end}" );
179             }
180              
181 0           my $glyph = $self->get_glyph( $index );
182              
183 0           $glyph->left_side_bearing = shift @fields;
184 0           $glyph->right_side_bearing = shift @fields;
185 0           $glyph->width = shift @fields;
186 0           $glyph->ascent = shift @fields;
187 0           $glyph->descent = shift @fields;
188 0           $glyph->attrs = shift @fields;
189             }
190              
191             # Pad to a multiple of 4 bytes
192 0 0         my $total = $compressed ? 2 + $count * 5 : 4 + $count * 10;
193 0 0         $self->read( my $tmp, 4 - ( $total % 4 ) ) if $total % 4;
194             }
195              
196             sub read_bitmaps_table
197             {
198 0     0 0   my $self = shift;
199 0           my ( $table ) = @_;
200              
201 0 0         ( $table->format & PCF_FORMAT_MASK ) == PCF_DEFAULT_FORMAT or
202             die "Expected PCF_BITMAPS to be in PCF_DEFAULT_FORMAT\n";
203              
204 0 0         my $end = $table->format & PCF_BYTE_MASK ? ">" : "<";
205              
206 0           my ( $format, $glyph_count ) = $self->unpack( "i< i${end}");
207 0 0         $format == $table->format or die "Expected format repeated\n";
208             # offsets
209 0           my @offsets = $self->unpack( "i${end}${glyph_count}" );
210              
211 0           my @sizes = $self->unpack( "i${end}4" );
212 0           my $size = $sizes[ $table->format & PCF_GLYPH_PAD_MASK ];
213              
214 0           my $scanunits = ( $table->format & PCF_SCAN_UNIT_MASK ) >> 4;
215              
216             # Continue reading chunks of data until we reach the next offset, add
217             # data so far to the previous glyph
218 0           my $offset = 0;
219 0           my $index = 0;
220 0           my $bitmap;
221 0           while( $offset < $size ) {
222 0 0 0       if( @offsets and $offset == $offsets[0] ) {
223 0           my $glyph = $self->get_glyph( $index++ );
224 0           $bitmap = $glyph->bitmap;
225 0           shift @offsets;
226             }
227              
228 0           push @$bitmap, $self->unpack( "I${end}" );
229 0           $offset += 4;
230             }
231             }
232              
233             sub read_encodings_table
234             {
235 0     0 0   my $self = shift;
236 0           my ( $table ) = @_;
237              
238 0 0         ( $table->format & PCF_FORMAT_MASK ) == PCF_DEFAULT_FORMAT or
239             die "Expected PCF_BITMAPS to be in PCF_DEFAULT_FORMAT\n";
240              
241 0 0         my $end = $table->format & PCF_BYTE_MASK ? ">" : "<";
242              
243 0           my ( $format, $min2, $max2, $min1, $max1, $default ) =
244             $self->unpack( "i< s$end s$end s$end s$end s$end" );
245 0 0         $format == $table->format or die "Expected format repeated\n";
246              
247 0           my $indices_count = ( $max2 - $min2 + 1 ) * ( $max1 - $min1 + 1 );
248              
249 0           my @indices = $self->unpack( "s${end}${indices_count}" );
250              
251 0           ${*$self}{encoding_to_glyph} = \@indices;
  0            
252              
253             # Pad to a multiple of 4 bytes
254             # Header was 2 bytes over so we're 2 off if even number of indices
255 0 0         $self->read( my $tmp, 2 ) if ( $indices_count % 2 ) == 0;
256             }
257              
258             sub read_glyph_names_table
259             {
260 0     0 0   my $self = shift;
261 0           my ( $table ) = @_;
262              
263 0 0         ( $table->format & PCF_FORMAT_MASK ) == PCF_DEFAULT_FORMAT or
264             die "Expected PCF_BITMAPS to be in PCF_DEFAULT_FORMAT\n";
265              
266 0 0         my $end = $table->format & PCF_BYTE_MASK ? ">" : "<";
267              
268 0           my ( $format, $glyph_count ) = $self->unpack( "i< i${end}");
269 0 0         $format == $table->format or die "Expected format repeated\n";
270              
271 0           my @offsets = $self->unpack( "i${end}${glyph_count}" );
272              
273 0           my $strlen = $self->unpack( "i${end}" );
274              
275             # Read this as one big string and cut it by @offsets
276 0           $self->read( my $names, $strlen );
277              
278 0           foreach my $index ( 0 .. $#offsets ) {
279 0           my $offset = $offsets[$index];
280 0   0       my $next_offset = $offsets[$index + 1] // $strlen;
281              
282             # Each glyph name ends with a \0 in the string data
283              
284 0           $self->get_glyph( $index )->name = substr( $names, $offset, $next_offset - $offset - 1 );
285             }
286              
287             # Pad to a multiple of 4 bytes
288 0 0         $self->read( my $tmp, 4 - ( $strlen % 4 ) ) if $strlen % 4;
289             }
290              
291             =head2 get_glyph_for_char
292              
293             $glyph = $font->get_glyph_for_char( $char )
294              
295             Returns a Glyph struct representing the unicode character; given as a
296             character string.
297              
298             =cut
299              
300             sub get_glyph_for_char
301             {
302 0     0 1   my $self = shift;
303 0           my ( $char ) = @_;
304              
305 0           my $index = ${*$self}{encoding_to_glyph}[ ord $char ];
  0            
306 0 0         $index == -1 and
307             die "Unmapped character\n";
308              
309 0           return $self->get_glyph( $index );
310             }
311              
312             sub get_glyph
313             {
314 0     0 0   my $self = shift;
315 0           my ( $index ) = @_;
316              
317 0   0       return ${*$self}{glyphs}[$index] //= Glyph( [], (undef) x 7 );
  0            
318             }
319              
320             =head1 GLYPH STRUCTURE
321              
322             Each glyph structure returned by L has the following
323             methods:
324              
325             =head2 bitmap
326              
327             @bits = $glyph->bitmap->@*
328              
329             Returns a reference to the array containing lines of the bitmap for this
330             character. Each line is represented by an integer, where high bits represent
331             set pixels. The MSB is the leftmost pixel of the character.
332              
333             =head2 width
334              
335             $pixels = $glyph->width
336              
337             The total number of pixels per line stored in the bitmaps.
338              
339             =head2 left_side_bearing
340              
341             =head2 right_side_bearing
342              
343             $pixels = $glyph->left_side_bearing
344              
345             $pixels = $glyph->right_side_bearing
346              
347             The number of pixels of bearing (that is, blank pixels of space) to either
348             side of the character data.
349              
350             =head2 ascent
351              
352             =head2 descent
353              
354             $pixels = $glyph->ascent
355              
356             $pixels = $glyph->descent
357              
358             The number of pixels above and below the glyph.
359              
360             =head2 name
361              
362             $str = $glyph->name
363              
364             The PostScript name for the glyph
365              
366             =cut
367              
368             =head1 AUTHOR
369              
370             Paul Evans
371              
372             =cut
373              
374             0x55AA;