Skip to content

Commit 3710d01

Browse files
ShowFonts: improve
1 parent 7b3d08c commit 3710d01

2 files changed

Lines changed: 384 additions & 33 deletions

File tree

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
import lvgl as lv
2+
import os
3+
4+
5+
CP_VARIATION_SELECTOR_TEXT = 0xFE0E
6+
CP_VARIATION_SELECTOR_EMOJI = 0xFE0F
7+
8+
9+
class FontManager:
10+
_DEFAULT_SIZE = 12
11+
_DEBUG = False
12+
_UNKNOWN_EMOJI_LOG_THRESHOLD = 0x2300
13+
_EMOJI_PNG_DIR = "openmoji-72x72-color"
14+
_EMOJI_PNG_SRC_PREFIX = "M:apps/com.micropythonos.showfonts/assets/openmoji-72x72-color/"
15+
_EMOJI_PNG_DIR_CANDIDATES = (
16+
_EMOJI_PNG_DIR,
17+
"./" + _EMOJI_PNG_DIR,
18+
"assets/" + _EMOJI_PNG_DIR,
19+
"apps/com.micropythonos.showfonts/assets/" + _EMOJI_PNG_DIR,
20+
"/apps/com.micropythonos.showfonts/assets/" + _EMOJI_PNG_DIR,
21+
"M:apps/com.micropythonos.showfonts/assets/" + _EMOJI_PNG_DIR,
22+
)
23+
24+
_emoji_map = None
25+
_builtin_font_records = None
26+
_composed_font_cache = {}
27+
_ttf_font_cache = {}
28+
_imgfont_scaled_src_cache = {}
29+
_imgfont_source_size_cache = {}
30+
_imgfont_empty_src_cache = {}
31+
_unknown_emoji_codepoints_logged = {}
32+
33+
@classmethod
34+
def getFont(cls, size=None, ttf=None, family=None, emoji=True):
35+
target_size = cls._normalize_size(size)
36+
37+
if ttf is None:
38+
base_font = cls._get_builtin_font(target_size, family)
39+
else:
40+
base_font = cls._get_ttf_font(ttf, target_size)
41+
42+
if not emoji:
43+
return base_font
44+
45+
return cls._get_composed_font(base_font)
46+
47+
@classmethod
48+
def normalizeEmojiText(cls, text):
49+
text = text.replace(chr(CP_VARIATION_SELECTOR_TEXT), "")
50+
text = text.replace(chr(CP_VARIATION_SELECTOR_EMOJI), "")
51+
return text
52+
53+
@classmethod
54+
def _normalize_size(cls, size):
55+
if size is None:
56+
return cls._DEFAULT_SIZE
57+
try:
58+
return max(1, int(size))
59+
except Exception:
60+
return cls._DEFAULT_SIZE
61+
62+
@classmethod
63+
def _get_builtin_font(cls, size, family=None):
64+
builtin_fonts = cls._get_builtin_font_records()
65+
search_fonts = []
66+
67+
if family is not None:
68+
for record in builtin_fonts:
69+
if record["family"] == family:
70+
search_fonts.append(record)
71+
72+
if not search_fonts:
73+
montserrat_fonts = []
74+
for record in builtin_fonts:
75+
if record["family"] == "Montserrat":
76+
montserrat_fonts.append(record)
77+
search_fonts = montserrat_fonts if montserrat_fonts else builtin_fonts
78+
79+
if search_fonts:
80+
best = search_fonts[0]
81+
best_key = (abs(best["size"] - size), 1 if best["size"] > size else 0)
82+
for candidate in search_fonts[1:]:
83+
key = (abs(candidate["size"] - size), 1 if candidate["size"] > size else 0)
84+
if key < best_key:
85+
best_key = key
86+
best = candidate
87+
return best["font"]
88+
fallback_records = cls._get_builtin_font_records()
89+
if fallback_records:
90+
return fallback_records[0]["font"]
91+
return lv.font_montserrat_12
92+
93+
@classmethod
94+
def listFonts(cls):
95+
fonts = []
96+
for record in cls._get_builtin_font_records():
97+
composed_font = cls._get_composed_font(record["font"], record["size"])
98+
fonts.append(
99+
{
100+
"name": "{} {}".format(record["family"], record["size"]),
101+
"family": record["family"],
102+
"size": record["size"],
103+
"font": composed_font,
104+
"base_font": record["font"],
105+
}
106+
)
107+
return fonts
108+
109+
@classmethod
110+
def _get_builtin_font_records(cls):
111+
if cls._builtin_font_records is not None:
112+
return cls._builtin_font_records
113+
114+
candidates = (
115+
("Montserrat", 8, "font_montserrat_8"),
116+
("Montserrat", 10, "font_montserrat_10"),
117+
("Montserrat", 12, "font_montserrat_12"),
118+
("Montserrat", 14, "font_montserrat_14"),
119+
("Montserrat", 16, "font_montserrat_16"),
120+
("Montserrat", 18, "font_montserrat_18"),
121+
("Montserrat", 20, "font_montserrat_20"),
122+
("Montserrat", 24, "font_montserrat_24"),
123+
("Montserrat", 28, "font_montserrat_28"),
124+
("Unscii", 8, "font_unscii_8"),
125+
("Unscii", 16, "font_unscii_16"),
126+
)
127+
128+
records = []
129+
for family, size, attr in candidates:
130+
if hasattr(lv, attr):
131+
records.append(
132+
{
133+
"family": family,
134+
"size": size,
135+
"font": getattr(lv, attr),
136+
}
137+
)
138+
139+
cls._builtin_font_records = records
140+
return cls._builtin_font_records
141+
142+
@classmethod
143+
def _get_composed_font(cls, base_font, size=None):
144+
if base_font is None:
145+
return None
146+
147+
font_id = cls._font_identity(base_font)
148+
emoji_size = size if size is not None else cls._font_pixel_height(base_font)
149+
cache_key = (font_id, int(emoji_size))
150+
if cache_key in cls._composed_font_cache:
151+
return cls._composed_font_cache[cache_key]
152+
153+
emoji_font = cls._create_emoji_font(emoji_size)
154+
if emoji_font is None:
155+
return base_font
156+
157+
try:
158+
# Do not mutate builtin font fallback: builtins may live in readonly memory.
159+
emoji_font.fallback = base_font
160+
except Exception as err:
161+
cls._debug("compose fallback set failed: " + repr(err))
162+
return base_font
163+
164+
cls._composed_font_cache[cache_key] = emoji_font
165+
return emoji_font
166+
167+
@classmethod
168+
def _font_identity(cls, font):
169+
try:
170+
return int(id(font))
171+
except Exception:
172+
return repr(font)
173+
174+
@classmethod
175+
def _debug(cls, message):
176+
if cls._DEBUG:
177+
print("font_manager: " + message)
178+
179+
@classmethod
180+
def _get_ttf_font(cls, ttf_path, size):
181+
key = (ttf_path, size)
182+
if key in cls._ttf_font_cache:
183+
return cls._ttf_font_cache[key]
184+
185+
try:
186+
font = lv.tiny_ttf_create_file(ttf_path, size)
187+
cls._ttf_font_cache[key] = font
188+
return font
189+
except Exception:
190+
return cls._get_builtin_font(size)
191+
192+
@classmethod
193+
def _create_emoji_font(cls, size):
194+
size = max(1, int(size))
195+
cls._ensure_emoji_map()
196+
197+
try:
198+
return lv.imgfont_create(size, cls._imgfont_path_cb, None)
199+
except Exception:
200+
return None
201+
202+
@classmethod
203+
def _ensure_emoji_map(cls):
204+
if cls._emoji_map is None:
205+
cls._emoji_map = cls._build_emoji_map_from_png_dir()
206+
207+
@classmethod
208+
def _build_emoji_map_from_png_dir(cls):
209+
emoji_map = {}
210+
dir_path = None
211+
entries = None
212+
213+
for candidate in cls._EMOJI_PNG_DIR_CANDIDATES:
214+
entries = cls._list_dir_names(candidate)
215+
if entries is not None:
216+
dir_path = candidate
217+
break
218+
219+
if entries is None:
220+
print("font_manager: could not list emoji dir candidates (cwd=" + cls._safe_getcwd() + ")")
221+
return emoji_map
222+
223+
for name in entries:
224+
if not name.lower().endswith(".png"):
225+
continue
226+
227+
codepoint_hex = name[:-4]
228+
try:
229+
codepoint = int(codepoint_hex, 16)
230+
except Exception:
231+
print("font_manager: skip non-hex emoji file: " + name)
232+
continue
233+
234+
emoji_map[codepoint] = cls._EMOJI_PNG_SRC_PREFIX + name
235+
236+
print("font_manager: loaded " + str(len(emoji_map)) + " emoji png mappings from " + str(dir_path))
237+
return emoji_map
238+
239+
@classmethod
240+
def _list_dir_names(cls, path):
241+
try:
242+
names = []
243+
for entry in os.ilistdir(path):
244+
if isinstance(entry, tuple):
245+
names.append(entry[0])
246+
else:
247+
names.append(entry)
248+
return names
249+
except Exception:
250+
pass
251+
252+
try:
253+
return os.listdir(path)
254+
except Exception:
255+
return None
256+
257+
@classmethod
258+
def _safe_getcwd(cls):
259+
try:
260+
return os.getcwd()
261+
except Exception:
262+
return "?"
263+
264+
@classmethod
265+
def _imgfont_path_cb(cls, font, unicode_cp, unicode_next, offset_y, user_data):
266+
if unicode_cp == CP_VARIATION_SELECTOR_TEXT or unicode_cp == CP_VARIATION_SELECTOR_EMOJI:
267+
offset_y.__dereference__(0)
268+
target_height = cls._font_pixel_height(font)
269+
return cls._get_empty_imgfont_src(target_height)
270+
271+
if unicode_cp in cls._emoji_map:
272+
offset_y.__dereference__(0)
273+
src = cls._emoji_map[unicode_cp]
274+
target_height = cls._font_pixel_height(font)
275+
return cls._get_scaled_imgfont_src(src, target_height)
276+
277+
cls._log_unknown_emoji_codepoint(unicode_cp)
278+
return None
279+
280+
@classmethod
281+
def _log_unknown_emoji_codepoint(cls, unicode_cp):
282+
if unicode_cp < cls._UNKNOWN_EMOJI_LOG_THRESHOLD:
283+
return
284+
if unicode_cp in cls._unknown_emoji_codepoints_logged:
285+
return
286+
287+
cls._unknown_emoji_codepoints_logged[unicode_cp] = True
288+
#print("font_manager: unknown emoji codepoint 0x{:X}".format(unicode_cp))
289+
290+
@classmethod
291+
def _get_empty_imgfont_src(cls, target_height):
292+
target_height = max(1, int(target_height))
293+
if target_height in cls._imgfont_empty_src_cache:
294+
return cls._imgfont_empty_src_cache[target_height]
295+
296+
empty = lv.obj(lv.layer_top())
297+
try:
298+
empty.add_flag(lv.obj.FLAG.HIDDEN)
299+
empty.set_size(1, target_height)
300+
src = lv.snapshot_take(empty, lv.COLOR_FORMAT.ARGB8888)
301+
finally:
302+
empty.delete()
303+
304+
cls._imgfont_empty_src_cache[target_height] = src
305+
return src
306+
307+
@classmethod
308+
def _font_pixel_height(cls, font):
309+
try:
310+
return max(1, int(font.get_line_height()))
311+
except Exception:
312+
pass
313+
try:
314+
return max(1, int(font.line_height))
315+
except Exception:
316+
return 1
317+
318+
@classmethod
319+
def _get_scaled_imgfont_src(cls, src, target_height):
320+
key = (src, target_height)
321+
if key in cls._imgfont_scaled_src_cache:
322+
return cls._imgfont_scaled_src_cache[key]
323+
324+
try:
325+
src_w, src_h = cls._get_image_size(src)
326+
if src_h <= 0:
327+
return src
328+
329+
target_width = max(1, round(src_w * target_height / src_h))
330+
scaled_src = cls._render_scaled_image_src(src, target_width, target_height)
331+
if scaled_src is not None:
332+
cls._imgfont_scaled_src_cache[key] = scaled_src
333+
return scaled_src
334+
except Exception:
335+
pass
336+
337+
return src
338+
339+
@classmethod
340+
def _get_image_size(cls, src):
341+
if src in cls._imgfont_source_size_cache:
342+
return cls._imgfont_source_size_cache[src]
343+
344+
probe = lv.image(lv.layer_top())
345+
try:
346+
header = lv.image_header_t()
347+
probe.decoder_get_info(src, header)
348+
size = (int(header.w), int(header.h))
349+
finally:
350+
probe.delete()
351+
352+
cls._imgfont_source_size_cache[src] = size
353+
return size
354+
355+
@classmethod
356+
def _render_scaled_image_src(cls, src, target_width, target_height):
357+
renderer = lv.image(lv.layer_top())
358+
try:
359+
renderer.add_flag(lv.obj.FLAG.HIDDEN)
360+
renderer.set_size(target_width, target_height)
361+
renderer.set_inner_align(lv.image.ALIGN.CONTAIN)
362+
renderer.set_src(src)
363+
364+
return lv.snapshot_take(renderer, lv.COLOR_FORMAT.ARGB8888)
365+
finally:
366+
renderer.delete()

0 commit comments

Comments
 (0)