Skip to content

Add additional cmap support #70

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 67 additions & 14 deletions adafruit_bitmap_font/lvfontbin.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ class LVGLFont(GlyphCache):
"""

def __init__(self, f: FileIO, bitmap_class=None):
"""Initialize LVGL font.

Args:
f: File object containing the LVGL font data
bitmap_class: Optional bitmap class to use for glyphs. Defaults to displayio.Bitmap.
"""
super().__init__()
f.seek(0)
self.file = f
Expand Down Expand Up @@ -77,7 +83,7 @@ def __init__(self, f: FileIO, bitmap_class=None):
self._x_offset = 0
self._y_offset = self._descent
elif table_marker == b"cmap":
self._load_cmap(remaining_section)
self._load_cmap(remaining_section, section_start)
elif table_marker == b"loca":
self._max_cid = struct.unpack("<I", remaining_section[0:4])[0]
self._loca_start = section_start + 4
Expand Down Expand Up @@ -112,21 +118,40 @@ def _load_head(self, data):
self._compression_alg = data[33]
self._subpixel_rendering = data[34]

def _load_cmap(self, data):
def _load_cmap(self, data, section_start):
data = memoryview(data)
subtable_count = struct.unpack("<I", data[0:4])[0]
self._cmap_tiny = []
self._cmap_subtables = []
data_offset = 4

for i in range(subtable_count):
subtable_header = data[4 + 16 * i : 4 + 16 * (i + 1)]
(_, range_start, range_length, glyph_offset, _) = struct.unpack(
"<IIHHH", subtable_header[:14]
subtable_header = data[data_offset + 16 * i : data_offset + 16 * (i + 1)]
# Subtable header format:
# 4 bytes - data offset
# 4 bytes - range start
# 2 bytes - range length
# 2 bytes - glyph ID offset
# 2 bytes - data entries count
# 1 byte - format type
# 1 byte - padding
if len(subtable_header) < 16:
raise RuntimeError("Invalid cmap subtable header size")

(data_offset_val, range_start, range_length, glyph_offset, entries_count) = (
struct.unpack("<IIHHH", subtable_header[:14])
)
format_type = subtable_header[14]

if format_type != 2:
raise RuntimeError(f"Unsupported cmap format {format_type}")

self._cmap_tiny.append((range_start, range_start + range_length, glyph_offset))
# Store subtable header info
subtable_info = {
"format": format_type,
"data_offset": data_offset_val + section_start - 8,
"range_start": range_start,
"range_length": range_length,
"glyph_offset": glyph_offset,
"entries_count": entries_count,
}
self._cmap_subtables.append(subtable_info)

@property
def ascent(self) -> int:
Expand Down Expand Up @@ -177,10 +202,38 @@ def load_glyphs(self, code_points: Union[int, str, Iterable[int]]) -> None:
for code_point in code_points:
# Find character ID in the cmap table
cid = None
for start, end, offset in self._cmap_tiny:
if start <= code_point < end:
cid = offset + (code_point - start)
break

# Search through all subtables
for subtable in self._cmap_subtables:
format_type = subtable["format"]
range_start = subtable["range_start"]
range_length = subtable["range_length"]
glyph_offset = subtable["glyph_offset"]
entries_count = subtable["entries_count"]
data_offset = subtable["data_offset"]

if range_start <= code_point < range_start + range_length:
if format_type == 0: # Continuous
# Read the glyph IDs from the data section (single bytes)
self.file.seek(data_offset)
subtable_data = self.file.read(entries_count)
glyph_id = subtable_data[code_point - range_start]
cid = glyph_id + glyph_offset
break
elif format_type == 2: # Format 0 tiny
cid = glyph_offset + (code_point - range_start)
break
elif format_type == 3: # Sparse tiny
# Read the codepoint offsets from the data section
self.file.seek(data_offset)
subtable_data = self.file.read(entries_count * 2)
for i in range(entries_count):
cp_offset = struct.unpack("<H", subtable_data[i * 2 : (i + 1) * 2])[0]
if cp_offset + range_start == code_point:
cid = glyph_offset + i
break
if cid is not None:
break

if cid is None or cid >= self._max_cid:
self._glyphs[code_point] = None
Expand Down
39 changes: 39 additions & 0 deletions examples/bitmap_font_lvgl_simpletest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# SPDX-FileCopyrightText: 2025 Scott Shawcroft for Adafruit Industries
# SPDX-License-Identifier: MIT

"""
This example demonstrates loading and using an LVGL format font.
You can convert fonts to LVGL format using the online converter:
https://lvgl.io/tools/fontconverter
"""

from adafruit_bitmap_font import bitmap_font

# Use the Japanese font file
font_file = "fonts/unifont-16.0.02-ja.bin"

font = bitmap_font.load_font(font_file)
print("Successfully loaded LVGL font")
print("Font metrics:")
print(f" Ascent: {font.ascent}")
print(f" Descent: {font.descent}")
bbox = font.get_bounding_box()
print(f" Bounding box: width={bbox[0]}, height={bbox[1]}, x_offset={bbox[2]}, y_offset={bbox[3]}")

# Test characters
test_japanese = "a こんにちは世界🎉" # Hello World in Japanese (according to Claude)
print(f"\nTesting characters: {test_japanese}")
font.load_glyphs(test_japanese)
for c in test_japanese:
glyph = font.get_glyph(ord(c))
if glyph:
print(f" Character '{c}' (U+{ord(c):04X}):") # Print ASCII art representation of the glyph
for y in range(glyph.height):
pixels = []
for x in range(glyph.width):
value = glyph.bitmap[x, y]
pixel = "#" if value > 0 else " "
pixels.append(pixel)
print(" " + "".join(pixels))
else:
print(f" Character '{c}' (U+{ord(c):04X}) not found in font")
Binary file modified examples/fonts/unifont-16.0.02-ascii-emoji.bin
Binary file not shown.
Binary file added examples/fonts/unifont-16.0.02-ja.bin
Binary file not shown.
7 changes: 7 additions & 0 deletions examples/fonts/unifont-16.0.02-ja.bin.license
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: 2024 GNU Unifont Contributors
#
# SPDX-License-Identifier: OFL-1.1

# Unifont version 16.0.02 is licensed under the SIL Open Font License 1.1 (OFL-1.1).

# Original Unifont converted to LVGL binary format for use with CircuitPython.