192 lines
5.5 KiB
Python

'''
Python lib for reading PF2 font files: http://grub.gibibit.com/New_font_format
No rights reserved.
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
Don't forget to see how it's used in `text_print.py`
'''
import io
from typing import Dict, Tuple
def uint32be(b: bytes):
'Translate 4 bytes as unsigned big-endian 32-bit int'
return (
(b[0] << 24) +
(b[1] << 16) +
(b[2] << 8) +
b[3]
)
def int32be(b: bytes):
'Translate 4 bytes as signed big-endian 32-bit int'
u = uint32be(b)
return u - ((u >> 31 & 0b1) << 32)
def uint16be(b: bytes):
'Translate 2 bytes as big-endian unsigned 16-bit int'
return (b[0] << 8) + b[1]
def int16be(b: bytes):
'Translate 2 bytes as big-endian signed 16-bit int'
u = uint16be(b)
return u - ((u >> 15 & 0b1) << 16)
class Character():
'A PF2 character'
width: int
height: int
x_offset: int
y_offset: int
device_width: int
bitmap_data: bytes
def get_bit(self, x, y):
'Get the bit at (x, y) of this character\'s raster glyph'
char_byte = (self.width * y + x) // 8
char_bit = 7 - (self.width * y + x) % 8
return self.bitmap_data[char_byte] & (0b1 << char_bit)
class PF2():
'The PF2 class, for serializing a PF2 font file'
is_pf2: bool
'Sets to false if the read file is not PF2 font file'
missing_character_code: int
in_memory: bool
font_name: str
family: str
weight: str
slant: str
point_size: int
max_width: int
max_height: int
ascent: int
descent: int
character_index: Dict[int, Tuple[int, int]]
data_offset: int
data_io: io.BufferedIOBase
def __init__(self, path='font.pf2', *, read_to_mem=True, missing_character: str='?'):
self.missing_character_code = ord(missing_character)
self.in_memory = read_to_mem
file = open(path, 'rb')
if read_to_mem:
self.data_io = io.BytesIO(file.read())
file.close()
file = self.data_io
self.is_pf2 = (file.read(12) == b'FILE\x00\x00\x00\x04PFF2')
if not self.is_pf2:
return
while True:
name = file.read(4)
data_length = int32be(file.read(4))
if name == b'CHIX':
self.character_index = {}
for _ in range(data_length // (4 + 1 + 4)):
code_point = int32be(file.read(4))
compression = file.read(1)[0]
offset = int32be(file.read(4))
self.character_index[code_point] = (
compression, offset
)
continue
elif name == b'DATA':
file.seek(0)
break
data = file.read(data_length)
if name == b'NAME':
self.font_name = data
elif name == b'FAMI':
self.family = data
elif name == b'WEIG':
self.weight = data
elif name == b'SLAN':
self.slant = data
elif name == b'PTSZ':
self.point_size = uint16be(data)
elif name == b'MAXW':
self.max_width = uint16be(data)
elif name == b'MAXH':
self.max_height = uint16be(data)
elif name == b'ASCE':
self.ascent = uint16be(data)
elif name == b'DESC':
self.descent = uint16be(data)
def get_char(self, char: str):
'Get a character, returning a `Character` instance'
char_point = ord(char)
info = self.character_index.get(char_point)
if info is None:
info = self.character_index[self.missing_character_code]
_compression, offset = info
data = self.data_io
data.seek(offset)
char = Character()
char.width = uint16be(data.read(2))
char.height = uint16be(data.read(2))
char.x_offset = int16be(data.read(2))
char.y_offset = int16be(data.read(2))
char.device_width = int16be(data.read(2))
char.bitmap_data = data.read(
(char.width * char.height + 7) // 8
)
return char
__getitem__ = get_char
def __del__(self):
self.data_io.close()
class CharacterS(Character):
'A "scaled" character'
scale: int = 1
def get_bit(self, x, y):
'Get the bit at (x, y) of this character\'s raster glyph'
scale = self.scale
width = self.width // scale
x //= scale
y //= scale
char_byte = (width * y + x) // 8
char_bit = 7 - (width * y + x) % 8
return (self.bitmap_data[char_byte] &
(0b1 << char_bit)) >> char_bit
class PF2S(PF2):
'PF2 class with glyph scaling support'
scale: int = 1
def __init__(self, *args, scale: int=1, **kwargs):
super().__init__(*args, **kwargs)
self.scale = scale
self.point_size *= scale
self.max_width *= scale
self.max_height *= scale
self.ascent *= scale
self.descent *= scale
def get_char(self, char):
scale = self.scale
char = super().get_char(char)
chars = CharacterS()
chars.scale = scale
chars.width = char.width * scale
chars.height = char.height * scale
chars.device_width = char.device_width * scale
chars.x_offset = char.x_offset * scale
chars.y_offset = char.y_offset * scale
chars.bitmap_data = char.bitmap_data
return chars
__getitem__ = get_char