Skip to content

Commit 9165015

Browse files
committed
Add support for LVGL binary font format
1 parent 03d935b commit 9165015

File tree

4 files changed

+229
-1
lines changed

4 files changed

+229
-1
lines changed

adafruit_bitmap_font/bitmap_font.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from . import bdf
2929
from . import pcf
3030
from . import ttf
31+
from . import lvfontbin
3132
except ImportError:
3233
pass
3334

@@ -37,7 +38,7 @@
3738

3839
def load_font(
3940
filename: str, bitmap: Optional[Bitmap] = None
40-
) -> Union[bdf.BDF, pcf.PCF, ttf.TTF]:
41+
) -> Union[bdf.BDF, pcf.PCF, ttf.TTF, lvfontbin.LVGLFont]:
4142
"""Loads a font file. Returns None if unsupported."""
4243
# pylint: disable=import-outside-toplevel, redefined-outer-name, consider-using-with
4344
if not bitmap:
@@ -59,4 +60,12 @@ def load_font(
5960

6061
return ttf.TTF(font_file, bitmap)
6162

63+
# The LVGL file starts with the size of the 'head' section. It hasn't changed in five years so
64+
# we can treat it like a magic number.
65+
LVGL_HEADER_SIZE = b"\x30\x00\x00\x00"
66+
if (filename.endswith("bin") or filename.endswith("lvfontbin")) and first_four == LVGL_HEADER_SIZE:
67+
from . import lvfontbin
68+
69+
return lvfontbin.LVGLFont(font_file, bitmap)
70+
6271
raise ValueError("Unknown magic number %r" % first_four)

adafruit_bitmap_font/lvfontbin.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# SPDX-FileCopyrightText: 2025 Scott Shawcroft for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""
6+
`adafruit_bitmap_font.lvfontbin`
7+
====================================================
8+
9+
Loads binary LVGL format fonts.
10+
11+
* Author(s): Scott Shawcroft
12+
13+
Implementation Notes
14+
--------------------
15+
16+
**Hardware:**
17+
18+
**Software and Dependencies:**
19+
20+
* Adafruit CircuitPython firmware for the supported boards:
21+
https://github.com/adafruit/circuitpython/releases
22+
23+
"""
24+
25+
import struct
26+
27+
try:
28+
from io import FileIO
29+
from typing import Union, Iterable, Tuple
30+
except ImportError:
31+
pass
32+
33+
from fontio import Glyph
34+
from .glyph_cache import GlyphCache
35+
36+
class LVGLFont(GlyphCache):
37+
def __init__(self, f: FileIO, bitmap_class=None):
38+
super().__init__()
39+
f.seek(0)
40+
self.file = f
41+
self.bitmap_class = bitmap_class
42+
# Initialize default values for bounding box
43+
self._width = None
44+
self._height = None
45+
self._x_offset = 0
46+
self._y_offset = 0
47+
48+
while True:
49+
buffer = f.read(4)
50+
if len(buffer) < 4:
51+
break
52+
section_size = struct.unpack('<I', buffer)[0]
53+
if section_size == 0:
54+
break
55+
table_marker = f.read(4)
56+
section_start = f.tell()
57+
remaining_section = f.read(section_size - 8)
58+
if table_marker == b'head':
59+
self._load_head(remaining_section)
60+
# Set bounding box based on font metrics from head section
61+
self._width = self._default_advance_width
62+
self._height = self._font_size
63+
self._x_offset = 0
64+
self._y_offset = self._descent
65+
elif table_marker == b'cmap':
66+
self._load_cmap(remaining_section)
67+
elif table_marker == b'loca':
68+
self._max_cid = struct.unpack('<I', remaining_section[0:4])[0]
69+
self._loca_start = section_start + 4
70+
elif table_marker == b'glyf':
71+
self._glyf_start = section_start - 8
72+
73+
def _load_head(self, data):
74+
self._version = struct.unpack('<I', data[0:4])[0]
75+
(self._font_size, self._ascent, self._descent, self._typo_ascent,
76+
self._typo_descent, self._line_gap, self._min_y, self._max_y,
77+
self._default_advance_width, self._kerning_scale) = struct.unpack(
78+
'<HHhHhHHHHH', data[6:26])
79+
self._index_to_loc_format = data[26]
80+
self._glyph_id_format = data[27]
81+
self._advance_format = data[28]
82+
self._bits_per_pixel = data[29]
83+
self._glyph_bbox_xy_bits = data[30]
84+
self._glyph_bbox_wh_bits = data[31]
85+
self._glyph_advance_bits = data[32]
86+
self._glyph_header_bits = self._glyph_advance_bits + 2 * self._glyph_bbox_xy_bits + 2 * self._glyph_bbox_wh_bits
87+
self._glyph_header_bytes = (self._glyph_header_bits + 7) // 8
88+
self._compression_alg = data[33]
89+
self._subpixel_rendering = data[34]
90+
91+
def _load_cmap(self, data):
92+
data = memoryview(data)
93+
subtable_count = struct.unpack('<I', data[0:4])[0]
94+
self._cmap_tiny = []
95+
for i in range(subtable_count):
96+
subtable_header = data[4 + 16 * i:4 + 16 * (i + 1)]
97+
(data_offset, range_start, range_length, glyph_offset,
98+
data_entries_count) = struct.unpack('<IIHHH', subtable_header[:14])
99+
format_type = subtable_header[14]
100+
# print(f"Subtable {i}: data_offset {data_offset:x} {range_start:x} {range_length:x} glyph_offset {glyph_offset:x} data_entries_count {data_entries_count} format_type {format_type}")
101+
102+
if format_type != 2:
103+
raise RuntimeError(f"Unsupported cmap format {format_type}")
104+
105+
self._cmap_tiny.append((range_start, range_start + range_length, glyph_offset))
106+
107+
@property
108+
def ascent(self) -> int:
109+
"""The number of pixels above the baseline of a typical ascender"""
110+
return self._ascent
111+
112+
@property
113+
def descent(self) -> int:
114+
"""The number of pixels below the baseline of a typical descender"""
115+
return self._descent
116+
117+
def get_bounding_box(self) -> tuple[int, int, int, int]:
118+
"""Return the maximum glyph size as a 4-tuple of: width, height, x_offset, y_offset"""
119+
return (self._width, self._height, self._x_offset, self._y_offset)
120+
121+
def _seek(self, offset):
122+
self.file.seek(offset)
123+
self._byte = 0
124+
self._remaining_bits = 0
125+
126+
def _read_bits(self, num_bits):
127+
result = 0
128+
needed_bits = num_bits
129+
while needed_bits > 0:
130+
if self._remaining_bits == 0:
131+
self._byte = self.file.read(1)[0]
132+
self._remaining_bits = 8
133+
available_bits = min(needed_bits, self._remaining_bits)
134+
result = (result << available_bits) | (self._byte >> (8 - available_bits))
135+
self._byte <<= available_bits
136+
self._byte &= 0xff
137+
self._remaining_bits -= available_bits
138+
needed_bits -= available_bits
139+
return result
140+
141+
def load_glyphs(self, code_points: Union[int, str, Iterable[int]]) -> None:
142+
# pylint: disable=too-many-statements,too-many-branches,too-many-nested-blocks,too-many-locals
143+
if isinstance(code_points, int):
144+
code_points = (code_points,)
145+
elif isinstance(code_points, str):
146+
code_points = [ord(c) for c in code_points]
147+
148+
# Only load glyphs that aren't already cached
149+
code_points = sorted(
150+
c for c in code_points if self._glyphs.get(c, None) is None
151+
)
152+
if not code_points:
153+
return
154+
155+
for code_point in code_points:
156+
# Find character ID in the cmap table
157+
cid = None
158+
for start, end, offset in self._cmap_tiny:
159+
if start <= code_point < end:
160+
cid = offset + (code_point - start)
161+
break
162+
163+
if cid is None or cid >= self._max_cid:
164+
self._glyphs[code_point] = None
165+
continue
166+
167+
offset_length = 4 if self._index_to_loc_format == 1 else 2
168+
169+
# Get the glyph offset from the location table
170+
self._seek(self._loca_start + cid * offset_length)
171+
glyph_offset = struct.unpack('<I' if offset_length == 4 else '<H', self.file.read(offset_length))[0]
172+
173+
# Read glyph header data
174+
self._seek(self._glyf_start + glyph_offset)
175+
glyph_advance = self._read_bits(self._glyph_advance_bits)
176+
177+
# Read and convert signed bbox_x and bbox_y
178+
bbox_x = self._read_bits(self._glyph_bbox_xy_bits)
179+
# Convert to signed value if needed (using two's complement)
180+
if (bbox_x & (1 << (self._glyph_bbox_xy_bits - 1))):
181+
bbox_x = bbox_x - (1 << self._glyph_bbox_xy_bits)
182+
183+
bbox_y = self._read_bits(self._glyph_bbox_xy_bits)
184+
# Convert to signed value if needed (using two's complement)
185+
if (bbox_y & (1 << (self._glyph_bbox_xy_bits - 1))):
186+
bbox_y = bbox_y - (1 << self._glyph_bbox_xy_bits)
187+
188+
bbox_w = self._read_bits(self._glyph_bbox_wh_bits)
189+
bbox_h = self._read_bits(self._glyph_bbox_wh_bits)
190+
191+
# Create bitmap for the glyph
192+
bitmap = self.bitmap_class(bbox_w, bbox_h, 2)
193+
194+
# Read bitmap data (starting from the current bit position)
195+
for y in range(bbox_h):
196+
for x in range(bbox_w):
197+
pixel_value = self._read_bits(self._bits_per_pixel)
198+
if pixel_value > 0: # Convert any non-zero value to 1
199+
bitmap[x, y] = 1
200+
201+
202+
# Create and cache the glyph
203+
self._glyphs[code_point] = Glyph(
204+
bitmap,
205+
0,
206+
bbox_w,
207+
bbox_h,
208+
bbox_x,
209+
bbox_y,
210+
glyph_advance,
211+
0
212+
)
86.1 KB
Binary file not shown.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# SPDX-FileCopyrightText: 2024 GNU Unifont Contributors
2+
#
3+
# SPDX-License-Identifier: OFL-1.1
4+
5+
# Unifont version 16.0.02 is licensed under the SIL Open Font License 1.1 (OFL-1.1).
6+
7+
# Original Unifont converted to LVGL binary format for use with CircuitPython.

0 commit comments

Comments
 (0)