Skip to content

Commit 775c4de

Browse files
committed
Add support for LVGL binary font format
There is an in-browser converter here: https://lvgl.io/tools/fontconverter The format is documented here: https://github.com/lvgl/lv_font_conv/tree/master/doc
1 parent 03d935b commit 775c4de

File tree

5 files changed

+335
-1
lines changed

5 files changed

+335
-1
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: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,22 @@
2828
from . import bdf
2929
from . import pcf
3030
from . import ttf
31+
from . import lvfontbin
3132
except ImportError:
3233
pass
3334

3435
__version__ = "0.0.0+auto.0"
3536
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Bitmap_Font.git"
3637

3738

39+
# The LVGL file starts with the size of the 'head' section. It hasn't changed in five years so
40+
# we can treat it like a magic number.
41+
LVGL_HEADER_SIZE = b"\x30\x00\x00\x00"
42+
43+
3844
def load_font(
3945
filename: str, bitmap: Optional[Bitmap] = None
40-
) -> Union[bdf.BDF, pcf.PCF, ttf.TTF]:
46+
) -> Union[bdf.BDF, pcf.PCF, ttf.TTF, lvfontbin.LVGLFont]:
4147
"""Loads a font file. Returns None if unsupported."""
4248
# pylint: disable=import-outside-toplevel, redefined-outer-name, consider-using-with
4349
if not bitmap:
@@ -59,4 +65,9 @@ def load_font(
5965

6066
return ttf.TTF(font_file, bitmap)
6167

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

adafruit_bitmap_font/lvfontbin.py

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