mirror of
https://github.com/esphome/esphome.git
synced 2025-03-15 07:08:20 +00:00
[font] Use freetype instead of Pillow for font rendering (#8300)
This commit is contained in:
parent
1029202848
commit
9bc4f68d87
@ -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,56 +295,59 @@ 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 = (
|
||||||
_LOGGER.debug("download_gfont: path=%s", 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()
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"Could not download font at {url}, please check the fonts exists "
|
||||||
|
f"at google fonts ({e})"
|
||||||
|
)
|
||||||
|
match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text)
|
||||||
|
if match is None:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"Could not extract ttf file from gfonts response for {name}, "
|
||||||
|
f"please report this."
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
ttf_url = match.group(1)
|
||||||
req = requests.get(url, timeout=external_files.NETWORK_TIMEOUT)
|
_LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
|
||||||
req.raise_for_status()
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"Could not download font at {url}, please check the fonts exists "
|
|
||||||
f"at google fonts ({e})"
|
|
||||||
)
|
|
||||||
match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text)
|
|
||||||
if match is None:
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"Could not extract ttf file from gfonts response for {name}, "
|
|
||||||
f"please report this."
|
|
||||||
)
|
|
||||||
|
|
||||||
ttf_url = match.group(1)
|
external_files.download_content(ttf_url, path)
|
||||||
_LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
|
# In case the remote file is not modified, the download_content function will return the existing file,
|
||||||
|
# so update the modification time to now.
|
||||||
external_files.download_content(ttf_url, path)
|
path.touch()
|
||||||
return FULLPATH_SCHEMA(path)
|
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(
|
||||||
|
{
|
||||||
|
CONF_TYPE: TYPE_LOCAL_BITMAP,
|
||||||
|
CONF_PATH: value,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
return {
|
|
||||||
CONF_TYPE: TYPE_LOCAL_BITMAP,
|
|
||||||
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 = 256 // (1 << bpp)
|
||||||
scale = 1
|
|
||||||
else:
|
|
||||||
mode = "L"
|
|
||||||
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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user