Skip to content

Commit 51b4032

Browse files
authored
Merge pull request #69 from tannewt/lvfontbin
Add support for LVGL binary font format
2 parents 79d62e7 + 9191096 commit 51b4032

File tree

5 files changed

+341
-2
lines changed

5 files changed

+341
-2
lines changed

LICENSES/OFL-1.1.txt

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
The SIL Open Font License version 1.1 is copied below, and is also
2+
available with a FAQ at http://scripts.sil.org/OFL.
3+
4+
5+
-----------------------------------------------------------
6+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
7+
-----------------------------------------------------------
8+
9+
PREAMBLE
10+
The goals of the Open Font License (OFL) are to stimulate worldwide
11+
development of collaborative font projects, to support the font creation
12+
efforts of academic and linguistic communities, and to provide a free and
13+
open framework in which fonts may be shared and improved in partnership
14+
with others.
15+
16+
The OFL allows the licensed fonts to be used, studied, modified and
17+
redistributed freely as long as they are not sold by themselves. The
18+
fonts, including any derivative works, can be bundled, embedded,
19+
redistributed and/or sold with any software provided that any reserved
20+
names are not used by derivative works. The fonts and derivatives,
21+
however, cannot be released under any other type of license. The
22+
requirement for fonts to remain under this license does not apply
23+
to any document created using the fonts or their derivatives.
24+
25+
DEFINITIONS
26+
"Font Software" refers to the set of files released by the Copyright
27+
Holder(s) under this license and clearly marked as such. This may
28+
include source files, build scripts and documentation.
29+
30+
"Reserved Font Name" refers to any names specified as such after the
31+
copyright statement(s).
32+
33+
"Original Version" refers to the collection of Font Software components as
34+
distributed by the Copyright Holder(s).
35+
36+
"Modified Version" refers to any derivative made by adding to, deleting,
37+
or substituting -- in part or in whole -- any of the components of the
38+
Original Version, by changing formats or by porting the Font Software to a
39+
new environment.
40+
41+
"Author" refers to any designer, engineer, programmer, technical
42+
writer or other person who contributed to the Font Software.
43+
44+
PERMISSION & CONDITIONS
45+
Permission is hereby granted, free of charge, to any person obtaining
46+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
47+
redistribute, and sell modified and unmodified copies of the Font
48+
Software, subject to the following conditions:
49+
50+
1) Neither the Font Software nor any of its individual components,
51+
in Original or Modified Versions, may be sold by itself.
52+
53+
2) Original or Modified Versions of the Font Software may be bundled,
54+
redistributed and/or sold with any software, provided that each copy
55+
contains the above copyright notice and this license. These can be
56+
included either as stand-alone text files, human-readable headers or
57+
in the appropriate machine-readable metadata fields within text or
58+
binary files as long as those fields can be easily viewed by the user.
59+
60+
3) No Modified Version of the Font Software may use the Reserved Font
61+
Name(s) unless explicit written permission is granted by the corresponding
62+
Copyright Holder. This restriction only applies to the primary font name as
63+
presented to the users.
64+
65+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
66+
Software shall not be used to promote, endorse or advertise any
67+
Modified Version, except to acknowledge the contribution(s) of the
68+
Copyright Holder(s) and the Author(s) or with their explicit written
69+
permission.
70+
71+
5) The Font Software, modified or unmodified, in part or in whole,
72+
must be distributed entirely under this license, and must not be
73+
distributed under any other license. The requirement for fonts to
74+
remain under this license does not apply to any document created
75+
using the Font Software.
76+
77+
TERMINATION
78+
This license becomes null and void if any of the above conditions are
79+
not met.
80+
81+
DISCLAIMER
82+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
83+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
84+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
85+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
86+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
87+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
88+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
89+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
90+
OTHER DEALINGS IN THE FONT SOFTWARE.

adafruit_bitmap_font/bitmap_font.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,22 @@
2727

2828
from displayio import Bitmap
2929

30-
from . import bdf, pcf, ttf
30+
from . import bdf, lvfontbin, pcf, ttf
3131
except ImportError:
3232
pass
3333

3434
__version__ = "0.0.0+auto.0"
3535
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Bitmap_Font.git"
3636

3737

