cr100/mkfont/adafruit_bitmap_font/pcf.py
Jeff Epler 3554999f0a Full font
The conversion process was so nasty & ad hoc that I'm committing the
generated font file.
2024-07-24 18:20:17 -05:00

412 lines
13 KiB
Python

# SPDX-FileCopyrightText: 2020 Jeff Epler for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_bitmap_font.pcf`
====================================================
Loads PCF format fonts.
* Author(s): Jeff Epler
Implementation Notes
--------------------
**Hardware:**
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the supported boards:
https://github.com/adafruit/circuitpython/releases
"""
try:
from typing import Union, Tuple, Iterator, Iterable
from io import FileIO
from displayio import Bitmap as displayioBitmap
except ImportError:
pass
from collections import namedtuple
import gc
import struct
from micropython import const
from fontio import Glyph
from .glyph_cache import GlyphCache
try:
from bitmaptools import readinto as _bitmap_readinto
except ImportError:
_bitmap_readinto = None # pylint: disable=invalid-name
_PCF_PROPERTIES = const(1 << 0)
_PCF_ACCELERATORS = const(1 << 1)
_PCF_METRICS = const(1 << 2)
_PCF_BITMAPS = const(1 << 3)
_PCF_INK_METRICS = const(1 << 4)
_PCF_BDF_ENCODINGS = const(1 << 5)
_PCF_SWIDTHS = const(1 << 6)
_PCF_GLYPH_NAMES = const(1 << 7)
_PCF_BDF_ACCELERATORS = const(1 << 8)
_PCF_DEFAULT_FORMAT = const(0x00000000)
_PCF_ACCEL_W_INKBOUNDS = const(0x00000100)
_PCF_COMPRESSED_METRICS = const(0x00000100)
_PCF_GLYPH_PAD_MASK = const(3 << 0) # See the bitmap table for explanation */
_PCF_BYTE_MASK = const(1 << 2) # If set then Most Sig Byte First */
_PCF_BIT_MASK = const(1 << 3) # If set then Most Sig Bit First */
_PCF_SCAN_UNIT_MASK = const(3 << 4)
# https://fontforge.org/docs/techref/pcf-format.html
Table = namedtuple("Table", ("format", "size", "offset"))
Metrics = namedtuple(
"Metrics",
(
"left_side_bearing",
"right_side_bearing",
"character_width",
"character_ascent",
"character_descent",
"character_attributes",
),
)
Accelerators = namedtuple(
"Accelerators",
(
"no_overlap",
"constant_metrics",
"terminal_font",
"constant_width",
"ink_inside",
"ink_metrics",
"draw_direction",
"font_ascent",
"font_descent",
"max_overlap",
"minbounds",
"maxbounds",
"ink_minbounds",
"ink_maxbounds",
),
)
Encoding = namedtuple(
"Encoding", ("min_byte2", "max_byte2", "min_byte1", "max_byte1", "default_char")
)
Bitmap = namedtuple("Bitmap", ("glyph_count", "bitmap_sizes"))
class PCF(GlyphCache):
"""Loads glyphs from a PCF file in the given bitmap_class."""
def __init__(self, f: FileIO, bitmap_class: displayioBitmap) -> None:
super().__init__()
self.file = f
self.name = f
f.seek(0)
self.buffer = bytearray(1)
self.bitmap_class = bitmap_class
_, table_count = self._read("<4sI")
self.tables = {}
for _ in range(table_count):
type_, format_, size, offset = self._read("<IIII")
self.tables[type_] = Table(format_, size, offset)
bitmap_format = self.tables[_PCF_BITMAPS].format
if bitmap_format != 0xE:
raise NotImplementedError("Unsupported format %s" % bitmap_format)
self._accel = self._read_accelerator_tables()
self._encoding = self._read_encoding_table()
self._bitmaps = self._read_bitmap_table()
self._ascent = self._accel.font_ascent
self._descent = self._accel.font_descent
minbounds = self._accel.ink_minbounds
maxbounds = self._accel.ink_maxbounds
width = maxbounds.right_side_bearing - minbounds.left_side_bearing
height = maxbounds.character_ascent + maxbounds.character_descent
self._bounding_box = (
width,
height,
minbounds.left_side_bearing,
-maxbounds.character_descent,
)
@property
def ascent(self) -> int:
"""The number of pixels above the baseline of a typical ascender"""
return self._ascent
@property
def descent(self) -> int:
"""The number of pixels below the baseline of a typical descender"""
return self._descent
def get_bounding_box(self) -> Tuple[int, int, int, int]:
"""Return the maximum glyph size as a 4-tuple of: width, height, x_offset, y_offset"""
return self._bounding_box
def _read(self, format_: str) -> Tuple:
size = struct.calcsize(format_)
if size != len(self.buffer):
self.buffer = bytearray(size)
self.file.readinto(self.buffer)
return struct.unpack_from(format_, self.buffer)
def _seek_table(self, table: Table) -> int:
self.file.seek(table.offset)
(format_,) = self._read("<I")
if format_ & _PCF_BYTE_MASK == 0:
raise RuntimeError("Only big endian supported")
return format_
def _read_encoding_table(self) -> Encoding:
encoding = self.tables[_PCF_BDF_ENCODINGS]
self._seek_table(encoding)
return Encoding(*self._read(">hhhhh"))
def _read_bitmap_table(self) -> Bitmap:
bitmaps = self.tables[_PCF_BITMAPS]
format_ = self._seek_table(bitmaps)
(glyph_count,) = self._read(">I")
self.file.seek(bitmaps.offset + 8 + 4 * glyph_count)
bitmap_sizes = self._read(">4I")
return Bitmap(glyph_count, bitmap_sizes[format_ & 3])
def _read_metrics(self, compressed_metrics: bool) -> Metrics:
if compressed_metrics:
(
left_side_bearing,
right_side_bearing,
character_width,
character_ascent,
character_descent,
) = self._read("5B")
left_side_bearing -= 0x80
right_side_bearing -= 0x80
character_width -= 0x80
character_ascent -= 0x80
character_descent -= 0x80
attributes = 0
else:
(
left_side_bearing,
right_side_bearing,
character_width,
character_ascent,
character_descent,
attributes,
) = self._read(">5hH")
return Metrics(
left_side_bearing,
right_side_bearing,
character_width,
character_ascent,
character_descent,
attributes,
)
def _read_accelerator_tables(self) -> Accelerators:
# pylint: disable=too-many-locals
accelerators = self.tables.get(_PCF_BDF_ACCELERATORS)
if not accelerators:
accelerators = self.tables.get(_PCF_ACCELERATORS)
if not accelerators:
raise RuntimeError("Accelerator table missing")
format_ = self._seek_table(accelerators)
has_inkbounds = format_ & _PCF_ACCEL_W_INKBOUNDS
(
no_overlap,
constant_metrics,
terminal_font,
constant_width,
ink_inside,
ink_metrics,
draw_direction,
_,
font_ascent,
font_descent,
max_overlap,
) = self._read(">BBBBBBBBIII")
minbounds = self._read_metrics(False)
maxbounds = self._read_metrics(False)
if has_inkbounds:
ink_minbounds = self._read_metrics(False)
ink_maxbounds = self._read_metrics(False)
else:
ink_minbounds = minbounds
ink_maxbounds = maxbounds
return Accelerators(
no_overlap,
constant_metrics,
terminal_font,
constant_width,
ink_inside,
ink_metrics,
draw_direction,
font_ascent,
font_descent,
max_overlap,
minbounds,
maxbounds,
ink_minbounds,
ink_maxbounds,
)
def _read_properties(self) -> Iterator[Tuple[bytes, Union[bytes, int]]]:
property_table_offset = self.tables[_PCF_PROPERTIES]["offset"]
self.file.seek(property_table_offset)
(format_,) = self._read("<I")
if format_ & _PCF_BYTE_MASK == 0:
raise RuntimeError("Only big endian supported")
(nprops,) = self._read(">I")
self.file.seek(property_table_offset + 8 + 9 * nprops)
pos = self.file.tell()
if pos % 4 > 0:
self.file.read(4 - pos % 4)
(string_size,) = self._read(">I")
strings = self.file.read(string_size)
string_map = {}
i = 0
for value in strings.split(b"\x00"):
string_map[i] = value
i += len(value) + 1
self.file.seek(property_table_offset + 8)
for _ in range(nprops):
name_offset, is_string_prop, value = self._read(">IBI")
if is_string_prop:
yield (string_map[name_offset], string_map[value])
else:
yield (string_map[name_offset], value)
def load_glyphs(self, code_points: Union[int, str, Iterable[int]]) -> None:
# pylint: disable=too-many-statements,too-many-branches,too-many-nested-blocks,too-many-locals
if isinstance(code_points, int):
code_points = (code_points,)
elif isinstance(code_points, str):
code_points = [ord(c) for c in code_points]
code_points = sorted(
c for c in code_points if self._glyphs.get(c, None) is None
)
if not code_points:
return
indices_offset = self.tables[_PCF_BDF_ENCODINGS].offset + 14
bitmap_offset_offsets = self.tables[_PCF_BITMAPS].offset + 8
first_bitmap_offset = self.tables[_PCF_BITMAPS].offset + 4 * (
6 + self._bitmaps.glyph_count
)
metrics_compressed = self.tables[_PCF_METRICS].format & _PCF_COMPRESSED_METRICS
first_metric_offset = self.tables[_PCF_METRICS].offset + (
6 if metrics_compressed else 8
)
metrics_size = 5 if metrics_compressed else 12
# These will each _tend to be_ forward reads in the file, at least
# sometimes we'll benefit from oofatfs's 512 byte cache and avoid
# excess reads
indices = [None] * len(code_points)
for i, code_point in enumerate(code_points):
enc1 = (code_point >> 8) & 0xFF
enc2 = code_point & 0xFF
if enc1 < self._encoding.min_byte1 or enc1 > self._encoding.max_byte1:
continue
if enc2 < self._encoding.min_byte2 or enc2 > self._encoding.max_byte2:
continue
encoding_idx = (
(enc1 - self._encoding.min_byte1)
* (self._encoding.max_byte2 - self._encoding.min_byte2 + 1)
+ enc2
- self._encoding.min_byte2
)
self.file.seek(indices_offset + 2 * encoding_idx)
(glyph_idx,) = self._read(">H")
if glyph_idx != 65535:
indices[i] = glyph_idx
all_metrics = [None] * len(code_points)
for i, code_point in enumerate(code_points):
index = indices[i]
if index is None:
continue
self.file.seek(first_metric_offset + metrics_size * index)
all_metrics[i] = self._read_metrics(metrics_compressed)
bitmap_offsets = [None] * len(code_points)
for i, code_point in enumerate(code_points):
index = indices[i]
if index is None:
continue
self.file.seek(bitmap_offset_offsets + 4 * index)
(bitmap_offset,) = self._read(">I")
bitmap_offsets[i] = bitmap_offset
# Batch creation of glyphs and bitmaps so that we need only gc.collect
# once
gc.collect()
bitmaps = [None] * len(code_points)
for i in range(len(all_metrics)): # pylint: disable=consider-using-enumerate
metrics = all_metrics[i]
if metrics is not None:
width = metrics.right_side_bearing - metrics.left_side_bearing
height = metrics.character_ascent + metrics.character_descent
bitmap = bitmaps[i] = self.bitmap_class(width, height, 2)
self._glyphs[code_points[i]] = Glyph(
bitmap,
0,
width,
height,
metrics.left_side_bearing,
-metrics.character_descent,
metrics.character_width,
0,
)
for i, code_point in enumerate(code_points):
metrics = all_metrics[i]
if metrics is None:
continue
self.file.seek(first_bitmap_offset + bitmap_offsets[i])
width = metrics.right_side_bearing - metrics.left_side_bearing
height = metrics.character_ascent + metrics.character_descent
bitmap = bitmaps[i]
if _bitmap_readinto:
_bitmap_readinto(
bitmap,
self.file,
bits_per_pixel=1,
element_size=4,
reverse_pixels_in_element=True,
)
else:
words_per_row = (width + 31) // 32
buf = bytearray(4 * words_per_row)
start = 0
for _ in range(height):
self.file.readinto(buf)
for k in range(width):
if buf[k // 8] & (128 >> (k % 8)):
bitmap[start + k] = 1
start += width