1
0
mirror of https://github.com/esphome/esphome.git synced 2025-03-15 15:18:16 +00:00

[font] Use freetype instead of Pillow for font rendering (#8300)

This commit is contained in:
Clyde Stubbs 2025-02-28 06:50:51 +11:00 committed by GitHub
parent 1029202848
commit 9bc4f68d87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 191 additions and 190 deletions

View File

@ -1,3 +1,4 @@
from collections.abc import MutableMapping
import functools import functools
import hashlib import hashlib
import logging import logging
@ -6,10 +7,10 @@ from pathlib import Path
import re import re
import esphome_glyphsets as glyphsets import esphome_glyphsets as glyphsets
import freetype from freetype import Face, ft_pixel_mode_grays, ft_pixel_mode_mono
import requests import requests
from esphome import core, external_files from esphome import external_files
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@ -26,7 +27,7 @@ from esphome.const import (
CONF_WEIGHT, CONF_WEIGHT,
) )
from esphome.core import CORE, HexInt from esphome.core import CORE, HexInt
from esphome.helpers import copy_file_if_changed, cpp_string_escape from esphome.helpers import cpp_string_escape
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -49,13 +50,42 @@ CONF_IGNORE_MISSING_GLYPHS = "ignore_missing_glyphs"
# Cache loaded freetype fonts # Cache loaded freetype fonts
class FontCache(dict): class FontCache(MutableMapping):
def __missing__(self, key): @staticmethod
try: def get_name(value):
res = self[key] = freetype.Face(key) if CONF_FAMILY in value:
return res return (
except freetype.FT_Exception as e: f"{value[CONF_FAMILY]}:{int(value[CONF_ITALIC])}:{value[CONF_WEIGHT]}"
raise cv.Invalid(f"Could not load Font file {key}: {e}") from e )
if CONF_URL in value:
return value[CONF_URL]
return value[CONF_PATH]
@staticmethod
def _keytransform(value):
if CONF_FAMILY in value:
return f"gfont:{value[CONF_FAMILY]}:{int(value[CONF_ITALIC])}:{value[CONF_WEIGHT]}"
if CONF_URL in value:
return f"url:{value[CONF_URL]}"
return f"file:{value[CONF_PATH]}"
def __init__(self):
self.store = {}
def __delitem__(self, key):
del self.store[self._keytransform(key)]
def __iter__(self):
return iter(self.store)
def __len__(self):
return len(self.store)
def __getitem__(self, item):
return self.store[self._keytransform(item)]
def __setitem__(self, key, value):
self.store[self._keytransform(key)] = Face(str(value))
FONT_CACHE = FontCache() FONT_CACHE = FontCache()
@ -109,14 +139,14 @@ def check_missing_glyphs(file, codepoints, warning: bool = False):
) )
if count > 10: if count > 10:
missing_str += f"\n and {count - 10} more." missing_str += f"\n and {count - 10} more."
message = f"Font {Path(file).name} is missing {count} glyph{'s' if count != 1 else ''}:\n {missing_str}" message = f"Font {FontCache.get_name(file)} is missing {count} glyph{'s' if count != 1 else ''}:\n {missing_str}"
if warning: if warning:
_LOGGER.warning(message) _LOGGER.warning(message)
else: else:
raise cv.Invalid(message) raise cv.Invalid(message)
def validate_glyphs(config): def validate_font_config(config):
""" """
Check for duplicate codepoints, then check that all requested codepoints actually Check for duplicate codepoints, then check that all requested codepoints actually
have glyphs defined in the appropriate font file. have glyphs defined in the appropriate font file.
@ -143,8 +173,6 @@ def validate_glyphs(config):
# Make setpoints and glyphspoints disjoint # Make setpoints and glyphspoints disjoint
setpoints.difference_update(glyphspoints) setpoints.difference_update(glyphspoints)
if fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP: if fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP:
# Pillow only allows 256 glyphs per bitmap font. Not sure if that is a Pillow limitation
# or a file format limitation
if any(x >= 256 for x in setpoints.copy().union(glyphspoints)): if any(x >= 256 for x in setpoints.copy().union(glyphspoints)):
raise cv.Invalid("Codepoints in bitmap fonts must be in the range 0-255") raise cv.Invalid("Codepoints in bitmap fonts must be in the range 0-255")
else: else:
@ -154,13 +182,14 @@ def validate_glyphs(config):
points = {ord(x) for x in flatten(extra[CONF_GLYPHS])} points = {ord(x) for x in flatten(extra[CONF_GLYPHS])}
glyphspoints.difference_update(points) glyphspoints.difference_update(points)
setpoints.difference_update(points) setpoints.difference_update(points)
check_missing_glyphs(extra[CONF_FILE][CONF_PATH], points) check_missing_glyphs(extra[CONF_FILE], points)
# A named glyph that can't be provided is an error # A named glyph that can't be provided is an error
check_missing_glyphs(fileconf[CONF_PATH], glyphspoints)
check_missing_glyphs(fileconf, glyphspoints)
# A missing glyph from a set is a warning. # A missing glyph from a set is a warning.
if not config[CONF_IGNORE_MISSING_GLYPHS]: if not config[CONF_IGNORE_MISSING_GLYPHS]:
check_missing_glyphs(fileconf[CONF_PATH], setpoints, warning=True) check_missing_glyphs(fileconf, setpoints, warning=True)
# Populate the default after the above checks so that use of the default doesn't trigger errors # Populate the default after the above checks so that use of the default doesn't trigger errors
if not config[CONF_GLYPHS] and not config[CONF_GLYPHSETS]: if not config[CONF_GLYPHS] and not config[CONF_GLYPHSETS]:
@ -168,17 +197,32 @@ def validate_glyphs(config):
config[CONF_GLYPHS] = [DEFAULT_GLYPHS] config[CONF_GLYPHS] = [DEFAULT_GLYPHS]
else: else:
# set a default glyphset, intersected with what the font actually offers # set a default glyphset, intersected with what the font actually offers
font = FONT_CACHE[fileconf[CONF_PATH]] font = FONT_CACHE[fileconf]
config[CONF_GLYPHS] = [ config[CONF_GLYPHS] = [
chr(x) chr(x)
for x in glyphsets.unicodes_per_glyphset(DEFAULT_GLYPHSET) for x in glyphsets.unicodes_per_glyphset(DEFAULT_GLYPHSET)
if font.get_char_index(x) != 0 if font.get_char_index(x) != 0
] ]
if config[CONF_FILE][CONF_TYPE] == TYPE_LOCAL_BITMAP:
if CONF_SIZE in config:
raise cv.Invalid(
"Size is not a valid option for bitmap fonts, which are inherently fixed size"
)
elif CONF_SIZE not in config:
config[CONF_SIZE] = 20
return config return config
FONT_EXTENSIONS = (".ttf", ".woff", ".otf") FONT_EXTENSIONS = (".ttf", ".woff", ".otf")
BITMAP_EXTENSIONS = (".bdf", ".pcf")
def validate_bitmap_file(value):
if not any(map(value.lower().endswith, BITMAP_EXTENSIONS)):
raise cv.Invalid(f"Only {', '.join(BITMAP_EXTENSIONS)} files are supported.")
return CORE.relative_config_path(cv.file_(value))
def validate_truetype_file(value): def validate_truetype_file(value):
@ -187,24 +231,40 @@ def validate_truetype_file(value):
f"Please unzip the font archive '{value}' first and then use the .ttf files inside." f"Please unzip the font archive '{value}' first and then use the .ttf files inside."
) )
if not any(map(value.lower().endswith, FONT_EXTENSIONS)): if not any(map(value.lower().endswith, FONT_EXTENSIONS)):
raise cv.Invalid(f"Only {FONT_EXTENSIONS} files are supported.") raise cv.Invalid(f"Only {', '.join(FONT_EXTENSIONS)} files are supported.")
return CORE.relative_config_path(cv.file_(value)) return CORE.relative_config_path(cv.file_(value))
def add_local_file(value):
if value in FONT_CACHE:
return value
path = value[CONF_PATH]
if not os.path.isfile(path):
raise cv.Invalid(f"File '{path}' not found.")
FONT_CACHE[value] = path
return value
TYPE_LOCAL = "local" TYPE_LOCAL = "local"
TYPE_LOCAL_BITMAP = "local_bitmap" TYPE_LOCAL_BITMAP = "local_bitmap"
TYPE_GFONTS = "gfonts" TYPE_GFONTS = "gfonts"
TYPE_WEB = "web" TYPE_WEB = "web"
LOCAL_SCHEMA = cv.Schema( LOCAL_SCHEMA = cv.All(
cv.Schema(
{ {
cv.Required(CONF_PATH): validate_truetype_file, cv.Required(CONF_PATH): validate_truetype_file,
} }
),
add_local_file,
) )
LOCAL_BITMAP_SCHEMA = cv.Schema( LOCAL_BITMAP_SCHEMA = cv.All(
cv.Schema(
{ {
cv.Required(CONF_PATH): cv.file_, cv.Required(CONF_PATH): validate_bitmap_file,
} }
),
add_local_file,
) )
FULLPATH_SCHEMA = cv.maybe_simple_value( FULLPATH_SCHEMA = cv.maybe_simple_value(
@ -235,27 +295,23 @@ def _compute_local_font_path(value: dict) -> Path:
h.update(url.encode()) h.update(url.encode())
key = h.hexdigest()[:8] key = h.hexdigest()[:8]
base_dir = external_files.compute_local_file_dir(DOMAIN) base_dir = external_files.compute_local_file_dir(DOMAIN)
_LOGGER.debug("_compute_local_font_path: base_dir=%s", base_dir / key) _LOGGER.debug("_compute_local_font_path: %s", base_dir / key)
return base_dir / key return base_dir / key
def get_font_path(value, font_type) -> Path:
if font_type == TYPE_GFONTS:
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1"
return external_files.compute_local_file_dir(DOMAIN) / f"{name}.ttf"
if font_type == TYPE_WEB:
return _compute_local_font_path(value) / "font.ttf"
assert False
def download_gfont(value): def download_gfont(value):
if value in FONT_CACHE:
return value
name = ( name = (
f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}" f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}"
) )
url = f"https://fonts.googleapis.com/css2?family={name}" url = f"https://fonts.googleapis.com/css2?family={name}"
path = get_font_path(value, TYPE_GFONTS) path = (
external_files.compute_local_file_dir(DOMAIN)
/ f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1.ttf"
)
if not external_files.is_file_recent(str(path), value[CONF_REFRESH]):
_LOGGER.debug("download_gfont: path=%s", path) _LOGGER.debug("download_gfont: path=%s", path)
try: try:
req = requests.get(url, timeout=external_files.NETWORK_TIMEOUT) req = requests.get(url, timeout=external_files.NETWORK_TIMEOUT)
req.raise_for_status() req.raise_for_status()
@ -275,16 +331,23 @@ def download_gfont(value):
_LOGGER.debug("download_gfont: ttf_url=%s", ttf_url) _LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
external_files.download_content(ttf_url, path) external_files.download_content(ttf_url, path)
return FULLPATH_SCHEMA(path) # In case the remote file is not modified, the download_content function will return the existing file,
# so update the modification time to now.
path.touch()
FONT_CACHE[value] = path
return value
def download_web_font(value): def download_web_font(value):
if value in FONT_CACHE:
return value
url = value[CONF_URL] url = value[CONF_URL]
path = get_font_path(value, TYPE_WEB) path = _compute_local_font_path(value) / "font.ttf"
external_files.download_content(url, path) external_files.download_content(url, path)
_LOGGER.debug("download_web_font: path=%s", path) _LOGGER.debug("download_web_font: path=%s", path)
return FULLPATH_SCHEMA(path) FONT_CACHE[value] = path
return value
EXTERNAL_FONT_SCHEMA = cv.Schema( EXTERNAL_FONT_SCHEMA = cv.Schema(
@ -340,14 +403,14 @@ def validate_file_shorthand(value):
} }
) )
if value.endswith(".pcf") or value.endswith(".bdf"): extension = Path(value).suffix
value = convert_bitmap_to_pillow_font( if extension in BITMAP_EXTENSIONS:
CORE.relative_config_path(cv.file_(value)) return font_file_schema(
) {
return {
CONF_TYPE: TYPE_LOCAL_BITMAP, CONF_TYPE: TYPE_LOCAL_BITMAP,
CONF_PATH: value, CONF_PATH: value,
} }
)
return font_file_schema( return font_file_schema(
{ {
@ -391,7 +454,7 @@ FONT_SCHEMA = cv.Schema(
cv.one_of(*glyphsets.defined_glyphsets()) cv.one_of(*glyphsets.defined_glyphsets())
), ),
cv.Optional(CONF_IGNORE_MISSING_GLYPHS, default=False): cv.boolean, cv.Optional(CONF_IGNORE_MISSING_GLYPHS, default=False): cv.boolean,
cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1), cv.Optional(CONF_SIZE): cv.int_range(min=1),
cv.Optional(CONF_BPP, default=1): cv.one_of(1, 2, 4, 8), cv.Optional(CONF_BPP, default=1): cv.one_of(1, 2, 4, 8),
cv.Optional(CONF_EXTRAS, default=[]): cv.ensure_list( cv.Optional(CONF_EXTRAS, default=[]): cv.ensure_list(
cv.Schema( cv.Schema(
@ -406,114 +469,19 @@ FONT_SCHEMA = cv.Schema(
}, },
) )
CONFIG_SCHEMA = cv.All(FONT_SCHEMA, validate_glyphs) CONFIG_SCHEMA = cv.All(FONT_SCHEMA, validate_font_config)
# PIL doesn't provide a consistent interface for both TrueType and bitmap
# fonts. So, we use our own wrappers to give us the consistency that we need.
class TrueTypeFontWrapper:
def __init__(self, font):
self.font = font
def getoffset(self, glyph):
_, (offset_x, offset_y) = self.font.font.getsize(glyph)
return offset_x, offset_y
def getmask(self, glyph, **kwargs):
return self.font.getmask(str(glyph), **kwargs)
def getmetrics(self, glyphs):
return self.font.getmetrics()
class BitmapFontWrapper:
def __init__(self, font):
self.font = font
self.max_height = 0
def getoffset(self, glyph):
return 0, 0
def getmask(self, glyph, **kwargs):
return self.font.getmask(str(glyph), **kwargs)
def getmetrics(self, glyphs):
max_height = 0
for glyph in glyphs:
mask = self.getmask(glyph, mode="1")
_, height = mask.size
max_height = max(max_height, height)
return max_height, 0
class EFont: class EFont:
def __init__(self, file, size, codepoints): def __init__(self, file, codepoints):
self.codepoints = codepoints self.codepoints = codepoints
path = file[CONF_PATH] self.font: Face = FONT_CACHE[file]
self.name = Path(path).name
ftype = file[CONF_TYPE]
if ftype == TYPE_LOCAL_BITMAP:
self.font = load_bitmap_font(path)
else:
self.font = load_ttf_font(path, size)
self.ascent, self.descent = self.font.getmetrics(codepoints)
def convert_bitmap_to_pillow_font(filepath):
from PIL import BdfFontFile, PcfFontFile
local_bitmap_font_file = external_files.compute_local_file_dir(
DOMAIN,
) / os.path.basename(filepath)
copy_file_if_changed(filepath, local_bitmap_font_file)
local_pil_font_file = local_bitmap_font_file.with_suffix(".pil")
with open(local_bitmap_font_file, "rb") as fp:
try:
try:
p = PcfFontFile.PcfFontFile(fp)
except SyntaxError:
fp.seek(0)
p = BdfFontFile.BdfFontFile(fp)
# Convert to pillow-formatted fonts, which have a .pil and .pbm extension.
p.save(local_pil_font_file)
except (SyntaxError, OSError) as err:
raise core.EsphomeError(
f"Failed to parse as bitmap font: '{filepath}': {err}"
)
return str(local_pil_font_file)
def load_bitmap_font(filepath):
from PIL import ImageFont
try:
font = ImageFont.load(str(filepath))
except Exception as e:
raise core.EsphomeError(f"Failed to load bitmap font file: {filepath}: {e}")
return BitmapFontWrapper(font)
def load_ttf_font(path, size):
from PIL import ImageFont
try:
font = ImageFont.truetype(str(path), size)
except Exception as e:
raise core.EsphomeError(f"Could not load TrueType file {path}: {e}")
return TrueTypeFontWrapper(font)
class GlyphInfo: class GlyphInfo:
def __init__(self, data_len, offset_x, offset_y, width, height): def __init__(self, data_len, advance, offset_x, offset_y, width, height):
self.data_len = data_len self.data_len = data_len
self.advance = advance
self.offset_x = offset_x self.offset_x = offset_x
self.offset_y = offset_y self.offset_y = offset_y
self.width = width self.width = width
@ -537,15 +505,14 @@ async def to_code(config):
} }
# get the codepoints from the glyphs key, flatten to a list of chrs and combine with the points from glyphsets # get the codepoints from the glyphs key, flatten to a list of chrs and combine with the points from glyphsets
point_set.update(flatten(config[CONF_GLYPHS])) point_set.update(flatten(config[CONF_GLYPHS]))
size = config[CONF_SIZE]
# Create the codepoint to font file map # Create the codepoint to font file map
base_font = EFont(config[CONF_FILE], size, point_set) base_font = FONT_CACHE[config[CONF_FILE]]
point_font_map: dict[str, EFont] = {c: base_font for c in point_set} point_font_map: dict[str, Face] = {c: base_font for c in point_set}
# process extras, updating the map and extending the codepoint list # process extras, updating the map and extending the codepoint list
for extra in config[CONF_EXTRAS]: for extra in config[CONF_EXTRAS]:
extra_points = flatten(extra[CONF_GLYPHS]) extra_points = flatten(extra[CONF_GLYPHS])
point_set.update(extra_points) point_set.update(extra_points)
extra_font = EFont(extra[CONF_FILE], size, extra_points) extra_font = FONT_CACHE[extra[CONF_FILE]]
point_font_map.update({c: extra_font for c in extra_points}) point_font_map.update({c: extra_font for c in extra_points})
codepoints = list(point_set) codepoints = list(point_set)
@ -553,28 +520,52 @@ async def to_code(config):
glyph_args = {} glyph_args = {}
data = [] data = []
bpp = config[CONF_BPP] bpp = config[CONF_BPP]
if bpp == 1: mode = ft_pixel_mode_grays
mode = "1"
scale = 1
else:
mode = "L"
scale = 256 // (1 << bpp) scale = 256 // (1 << bpp)
# create the data array for all glyphs # create the data array for all glyphs
for codepoint in codepoints: for codepoint in codepoints:
font = point_font_map[codepoint] font = point_font_map[codepoint]
mask = font.font.getmask(codepoint, mode=mode) if not font.has_fixed_sizes:
offset_x, offset_y = font.font.getoffset(codepoint) font.set_pixel_sizes(config[CONF_SIZE], 0)
width, height = mask.size font.load_char(codepoint)
font.glyph.render(mode)
width = font.glyph.bitmap.width
height = font.glyph.bitmap.rows
buffer = font.glyph.bitmap.buffer
pitch = font.glyph.bitmap.pitch
glyph_data = [0] * ((height * width * bpp + 7) // 8) glyph_data = [0] * ((height * width * bpp + 7) // 8)
src_mode = font.glyph.bitmap.pixel_mode
pos = 0 pos = 0
for y in range(height): for y in range(height):
for x in range(width): for x in range(width):
pixel = mask.getpixel((x, y)) // scale if src_mode == ft_pixel_mode_mono:
pixel = (
(1 << bpp) - 1
if buffer[y * pitch + x // 8] & (1 << (7 - x % 8))
else 0
)
else:
pixel = buffer[y * pitch + x] // scale
for bit_num in range(bpp): for bit_num in range(bpp):
if pixel & (1 << (bpp - bit_num - 1)): if pixel & (1 << (bpp - bit_num - 1)):
glyph_data[pos // 8] |= 0x80 >> (pos % 8) glyph_data[pos // 8] |= 0x80 >> (pos % 8)
pos += 1 pos += 1
glyph_args[codepoint] = GlyphInfo(len(data), offset_x, offset_y, width, height) ascender = font.size.ascender // 64
if ascender == 0:
if font.has_fixed_sizes:
ascender = font.available_sizes[0].height
else:
_LOGGER.error(
"Unable to determine ascender of font %s", config[CONF_FILE]
)
glyph_args[codepoint] = GlyphInfo(
len(data),
font.glyph.metrics.horiAdvance // 64,
font.glyph.bitmap_left,
ascender - font.glyph.bitmap_top,
width,
height,
)
data += glyph_data data += glyph_data
rhs = [HexInt(x) for x in data] rhs = [HexInt(x) for x in data]
@ -598,6 +589,7 @@ async def to_code(config):
f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}" f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}"
), ),
), ),
("advance", glyph_args[codepoint].advance),
("offset_x", glyph_args[codepoint].offset_x), ("offset_x", glyph_args[codepoint].offset_x),
("offset_y", glyph_args[codepoint].offset_y), ("offset_y", glyph_args[codepoint].offset_y),
("width", glyph_args[codepoint].width), ("width", glyph_args[codepoint].width),
@ -607,11 +599,19 @@ async def to_code(config):
glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer) glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
font_height = base_font.size.height // 64
ascender = base_font.size.ascender // 64
if font_height == 0:
if base_font.has_fixed_sizes:
font_height = base_font.available_sizes[0].height
ascender = font_height
else:
_LOGGER.error("Unable to determine height of font %s", config[CONF_FILE])
cg.new_Pvariable( cg.new_Pvariable(
config[CONF_ID], config[CONF_ID],
glyphs, glyphs,
len(glyph_initializer), len(glyph_initializer),
base_font.ascent, ascender,
base_font.ascent + base_font.descent, font_height,
bpp, bpp,
) )

