mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	[font] Use freetype instead of Pillow for font rendering (#8300)
This commit is contained in:
		| @@ -1,3 +1,4 @@ | ||||
| from collections.abc import MutableMapping | ||||
| import functools | ||||
| import hashlib | ||||
| import logging | ||||
| @@ -6,10 +7,10 @@ from pathlib import Path | ||||
| import re | ||||
|  | ||||
| import esphome_glyphsets as glyphsets | ||||
| import freetype | ||||
| from freetype import Face, ft_pixel_mode_grays, ft_pixel_mode_mono | ||||
| import requests | ||||
|  | ||||
| from esphome import core, external_files | ||||
| from esphome import external_files | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
| @@ -26,7 +27,7 @@ from esphome.const import ( | ||||
|     CONF_WEIGHT, | ||||
| ) | ||||
| 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__) | ||||
|  | ||||
| @@ -49,13 +50,42 @@ CONF_IGNORE_MISSING_GLYPHS = "ignore_missing_glyphs" | ||||
|  | ||||
|  | ||||
| # Cache loaded freetype fonts | ||||
| class FontCache(dict): | ||||
|     def __missing__(self, key): | ||||
|         try: | ||||
|             res = self[key] = freetype.Face(key) | ||||
|             return res | ||||
|         except freetype.FT_Exception as e: | ||||
|             raise cv.Invalid(f"Could not load Font file {key}: {e}") from e | ||||
| class FontCache(MutableMapping): | ||||
|     @staticmethod | ||||
|     def get_name(value): | ||||
|         if CONF_FAMILY in value: | ||||
|             return ( | ||||
|                 f"{value[CONF_FAMILY]}:{int(value[CONF_ITALIC])}:{value[CONF_WEIGHT]}" | ||||
|             ) | ||||
|         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() | ||||
| @@ -109,14 +139,14 @@ def check_missing_glyphs(file, codepoints, warning: bool = False): | ||||
|         ) | ||||
|         if count > 10: | ||||
|             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: | ||||
|             _LOGGER.warning(message) | ||||
|         else: | ||||
|             raise cv.Invalid(message) | ||||
|  | ||||
|  | ||||
| def validate_glyphs(config): | ||||
| def validate_font_config(config): | ||||
|     """ | ||||
|     Check for duplicate codepoints, then check that all requested codepoints actually | ||||
|     have glyphs defined in the appropriate font file. | ||||
| @@ -143,8 +173,6 @@ def validate_glyphs(config): | ||||
|     # Make setpoints and glyphspoints disjoint | ||||
|     setpoints.difference_update(glyphspoints) | ||||
|     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)): | ||||
|             raise cv.Invalid("Codepoints in bitmap fonts must be in the range 0-255") | ||||
|     else: | ||||
| @@ -154,13 +182,14 @@ def validate_glyphs(config): | ||||
|             points = {ord(x) for x in flatten(extra[CONF_GLYPHS])} | ||||
|             glyphspoints.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 | ||||
|         check_missing_glyphs(fileconf[CONF_PATH], glyphspoints) | ||||
|  | ||||
|         check_missing_glyphs(fileconf, glyphspoints) | ||||
|         # A missing glyph from a set is a warning. | ||||
|         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 | ||||
|     if not config[CONF_GLYPHS] and not config[CONF_GLYPHSETS]: | ||||
| @@ -168,17 +197,32 @@ def validate_glyphs(config): | ||||
|             config[CONF_GLYPHS] = [DEFAULT_GLYPHS] | ||||
|         else: | ||||
|             # set a default glyphset, intersected with what the font actually offers | ||||
|             font = FONT_CACHE[fileconf[CONF_PATH]] | ||||
|             font = FONT_CACHE[fileconf] | ||||
|             config[CONF_GLYPHS] = [ | ||||
|                 chr(x) | ||||
|                 for x in glyphsets.unicodes_per_glyphset(DEFAULT_GLYPHSET) | ||||
|                 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 | ||||
|  | ||||
|  | ||||
| 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): | ||||
| @@ -187,24 +231,40 @@ def validate_truetype_file(value): | ||||
|             f"Please unzip the font archive '{value}' first and then use the .ttf files inside." | ||||
|         ) | ||||
|     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)) | ||||
|  | ||||
|  | ||||
| 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_BITMAP = "local_bitmap" | ||||
| TYPE_GFONTS = "gfonts" | ||||
| TYPE_WEB = "web" | ||||
| LOCAL_SCHEMA = cv.Schema( | ||||
| LOCAL_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             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( | ||||
| @@ -235,27 +295,23 @@ def _compute_local_font_path(value: dict) -> Path: | ||||
|     h.update(url.encode()) | ||||
|     key = h.hexdigest()[:8] | ||||
|     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 | ||||
|  | ||||
|  | ||||
| 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): | ||||
|     if value in FONT_CACHE: | ||||
|         return value | ||||
|     name = ( | ||||
|         f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}" | ||||
|     ) | ||||
|     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) | ||||
|  | ||||
|         try: | ||||
|             req = requests.get(url, timeout=external_files.NETWORK_TIMEOUT) | ||||
|             req.raise_for_status() | ||||
| @@ -275,16 +331,23 @@ def download_gfont(value): | ||||
|         _LOGGER.debug("download_gfont: ttf_url=%s", ttf_url) | ||||
|  | ||||
|         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): | ||||
|     if value in FONT_CACHE: | ||||
|         return value | ||||
|     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) | ||||
|     _LOGGER.debug("download_web_font: path=%s", path) | ||||
|     return FULLPATH_SCHEMA(path) | ||||
|     FONT_CACHE[value] = path | ||||
|     return value | ||||
|  | ||||
|  | ||||
| EXTERNAL_FONT_SCHEMA = cv.Schema( | ||||
| @@ -340,14 +403,14 @@ def validate_file_shorthand(value): | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     if value.endswith(".pcf") or value.endswith(".bdf"): | ||||
|         value = convert_bitmap_to_pillow_font( | ||||
|             CORE.relative_config_path(cv.file_(value)) | ||||
|         ) | ||||
|         return { | ||||
|     extension = Path(value).suffix | ||||
|     if extension in BITMAP_EXTENSIONS: | ||||
|         return font_file_schema( | ||||
|             { | ||||
|                 CONF_TYPE: TYPE_LOCAL_BITMAP, | ||||
|                 CONF_PATH: value, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     return font_file_schema( | ||||
|         { | ||||
| @@ -391,7 +454,7 @@ FONT_SCHEMA = cv.Schema( | ||||
|             cv.one_of(*glyphsets.defined_glyphsets()) | ||||
|         ), | ||||
|         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_EXTRAS, default=[]): cv.ensure_list( | ||||
|             cv.Schema( | ||||
| @@ -406,114 +469,19 @@ FONT_SCHEMA = cv.Schema( | ||||
|     }, | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All(FONT_SCHEMA, validate_glyphs) | ||||
|  | ||||
|  | ||||
| # 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 | ||||
| CONFIG_SCHEMA = cv.All(FONT_SCHEMA, validate_font_config) | ||||
|  | ||||
|  | ||||
| class EFont: | ||||
|     def __init__(self, file, size, codepoints): | ||||
|     def __init__(self, file, codepoints): | ||||
|         self.codepoints = codepoints | ||||
|         path = file[CONF_PATH] | ||||
|         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) | ||||
|         self.font: Face = FONT_CACHE[file] | ||||
|  | ||||
|  | ||||
| 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.advance = advance | ||||
|         self.offset_x = offset_x | ||||
|         self.offset_y = offset_y | ||||
|         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 | ||||
|     point_set.update(flatten(config[CONF_GLYPHS])) | ||||
|     size = config[CONF_SIZE] | ||||
|     # Create the codepoint to font file map | ||||
|     base_font = EFont(config[CONF_FILE], size, point_set) | ||||
|     point_font_map: dict[str, EFont] = {c: base_font for c in point_set} | ||||
|     base_font = FONT_CACHE[config[CONF_FILE]] | ||||
|     point_font_map: dict[str, Face] = {c: base_font for c in point_set} | ||||
|     # process extras, updating the map and extending the codepoint list | ||||
|     for extra in config[CONF_EXTRAS]: | ||||
|         extra_points = flatten(extra[CONF_GLYPHS]) | ||||
|         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}) | ||||
|  | ||||
|     codepoints = list(point_set) | ||||
| @@ -553,28 +520,52 @@ async def to_code(config): | ||||
|     glyph_args = {} | ||||
|     data = [] | ||||
|     bpp = config[CONF_BPP] | ||||
|     if bpp == 1: | ||||
|         mode = "1" | ||||
|         scale = 1 | ||||
|     else: | ||||
|         mode = "L" | ||||
|     mode = ft_pixel_mode_grays | ||||
|     scale = 256 // (1 << bpp) | ||||
|     # create the data array for all glyphs | ||||
|     for codepoint in codepoints: | ||||
|         font = point_font_map[codepoint] | ||||
|         mask = font.font.getmask(codepoint, mode=mode) | ||||
|         offset_x, offset_y = font.font.getoffset(codepoint) | ||||
|         width, height = mask.size | ||||
|         if not font.has_fixed_sizes: | ||||
|             font.set_pixel_sizes(config[CONF_SIZE], 0) | ||||
|         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) | ||||
|         src_mode = font.glyph.bitmap.pixel_mode | ||||
|         pos = 0 | ||||
|         for y in range(height): | ||||
|             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): | ||||
|                     if pixel & (1 << (bpp - bit_num - 1)): | ||||
|                         glyph_data[pos // 8] |= 0x80 >> (pos % 8) | ||||
|                     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 | ||||
|  | ||||
|     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)}" | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("advance", glyph_args[codepoint].advance), | ||||
|                 ("offset_x", glyph_args[codepoint].offset_x), | ||||
|                 ("offset_y", glyph_args[codepoint].offset_y), | ||||
|                 ("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) | ||||
|  | ||||
|     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( | ||||
|         config[CONF_ID], | ||||
|         glyphs, | ||||
|         len(glyph_initializer), | ||||
|         base_font.ascent, | ||||
|         base_font.ascent + base_font.descent, | ||||
|         ascender, | ||||
|         font_height, | ||||
|         bpp, | ||||
|     ) | ||||
|   | ||||
| @@ -81,7 +81,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in | ||||
|     if (glyph_n < 0) { | ||||
|       // Unknown char, skip | ||||
|       if (!this->get_glyphs().empty()) | ||||
|         x += this->get_glyphs()[0].glyph_data_->width; | ||||
|         x += this->get_glyphs()[0].glyph_data_->advance; | ||||
|       i++; | ||||
|       continue; | ||||
|     } | ||||
| @@ -92,7 +92,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in | ||||
|     } else { | ||||
|       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; | ||||
|     has_char = true; | ||||
| @@ -111,7 +111,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo | ||||
|       // Unknown char, skip | ||||
|       ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); | ||||
|       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); | ||||
|         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; | ||||
|   } | ||||
|   | ||||
| @@ -15,6 +15,7 @@ class Font; | ||||
| struct GlyphData { | ||||
|   const uint8_t *a_char; | ||||
|   const uint8_t *data; | ||||
|   int advance; | ||||
|   int offset_x; | ||||
|   int offset_y; | ||||
|   int width; | ||||
|   | ||||
| @@ -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); | ||||
|   if (gd == nullptr) | ||||
|     return false; | ||||
|   dsc->adv_w = gd->offset_x + gd->width; | ||||
|   dsc->adv_w = gd->advance; | ||||
|   dsc->ofs_x = gd->offset_x; | ||||
|   dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline; | ||||
|   dsc->box_w = gd->width; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user