mirror of
https://github.com/esphome/esphome.git
synced 2025-03-14 06:38:17 +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 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(
|
||||
{
|
||||
cv.Required(CONF_PATH): validate_truetype_file,
|
||||
}
|
||||
LOCAL_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_PATH): validate_truetype_file,
|
||||
}
|
||||
),
|
||||
add_local_file,
|
||||
)
|
||||
|
||||
LOCAL_BITMAP_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_PATH): cv.file_,
|
||||
}
|
||||
LOCAL_BITMAP_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_PATH): validate_bitmap_file,
|
||||
}
|
||||
),
|
||||
add_local_file,
|
||||
)
|
||||
|
||||
FULLPATH_SCHEMA = cv.maybe_simple_value(
|
||||
@ -235,56 +295,59 @@ 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)
|
||||
_LOGGER.debug("download_gfont: path=%s", path)
|
||||
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:
|
||||
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."
|
||||
)
|
||||
ttf_url = match.group(1)
|
||||
_LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
|
||||
|
||||
ttf_url = match.group(1)
|
||||
_LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
|
||||
|
||||
external_files.download_content(ttf_url, path)
|
||||
return FULLPATH_SCHEMA(path)
|
||||
external_files.download_content(ttf_url, 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))
|
||||
extension = Path(value).suffix
|
||||
if extension in BITMAP_EXTENSIONS:
|
||||
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(
|
||||
{
|
||||
@ -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"
|
||||
scale = 256 // (1 << bpp)
|
||||
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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user