mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +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 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, | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -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; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user