File Coverage

blib/lib/Font/PCF.pm
Criterion Covered Total %
statement 29 170 17.0
branch 0 56 0.0
condition 0 9 0.0
subroutine 10 31 32.2
pod 2 8 25.0
total 41 274 14.9


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