38-
def load_font(filename: str, bitmap: Optional[Bitmap] = None) -> Union[bdf.BDF, pcf.PCF, ttf.TTF]:
38+
# The LVGL file starts with the size of the 'head' section. It hasn't changed in five years so
39+
# we can treat it like a magic number.
40+
LVGL_HEADER_SIZE = b"\x30\x00\x00\x00"
41+
42+
43+
def load_font(
44+
filename: str, bitmap: Optional[Bitmap] = None
45+
) -> Union[bdf.BDF, lvfontbin.LVGLFont, pcf.PCF, ttf.TTF]:
3946
"""Loads a font file. Returns None if unsupported."""
4047
if not bitmap:
4148
import displayio
@@ -56,4 +63,11 @@ def load_font(filename: str, bitmap: Optional[Bitmap] = None) -> Union[bdf.BDF,
5663

5764
return ttf.TTF(font_file, bitmap)
5865

66+
if (
67+
filename.endswith("bin") or filename.endswith("lvfontbin")
68+
) and first_four == LVGL_HEADER_SIZE:
69+
from . import lvfontbin
70+
71+
return lvfontbin.LVGLFont(font_file, bitmap)
72+
5973
raise ValueError("Unknown magic number %r" % first_four)

adafruit_bitmap_font/lvfontbin.py

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
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 Iterable, Union
30+
except ImportError:
31+
pass
32+
33+
from fontio import Glyph
34+
35+
from .glyph_cache import GlyphCache
36+
37+
38+
class LVGLFont(GlyphCache):
39+
"""Loads glyphs from a LVGL binary font file in the given bitmap_class.
40+
41+
There is an in-browser converter here: https://lvgl.io/tools/fontconverter
42+
43+
The format is documented here: https://github.com/lvgl/lv_font_conv/tree/master/doc
44+
45+
"""
46+
47+
def __init__(self, f: FileIO, bitmap_class=None):
48+
super().__init__()
49+
f.seek(0)
50+
self.file = f
51+
self.bitmap_class = bitmap_class
52+
# Initialize default values for bounding box
53+
self._width = None
54+
self._height = None
55+
self._x_offset = 0
56+
self._y_offset = 0
57+
58+
# For reading bits
59+
self._byte = 0
60+
self._remaining_bits = 0
61+
62+
while True:
63+
buffer = f.read(4)
64+
if len(buffer) < 4:
65+
break
66+
section_size = struct.unpack("<I", buffer)[0]
67+
if section_size == 0:
68+
break
69+
table_marker = f.read(4)
70+
section_start = f.tell()
71+
remaining_section = f.read(section_size - 8)
72+
if table_marker == b"head":
73+
self._load_head(remaining_section)
74+
# Set bounding box based on font metrics from head section
75+
self._width = self._default_advance_width
76+
self._height = self._font_size
77+
self._x_offset = 0
78+
self._y_offset = self._descent
79+
elif table_marker == b"cmap":
80+
self._load_cmap(remaining_section)
81+
elif table_marker == b"loca":
82+
self._max_cid = struct.unpack("<I", remaining_section[0:4])[0]
83+
self._loca_start = section_start + 4
84+
elif table_marker == b"glyf":
85+
self._glyf_start = section_start - 8
86+
87+
def _load_head(self, data):
88+
self._version = struct.unpack("<I", data[0:4])[0]
89+
(
90+
self._font_size,
91+
self._ascent,
92+
self._descent,
93+
self._typo_ascent,
94+
self._typo_descent,
95+
self._line_gap,
96+
self._min_y,
97+
self._max_y,
98+
self._default_advance_width,
99+
self._kerning_scale,
100+
) = struct.unpack("<HHhHhHHHHH", data[6:26])
101+
self._index_to_loc_format = data[26]
102+
self._glyph_id_format = data[27]
103+
self._advance_format = data[28]
104+
self._bits_per_pixel = data[29]
105+
self._glyph_bbox_xy_bits = data[30]
106+
self._glyph_bbox_wh_bits = data[31]
107+
self._glyph_advance_bits = data[32]
108+
self._glyph_header_bits = (
109+
self._glyph_advance_bits + 2 * self._glyph_bbox_xy_bits + 2 * self._glyph_bbox_wh_bits
110+
)
111+
self._glyph_header_bytes = (self._glyph_header_bits + 7) // 8
112+
self._compression_alg = data[33]
113+
self._subpixel_rendering = data[34]
114+
115+
def _load_cmap(self, data):
116+
data = memoryview(data)
117+
subtable_count = struct.unpack("<I", data[0:4])[0]
118+
self._cmap_tiny = []
119+
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]
123+
)
124+
format_type = subtable_header[14]
125+
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))
130+
131+
@property
132+
def ascent(self) -> int:
133+
"""The number of pixels above the baseline of a typical ascender"""
134+
return self._ascent
135+
136+
@property
137+
def descent(self) -> int:
138+
"""The number of pixels below the baseline of a typical descender"""
139+
return self._descent
140+
141+
def get_bounding_box(self) -> tuple[int, int, int, int]:
142+
"""Return the maximum glyph size as a 4-tuple of: width, height, x_offset, y_offset"""
143+
return (self._width, self._height, self._x_offset, self._y_offset)
144+
145+
def _seek(self, offset):
146+
self.file.seek(offset)
147+
self._byte = 0
148+
self._remaining_bits = 0
149+
150+
def _read_bits(self, num_bits):
151+
result = 0
152+
needed_bits = num_bits
153+
while needed_bits > 0:
154+
if self._remaining_bits == 0:
155+
self._byte = self.file.read(1)[0]
156+
self._remaining_bits = 8
157+
available_bits = min(needed_bits, self._remaining_bits)
158+
result = (result << available_bits) | (self._byte >> (8 - available_bits))
159+
self._byte <<= available_bits
160+
self._byte &= 0xFF
161+
self._remaining_bits -= available_bits
162+
needed_bits -= available_bits
163+
return result
164+
165+
def load_glyphs(self, code_points: Union[int, str, Iterable[int]]) -> None:
166+
# pylint: disable=too-many-statements,too-many-branches,too-many-nested-blocks,too-many-locals
167+
if isinstance(code_points, int):
168+
code_points = (code_points,)
169+
elif isinstance(code_points, str):
170+
code_points = [ord(c) for c in code_points]
171+
172+
# Only load glyphs that aren't already cached
173+
code_points = sorted(c for c in code_points if self._glyphs.get(c, None) is None)
174+
if not code_points:
175+
return
176+
177+
for code_point in code_points:
178+
# Find character ID in the cmap table
179+
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
184+
185+
if cid is None or cid >= self._max_cid:
186+
self._glyphs[code_point] = None
187+
continue
188+
189+
offset_length = 4 if self._index_to_loc_format == 1 else 2
190+
191+
# Get the glyph offset from the location table
192+
self._seek(self._loca_start + cid * offset_length)
193+
glyph_offset = struct.unpack(
194+
"<I" if offset_length == 4 else "<H", self.file.read(offset_length)
195+
)[0]
196+
197+
# Read glyph header data
198+
self._seek(self._glyf_start + glyph_offset)
199+
glyph_advance = self._read_bits(self._glyph_advance_bits)
200+
201+
# Read and convert signed bbox_x and bbox_y
202+
bbox_x = self._read_bits(self._glyph_bbox_xy_bits)
203+
# Convert to signed value if needed (using two's complement)
204+
if bbox_x & (1 << (self._glyph_bbox_xy_bits - 1)):
205+
bbox_x = bbox_x - (1 << self._glyph_bbox_xy_bits)
206+
207+
bbox_y = self._read_bits(self._glyph_bbox_xy_bits)
208+
# Convert to signed value if needed (using two's complement)
209+
if bbox_y & (1 << (self._glyph_bbox_xy_bits - 1)):
210+
bbox_y = bbox_y - (1 << self._glyph_bbox_xy_bits)
211+
212+
bbox_w = self._read_bits(self._glyph_bbox_wh_bits)
213+
bbox_h = self._read_bits(self._glyph_bbox_wh_bits)
214+
215+
# Create bitmap for the glyph
216+
bitmap = self.bitmap_class(bbox_w, bbox_h, 2)
217+
218+
# Read bitmap data (starting from the current bit position)
219+
for y in range(bbox_h):
220+
for x in range(bbox_w):
221+
pixel_value = self._read_bits(self._bits_per_pixel)
222+
if pixel_value > 0: # Convert any non-zero value to 1
223+
bitmap[x, y] = 1
224+
225+
# Create and cache the glyph
226+
self._glyphs[code_point] = Glyph(
227+
bitmap, 0, bbox_w, bbox_h, bbox_x, bbox_y, glyph_advance, 0
228+
)
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)