Skip to content

Commit 08d6cb8

Browse files
committed
Add additional cmap support
Formats 0 and 3 are used in Japanese fonts.
1 parent 51b4032 commit 08d6cb8

File tree

5 files changed

+113
-14
lines changed

5 files changed

+113
-14
lines changed

adafruit_bitmap_font/lvfontbin.py

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ class LVGLFont(GlyphCache):
4545
"""
4646

4747
def __init__(self, f: FileIO, bitmap_class=None):
48+
"""Initialize LVGL font.
49+
50+
Args:
51+
f: File object containing the LVGL font data
52+
bitmap_class: Optional bitmap class to use for glyphs. Defaults to displayio.Bitmap.
53+
"""
4854
super().__init__()
4955
f.seek(0)
5056
self.file = f
@@ -77,7 +83,7 @@ def __init__(self, f: FileIO, bitmap_class=None):
7783
self._x_offset = 0
7884
self._y_offset = self._descent
7985
elif table_marker == b"cmap":
80-
self._load_cmap(remaining_section)
86+
self._load_cmap(remaining_section, section_start)
8187
elif table_marker == b"loca":
8288
self._max_cid = struct.unpack("<I", remaining_section[0:4])[0]
8389
self._loca_start = section_start + 4
@@ -112,21 +118,40 @@ def _load_head(self, data):
112118
self._compression_alg = data[33]
113119
self._subpixel_rendering = data[34]
114120

115-
def _load_cmap(self, data):
121+
def _load_cmap(self, data, section_start):
116122
data = memoryview(data)
117123
subtable_count = struct.unpack("<I", data[0:4])[0]
118-
self._cmap_tiny = []
124+
self._cmap_subtables = []
125+
data_offset = 4
126+
119127
for i in range(subtable_count):
120-
subtable_header = data[4 + 16 * i : 4 + 16 * (i + 1)]
121-
(_, range_start, range_length, glyph_offset, _) = struct.unpack(
122-
"<IIHHH", subtable_header[:14]
128+
subtable_header = data[data_offset + 16 * i : data_offset + 16 * (i + 1)]
129+
# Subtable header format:
130+
# 4 bytes - data offset
131+
# 4 bytes - range start
132+
# 2 bytes - range length
133+
# 2 bytes - glyph ID offset
134+
# 2 bytes - data entries count
135+
# 1 byte - format type
136+
# 1 byte - padding
137+
if len(subtable_header) < 16:
138+
raise RuntimeError("Invalid cmap subtable header size")
139+
140+
(data_offset_val, range_start, range_length, glyph_offset, entries_count) = (
141+
struct.unpack("<IIHHH", subtable_header[:14])
123142
)
124143
format_type = subtable_header[14]
125144

126-
if format_type != 2:
127-
raise RuntimeError(f"Unsupported cmap format {format_type}")
128-
129-
self._cmap_tiny.append((range_start, range_start + range_length, glyph_offset))
145+
# Store subtable header info
146+
subtable_info = {
147+
"format": format_type,
148+
"data_offset": data_offset_val + section_start - 8,
149+
"range_start": range_start,
150+
"range_length": range_length,
151+
"glyph_offset": glyph_offset,
152+
"entries_count": entries_count,
153+
}
154+
self._cmap_subtables.append(subtable_info)
130155

131156
@property
132157
def ascent(self) -> int:
@@ -177,10 +202,38 @@ def load_glyphs(self, code_points: Union[int, str, Iterable[int]]) -> None:
177202
for code_point in code_points:
178203
# Find character ID in the cmap table
179204
cid = None
180-
for start, end, offset in self._cmap_tiny:
181-
if start <= code_point < end:
182-
cid = offset + (code_point - start)
183-
break
205+
206+
# Search through all subtables
207+
for subtable in self._cmap_subtables:
208+
format_type = subtable["format"]
209+
range_start = subtable["range_start"]
210+
range_length = subtable["range_length"]
211+
glyph_offset = subtable["glyph_offset"]
212+
entries_count = subtable["entries_count"]
213+
data_offset = subtable["data_offset"]
214+
215+
if range_start <= code_point < range_start + range_length:
216+
if format_type == 0: # Continuous
217+
# Read the glyph IDs from the data section (single bytes)
218+
self.file.seek(data_offset)
219+
subtable_data = self.file.read(entries_count)
220+
glyph_id = subtable_data[code_point - range_start]
221+
cid = glyph_id + glyph_offset
222+
break
223+
elif format_type == 2: # Format 0 tiny
224+
cid = glyph_offset + (code_point - range_start)
225+
break
226+
elif format_type == 3: # Sparse tiny
227+
# Read the codepoint offsets from the data section
228+
self.file.seek(data_offset)
229+
subtable_data = self.file.read(entries_count * 2)
230+
for i in range(entries_count):
231+
cp_offset = struct.unpack("<H", subtable_data[i * 2 : (i + 1) * 2])[0]
232+
if cp_offset + range_start == code_point:
233+
cid = glyph_offset + i
234+
break
235+
if cid is not None:
236+
break
184237

185238
if cid is None or cid >= self._max_cid:
186239
self._glyphs[code_point] = None
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# SPDX-FileCopyrightText: 2025 Scott Shawcroft for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
4+
"""
5+
This example demonstrates loading and using an LVGL format font.
6+
You can convert fonts to LVGL format using the online converter:
7+
https://lvgl.io/tools/fontconverter
8+
"""
9+
10+
from adafruit_bitmap_font import bitmap_font
11+
12+
# Use the Japanese font file
13+
font_file = "fonts/unifont-16.0.02-ja.bin"
14+
15+
font = bitmap_font.load_font(font_file)
16+
print("Successfully loaded LVGL font")
17+
print("Font metrics:")
18+
print(f" Ascent: {font.ascent}")
19+
print(f" Descent: {font.descent}")
20+
bbox = font.get_bounding_box()
21+
print(f" Bounding box: width={bbox[0]}, height={bbox[1]}, x_offset={bbox[2]}, y_offset={bbox[3]}")
22+
23+
# Test characters
24+
test_japanese = "a こんにちは世界🎉" # Hello World in Japanese (according to Claude)
25+
print(f"\nTesting characters: {test_japanese}")
26+
font.load_glyphs(test_japanese)
27+
for c in test_japanese:
28+
glyph = font.get_glyph(ord(c))
29+
if glyph:
30+
print(f" Character '{c}' (U+{ord(c):04X}):") # Print ASCII art representation of the glyph
31+
for y in range(glyph.height):
32+
pixels = []
33+
for x in range(glyph.width):
34+
value = glyph.bitmap[x, y]
35+
pixel = "#" if value > 0 else " "
36+
pixels.append(pixel)
37+
print(" " + "".join(pixels))
38+
else:
39+
print(f" Character '{c}' (U+{ord(c):04X}) not found in font")
0 Bytes
Binary file not shown.

examples/fonts/unifont-16.0.02-ja.bin

963 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)