View File

@ -81,7 +81,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in
if (glyph_n < 0) { if (glyph_n < 0) {
// Unknown char, skip // Unknown char, skip
if (!this->get_glyphs().empty()) if (!this->get_glyphs().empty())
x += this->get_glyphs()[0].glyph_data_->width; x += this->get_glyphs()[0].glyph_data_->advance;
i++; i++;
continue; continue;
} }
@ -92,7 +92,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in
} else { } else {
min_x = std::min(min_x, x + glyph.glyph_data_->offset_x); min_x = std::min(min_x, x + glyph.glyph_data_->offset_x);
} }
x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; x += glyph.glyph_data_->advance;
i += match_length; i += match_length;
has_char = true; has_char = true;
@ -111,7 +111,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
// Unknown char, skip // Unknown char, skip
ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
if (!this->get_glyphs().empty()) { if (!this->get_glyphs().empty()) {
uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->width; uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->advance;
display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color); display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color);
x_at += glyph_width; x_at += glyph_width;
} }
@ -161,7 +161,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
} }
} }
} }
x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; x_at += glyph.glyph_data_->advance;
i += match_length; i += match_length;
} }

View File

@ -15,6 +15,7 @@ class Font;
struct GlyphData { struct GlyphData {
const uint8_t *a_char; const uint8_t *a_char;
const uint8_t *data; const uint8_t *data;
int advance;
int offset_x; int offset_x;
int offset_y; int offset_y;
int width; int width;

View File

@ -19,7 +19,7 @@ static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, ui
const auto *gd = fe->get_glyph_data(unicode_letter); const auto *gd = fe->get_glyph_data(unicode_letter);
if (gd == nullptr) if (gd == nullptr)
return false; return false;
dsc->adv_w = gd->offset_x + gd->width; dsc->adv_w = gd->advance;
dsc->ofs_x = gd->offset_x; dsc->ofs_x = gd->offset_x;
dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline; dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline;
dsc->box_w = gd->width; dsc->box_w = gd->width;