mirror of
https://github.com/esphome/esphome.git
synced 2024-12-21 13:04:54 +01:00
[font] Add support for "glyphsets" (#7429)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
This commit is contained in:
parent
8b7e061f3a
commit
a043022444
10 changed files with 7771 additions and 129 deletions
|
@ -40,25 +40,6 @@ RUN \
|
|||
libcairo2=1.16.0-7 \
|
||||
libmagic1=1:5.44-3 \
|
||||
patch=2.7.6-7 \
|
||||
&& ( \
|
||||
( \
|
||||
[ "$TARGETARCH$TARGETVARIANT" = "armv7" ] && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential=12.9 \
|
||||
python3-dev=3.11.2-1+b1 \
|
||||
zlib1g-dev=1:1.2.13.dfsg-1 \
|
||||
libjpeg-dev=1:2.1.5-2 \
|
||||
libfreetype-dev=2.12.1+dfsg-5+deb12u3 \
|
||||
libssl-dev=3.0.14-1~deb12u2 \
|
||||
libffi-dev=3.4.4-1 \
|
||||
libopenjp2-7=2.5.0-2 \
|
||||
libtiff6=4.5.0-6+deb12u1 \
|
||||
cargo=0.66.0+ds1-1 \
|
||||
pkg-config=1.8.1-1 \
|
||||
gcc-arm-linux-gnueabihf=4:12.2.0-3 \
|
||||
) \
|
||||
|| [ "$TARGETARCH$TARGETVARIANT" != "armv7" ] \
|
||||
) \
|
||||
&& rm -rf \
|
||||
/tmp/* \
|
||||
/var/{cache,log}/* \
|
||||
|
@ -97,15 +78,48 @@ RUN \
|
|||
# tmpfs is for https://github.com/rust-lang/cargo/issues/8719
|
||||
|
||||
COPY requirements.txt requirements_optional.txt /
|
||||
RUN --mount=type=tmpfs,target=/root/.cargo if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
|
||||
curl -L https://www.piwheels.org/cp311/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl -o /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \
|
||||
&& pip3 install --break-system-packages --no-cache-dir /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \
|
||||
&& rm /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \
|
||||
&& export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
|
||||
fi; \
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo \
|
||||
pip3 install \
|
||||
--break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt
|
||||
RUN --mount=type=tmpfs,target=/root/.cargo <<END-OF-RUN
|
||||
# Fail on any non-zero status
|
||||
set -e
|
||||
|
||||
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]
|
||||
then
|
||||
curl -L https://www.piwheels.org/cp311/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl -o /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl
|
||||
pip3 install --break-system-packages --no-cache-dir /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl
|
||||
rm /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl
|
||||
export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple";
|
||||
fi
|
||||
|
||||
# install build tools in case wheels are not available
|
||||
BUILD_DEPS="
|
||||
build-essential=12.9
|
||||
python3-dev=3.11.2-1+b1
|
||||
zlib1g-dev=1:1.2.13.dfsg-1
|
||||
libjpeg-dev=1:2.1.5-2
|
||||
libfreetype-dev=2.12.1+dfsg-5+deb12u3
|
||||
libssl-dev=3.0.14-1~deb12u2
|
||||
libffi-dev=3.4.4-1
|
||||
libopenjp2-7=2.5.0-2
|
||||
libtiff6=4.5.0-6+deb12u1
|
||||
cargo=0.66.0+ds1-1
|
||||
pkg-config=1.8.1-1
|
||||
"
|
||||
if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ] || [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]
|
||||
then
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends $BUILD_DEPS
|
||||
fi
|
||||
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo
|
||||
pip3 install --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt
|
||||
|
||||
if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ] || [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]
|
||||
then
|
||||
apt-get remove -y --purge --auto-remove $BUILD_DEPS
|
||||
rm -rf /tmp/* /var/{cache,log}/* /var/lib/apt/lists/*
|
||||
fi
|
||||
END-OF-RUN
|
||||
|
||||
|
||||
COPY script/platformio_install_deps.py platformio.ini /
|
||||
RUN /platformio_install_deps.py /platformio.ini --libraries
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from collections.abc import Iterable
|
||||
import functools
|
||||
import hashlib
|
||||
import logging
|
||||
|
@ -5,6 +6,8 @@ import os
|
|||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import freetype
|
||||
import glyphsets
|
||||
from packaging import version
|
||||
import requests
|
||||
|
||||
|
@ -43,6 +46,18 @@ GlyphData = font_ns.struct("GlyphData")
|
|||
CONF_BPP = "bpp"
|
||||
CONF_EXTRAS = "extras"
|
||||
CONF_FONTS = "fonts"
|
||||
CONF_GLYPHSETS = "glyphsets"
|
||||
CONF_IGNORE_MISSING_GLYPHS = "ignore_missing_glyphs"
|
||||
|
||||
|
||||
# Cache loaded freetype fonts
|
||||
class FontCache(dict):
|
||||
def __missing__(self, key):
|
||||
res = self[key] = freetype.Face(key)
|
||||
return res
|
||||
|
||||
|
||||
FONT_CACHE = FontCache()
|
||||
|
||||
|
||||
def glyph_comparator(x, y):
|
||||
|
@ -59,36 +74,106 @@ def glyph_comparator(x, y):
|
|||
return -1
|
||||
if len(x_) > len(y_):
|
||||
return 1
|
||||
raise cv.Invalid(f"Found duplicate glyph {x}")
|
||||
return 0
|
||||
|
||||
|
||||
def validate_glyphs(value):
|
||||
if isinstance(value, list):
|
||||
value = cv.Schema([cv.string])(value)
|
||||
value = cv.Schema([cv.string])(list(value))
|
||||
def flatten(lists) -> list:
|
||||
"""
|
||||
Given a list of lists, flatten it to a single list of all elements of all lists.
|
||||
This wraps itertools.chain.from_iterable to make it more readable, and return a list
|
||||
rather than a single use iterable.
|
||||
"""
|
||||
from itertools import chain
|
||||
|
||||
value.sort(key=functools.cmp_to_key(glyph_comparator))
|
||||
return value
|
||||
return list(chain.from_iterable(lists))
|
||||
|
||||
|
||||
font_map = {}
|
||||
def check_missing_glyphs(file, codepoints: Iterable, warning: bool = False):
|
||||
"""
|
||||
Check that the given font file actually contains the requested glyphs
|
||||
:param file: A Truetype font file
|
||||
:param codepoints: A list of codepoints to check
|
||||
:param warning: If true, log a warning instead of raising an exception
|
||||
"""
|
||||
|
||||
|
||||
def merge_glyphs(config):
|
||||
glyphs = []
|
||||
glyphs.extend(config[CONF_GLYPHS])
|
||||
font_list = [(EFont(config[CONF_FILE], config[CONF_SIZE], config[CONF_GLYPHS]))]
|
||||
if extras := config.get(CONF_EXTRAS):
|
||||
extra_fonts = list(
|
||||
map(
|
||||
lambda x: EFont(x[CONF_FILE], config[CONF_SIZE], x[CONF_GLYPHS]), extras
|
||||
)
|
||||
font = FONT_CACHE[file]
|
||||
missing = [chr(x) for x in codepoints if font.get_char_index(x) == 0]
|
||||
if missing:
|
||||
# Only list up to 10 missing glyphs
|
||||
missing.sort(key=functools.cmp_to_key(glyph_comparator))
|
||||
count = len(missing)
|
||||
missing = missing[:10]
|
||||
missing_str = "\n ".join(
|
||||
f"{x} ({x.encode('unicode_escape')})" for x in missing
|
||||
)
|
||||
font_list.extend(extra_fonts)
|
||||
for extra in extras:
|
||||
glyphs.extend(extra[CONF_GLYPHS])
|
||||
validate_glyphs(glyphs)
|
||||
font_map[config[CONF_ID]] = font_list
|
||||
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}"
|
||||
if warning:
|
||||
_LOGGER.warning(message)
|
||||
else:
|
||||
raise cv.Invalid(message)
|
||||
|
||||
|
||||
def validate_glyphs(config):
|
||||
"""
|
||||
Check for duplicate codepoints, then check that all requested codepoints actually
|
||||
have glyphs defined in the appropriate font file.
|
||||
"""
|
||||
|
||||
# Collect all glyph codepoints and flatten to a list of chars
|
||||
glyphspoints = flatten(
|
||||
[x[CONF_GLYPHS] for x in config[CONF_EXTRAS]] + config[CONF_GLYPHS]
|
||||
)
|
||||
# Convert a list of strings to a list of chars (one char strings)
|
||||
glyphspoints = flatten([list(x) for x in glyphspoints])
|
||||
if len(set(glyphspoints)) != len(glyphspoints):
|
||||
duplicates = {x for x in glyphspoints if glyphspoints.count(x) > 1}
|
||||
dup_str = ", ".join(f"{x} ({x.encode('unicode_escape')})" for x in duplicates)
|
||||
raise cv.Invalid(
|
||||
f"Found duplicate glyph{'s' if len(duplicates) != 1 else ''}: {dup_str}"
|
||||
)
|
||||
# convert to codepoints
|
||||
glyphspoints = {ord(x) for x in glyphspoints}
|
||||
fileconf = config[CONF_FILE]
|
||||
setpoints = set(
|
||||
flatten([glyphsets.unicodes_per_glyphset(x) for x in config[CONF_GLYPHSETS]])
|
||||
)
|
||||
# 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:
|
||||
# for TT fonts, check that glyphs are actually present
|
||||
# Check extras against their own font, exclude from parent font codepoints
|
||||
for extra in config[CONF_EXTRAS]:
|
||||
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)
|
||||
|
||||
# A named glyph that can't be provided is an error
|
||||
check_missing_glyphs(fileconf[CONF_PATH], 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)
|
||||
|
||||
# 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 fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP:
|
||||
config[CONF_GLYPHS] = [DEFAULT_GLYPHS]
|
||||
else:
|
||||
# set a default glyphset, intersected with what the font actually offers
|
||||
font = FONT_CACHE[fileconf[CONF_PATH]]
|
||||
config[CONF_GLYPHS] = [
|
||||
chr(x)
|
||||
for x in glyphsets.unicodes_per_glyphset(DEFAULT_GLYPHSET)
|
||||
if font.get_char_index(x) != 0
|
||||
]
|
||||
|
||||
return config
|
||||
|
||||
|
||||
|
@ -120,7 +205,7 @@ def validate_truetype_file(value):
|
|||
)
|
||||
if not any(map(value.lower().endswith, FONT_EXTENSIONS)):
|
||||
raise cv.Invalid(f"Only {FONT_EXTENSIONS} files are supported.")
|
||||
return cv.file_(value)
|
||||
return CORE.relative_config_path(cv.file_(value))
|
||||
|
||||
|
||||
TYPE_LOCAL = "local"
|
||||
|
@ -139,6 +224,10 @@ LOCAL_BITMAP_SCHEMA = cv.Schema(
|
|||
}
|
||||
)
|
||||
|
||||
FULLPATH_SCHEMA = cv.maybe_simple_value(
|
||||
{cv.Required(CONF_PATH): cv.string}, key=CONF_PATH
|
||||
)
|
||||
|
||||
CONF_ITALIC = "italic"
|
||||
FONT_WEIGHTS = {
|
||||
"thin": 100,
|
||||
|
@ -167,13 +256,13 @@ def _compute_local_font_path(value: dict) -> Path:
|
|||
return base_dir / key
|
||||
|
||||
|
||||
def get_font_path(value, type) -> Path:
|
||||
if type == TYPE_GFONTS:
|
||||
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 type == TYPE_WEB:
|
||||
if font_type == TYPE_WEB:
|
||||
return _compute_local_font_path(value) / "font.ttf"
|
||||
return None
|
||||
assert False
|
||||
|
||||
|
||||
def download_gfont(value):
|
||||
|
@ -203,7 +292,7 @@ def download_gfont(value):
|
|||
_LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
|
||||
|
||||
external_files.download_content(ttf_url, path)
|
||||
return value
|
||||
return FULLPATH_SCHEMA(path)
|
||||
|
||||
|
||||
def download_web_font(value):
|
||||
|
@ -212,7 +301,7 @@ def download_web_font(value):
|
|||
|
||||
external_files.download_content(url, path)
|
||||
_LOGGER.debug("download_web_font: path=%s", path)
|
||||
return value
|
||||
return FULLPATH_SCHEMA(path)
|
||||
|
||||
|
||||
EXTERNAL_FONT_SCHEMA = cv.Schema(
|
||||
|
@ -225,7 +314,6 @@ EXTERNAL_FONT_SCHEMA = cv.Schema(
|
|||
}
|
||||
)
|
||||
|
||||
|
||||
GFONTS_SCHEMA = cv.All(
|
||||
EXTERNAL_FONT_SCHEMA.extend(
|
||||
{
|
||||
|
@ -259,10 +347,10 @@ def validate_file_shorthand(value):
|
|||
}
|
||||
if weight is not None:
|
||||
data[CONF_WEIGHT] = weight[1:]
|
||||
return FILE_SCHEMA(data)
|
||||
return font_file_schema(data)
|
||||
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
return FILE_SCHEMA(
|
||||
return font_file_schema(
|
||||
{
|
||||
CONF_TYPE: TYPE_WEB,
|
||||
CONF_URL: value,
|
||||
|
@ -270,14 +358,15 @@ def validate_file_shorthand(value):
|
|||
)
|
||||
|
||||
if value.endswith(".pcf") or value.endswith(".bdf"):
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_TYPE: TYPE_LOCAL_BITMAP,
|
||||
CONF_PATH: value,
|
||||
}
|
||||
value = convert_bitmap_to_pillow_font(
|
||||
CORE.relative_config_path(cv.file_(value))
|
||||
)
|
||||
return {
|
||||
CONF_TYPE: TYPE_LOCAL_BITMAP,
|
||||
CONF_PATH: value,
|
||||
}
|
||||
|
||||
return FILE_SCHEMA(
|
||||
return font_file_schema(
|
||||
{
|
||||
CONF_TYPE: TYPE_LOCAL,
|
||||
CONF_PATH: value,
|
||||
|
@ -295,31 +384,35 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
|
|||
)
|
||||
|
||||
|
||||
def _file_schema(value):
|
||||
def font_file_schema(value):
|
||||
if isinstance(value, str):
|
||||
return validate_file_shorthand(value)
|
||||
return TYPED_FILE_SCHEMA(value)
|
||||
|
||||
|
||||
FILE_SCHEMA = cv.All(_file_schema)
|
||||
# Default if no glyphs or glyphsets are provided
|
||||
DEFAULT_GLYPHSET = "GF_Latin_Kernel"
|
||||
# default for bitmap fonts
|
||||
DEFAULT_GLYPHS = ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz<C2><B0>'
|
||||
|
||||
DEFAULT_GLYPHS = (
|
||||
' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
|
||||
)
|
||||
CONF_RAW_GLYPH_ID = "raw_glyph_id"
|
||||
|
||||
FONT_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(Font),
|
||||
cv.Required(CONF_FILE): FILE_SCHEMA,
|
||||
cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs,
|
||||
cv.Required(CONF_FILE): font_file_schema,
|
||||
cv.Optional(CONF_GLYPHS, default=[]): cv.ensure_list(cv.string_strict),
|
||||
cv.Optional(CONF_GLYPHSETS, default=[]): cv.ensure_list(
|
||||
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_BPP, default=1): cv.one_of(1, 2, 4, 8),
|
||||
cv.Optional(CONF_EXTRAS): cv.ensure_list(
|
||||
cv.Optional(CONF_EXTRAS, default=[]): cv.ensure_list(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_FILE): FILE_SCHEMA,
|
||||
cv.Required(CONF_GLYPHS): validate_glyphs,
|
||||
cv.Required(CONF_FILE): font_file_schema,
|
||||
cv.Required(CONF_GLYPHS): cv.ensure_list(cv.string_strict),
|
||||
}
|
||||
)
|
||||
),
|
||||
|
@ -328,7 +421,7 @@ FONT_SCHEMA = cv.Schema(
|
|||
},
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, merge_glyphs)
|
||||
CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, validate_glyphs)
|
||||
|
||||
|
||||
# PIL doesn't provide a consistent interface for both TrueType and bitmap
|
||||
|
@ -367,28 +460,20 @@ class BitmapFontWrapper:
|
|||
mask = self.getmask(glyph, mode="1")
|
||||
_, height = mask.size
|
||||
max_height = max(max_height, height)
|
||||
return (max_height, 0)
|
||||
return max_height, 0
|
||||
|
||||
|
||||
class EFont:
|
||||
def __init__(self, file, size, glyphs):
|
||||
self.glyphs = glyphs
|
||||
def __init__(self, file, size, codepoints):
|
||||
self.codepoints = codepoints
|
||||
path = file[CONF_PATH]
|
||||
self.name = Path(path).name
|
||||
ftype = file[CONF_TYPE]
|
||||
if ftype == TYPE_LOCAL_BITMAP:
|
||||
font = load_bitmap_font(CORE.relative_config_path(file[CONF_PATH]))
|
||||
elif ftype == TYPE_LOCAL:
|
||||
path = CORE.relative_config_path(file[CONF_PATH])
|
||||
font = load_ttf_font(path, size)
|
||||
elif ftype in (TYPE_GFONTS, TYPE_WEB):
|
||||
path = get_font_path(file, ftype)
|
||||
font = load_ttf_font(path, size)
|
||||
self.font = load_bitmap_font(path)
|
||||
else:
|
||||
raise cv.Invalid(f"Could not load font: unknown type: {ftype}")
|
||||
self.font = font
|
||||
self.ascent, self.descent = font.getmetrics(glyphs)
|
||||
|
||||
def has_glyph(self, glyph):
|
||||
return glyph in self.glyphs
|
||||
self.font = load_ttf_font(path, size)
|
||||
self.ascent, self.descent = self.font.getmetrics(codepoints)
|
||||
|
||||
|
||||
def convert_bitmap_to_pillow_font(filepath):
|
||||
|
@ -400,6 +485,7 @@ def convert_bitmap_to_pillow_font(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:
|
||||
|
@ -409,28 +495,22 @@ def convert_bitmap_to_pillow_font(filepath):
|
|||
p = BdfFontFile.BdfFontFile(fp)
|
||||
|
||||
# Convert to pillow-formatted fonts, which have a .pil and .pbm extension.
|
||||
p.save(local_bitmap_font_file)
|
||||
p.save(local_pil_font_file)
|
||||
except (SyntaxError, OSError) as err:
|
||||
raise core.EsphomeError(
|
||||
f"Failed to parse as bitmap font: '{filepath}': {err}"
|
||||
)
|
||||
|
||||
local_pil_font_file = os.path.splitext(local_bitmap_font_file)[0] + ".pil"
|
||||
return cv.file_(local_pil_font_file)
|
||||
return str(local_pil_font_file)
|
||||
|
||||
|
||||
def load_bitmap_font(filepath):
|
||||
from PIL import ImageFont
|
||||
|
||||
# Convert bpf and pcf files to pillow fonts, first.
|
||||
pil_font_path = convert_bitmap_to_pillow_font(filepath)
|
||||
|
||||
try:
|
||||
font = ImageFont.load(str(pil_font_path))
|
||||
font = ImageFont.load(str(filepath))
|
||||
except Exception as e:
|
||||
raise core.EsphomeError(
|
||||
f"Failed to load bitmap font file: {pil_font_path} : {e}"
|
||||
)
|
||||
raise core.EsphomeError(f"Failed to load bitmap font file: {filepath}: {e}")
|
||||
|
||||
return BitmapFontWrapper(font)
|
||||
|
||||
|
@ -441,7 +521,7 @@ def load_ttf_font(path, size):
|
|||
try:
|
||||
font = ImageFont.truetype(str(path), size)
|
||||
except Exception as e:
|
||||
raise core.EsphomeError(f"Could not load truetype file {path}: {e}")
|
||||
raise core.EsphomeError(f"Could not load TrueType file {path}: {e}")
|
||||
|
||||
return TrueTypeFontWrapper(font)
|
||||
|
||||
|
@ -456,14 +536,35 @@ class GlyphInfo:
|
|||
|
||||
|
||||
async def to_code(config):
|
||||
glyph_to_font_map = {}
|
||||
font_list = font_map[config[CONF_ID]]
|
||||
glyphs = []
|
||||
for font in font_list:
|
||||
glyphs.extend(font.glyphs)
|
||||
for glyph in font.glyphs:
|
||||
glyph_to_font_map[glyph] = font
|
||||
glyphs.sort(key=functools.cmp_to_key(glyph_comparator))
|
||||
"""
|
||||
Collect all glyph codepoints, construct a map from a codepoint to a font file.
|
||||
Codepoints are either explicit (glyphs key in top level or extras) or part of a glyphset.
|
||||
Codepoints listed in extras use the extra font and override codepoints from glyphsets.
|
||||
Achieve this by processing the base codepoints first, then the extras
|
||||
"""
|
||||
|
||||
# get the codepoints from glyphsets and flatten to a set of chrs.
|
||||
point_set: set[str] = {
|
||||
chr(x)
|
||||
for x in flatten(
|
||||
[glyphsets.unicodes_per_glyphset(x) for x in config[CONF_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]))
|
||||
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}
|
||||
# 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)
|
||||
point_font_map.update({c: extra_font for c in extra_points})
|
||||
|
||||
codepoints = list(point_set)
|
||||
codepoints.sort(key=functools.cmp_to_key(glyph_comparator))
|
||||
glyph_args = {}
|
||||
data = []
|
||||
bpp = config[CONF_BPP]
|
||||
|
@ -473,10 +574,11 @@ async def to_code(config):
|
|||
else:
|
||||
mode = "L"
|
||||
scale = 256 // (1 << bpp)
|
||||
for glyph in glyphs:
|
||||
font = glyph_to_font_map[glyph].font
|
||||
mask = font.getmask(glyph, mode=mode)
|
||||
offset_x, offset_y = font.getoffset(glyph)
|
||||
# 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
|
||||
glyph_data = [0] * ((height * width * bpp + 7) // 8)
|
||||
pos = 0
|
||||
|
@ -487,31 +589,34 @@ async def to_code(config):
|
|||
if pixel & (1 << (bpp - bit_num - 1)):
|
||||
glyph_data[pos // 8] |= 0x80 >> (pos % 8)
|
||||
pos += 1
|
||||
glyph_args[glyph] = GlyphInfo(len(data), offset_x, offset_y, width, height)
|
||||
glyph_args[codepoint] = GlyphInfo(len(data), offset_x, offset_y, width, height)
|
||||
data += glyph_data
|
||||
|
||||
rhs = [HexInt(x) for x in data]
|
||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
|
||||
# Create the glyph table that points to data in the above array.
|
||||
glyph_initializer = []
|
||||
for glyph in glyphs:
|
||||
for codepoint in codepoints:
|
||||
glyph_initializer.append(
|
||||
cg.StructInitializer(
|
||||
GlyphData,
|
||||
(
|
||||
"a_char",
|
||||
cg.RawExpression(f"(const uint8_t *){cpp_string_escape(glyph)}"),
|
||||
cg.RawExpression(
|
||||
f"(const uint8_t *){cpp_string_escape(codepoint)}"
|
||||
),
|
||||
),
|
||||
(
|
||||
"data",
|
||||
cg.RawExpression(
|
||||
f"{str(prog_arr)} + {str(glyph_args[glyph].data_len)}"
|
||||
f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}"
|
||||
),
|
||||
),
|
||||
("offset_x", glyph_args[glyph].offset_x),
|
||||
("offset_y", glyph_args[glyph].offset_y),
|
||||
("width", glyph_args[glyph].width),
|
||||
("height", glyph_args[glyph].height),
|
||||
("offset_x", glyph_args[codepoint].offset_x),
|
||||
("offset_y", glyph_args[codepoint].offset_y),
|
||||
("width", glyph_args[codepoint].width),
|
||||
("height", glyph_args[codepoint].height),
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -521,7 +626,7 @@ async def to_code(config):
|
|||
config[CONF_ID],
|
||||
glyphs,
|
||||
len(glyph_initializer),
|
||||
font_list[0].ascent,
|
||||
font_list[0].ascent + font_list[0].descent,
|
||||
base_font.ascent,
|
||||
base_font.ascent + base_font.descent,
|
||||
bpp,
|
||||
)
|
||||
|
|
|
@ -17,6 +17,9 @@ aioesphomeapi==24.6.2
|
|||
zeroconf==0.132.2
|
||||
puremagic==1.27
|
||||
ruamel.yaml==0.18.6 # dashboard_import
|
||||
glyphsets==1.0.0
|
||||
pillow==10.4.0
|
||||
freetype-py==2.5.1
|
||||
|
||||
# esp-idf requires this, but doesn't bundle it by default
|
||||
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
pillow==10.4.0
|
||||
cairosvg==2.7.1
|
||||
|
|
|
@ -58,7 +58,7 @@ file_types = (
|
|||
)
|
||||
cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc")
|
||||
py_include = ("*.py",)
|
||||
ignore_types = (".ico", ".png", ".woff", ".woff2", "", ".ttf", ".otf")
|
||||
ignore_types = (".ico", ".png", ".woff", ".woff2", "", ".ttf", ".otf", ".pcf")
|
||||
|
||||
LINT_FILE_CHECKS = []
|
||||
LINT_CONTENT_CHECKS = []
|
||||
|
|
2
tests/components/font/.gitattributes
vendored
Normal file
2
tests/components/font/.gitattributes
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*.pcf -text
|
||||
|
7461
tests/components/font/MatrixChunky8X.bdf
Normal file
7461
tests/components/font/MatrixChunky8X.bdf
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,12 @@
|
|||
font:
|
||||
- file:
|
||||
type: gfonts
|
||||
family: "Roboto"
|
||||
weight: bold
|
||||
italic: true
|
||||
size: 32
|
||||
id: roboto32
|
||||
|
||||
- file: "gfonts://Roboto"
|
||||
id: roboto
|
||||
size: 20
|
||||
|
@ -9,6 +17,10 @@ font:
|
|||
- file: "gfonts://Roboto"
|
||||
id: roboto_web
|
||||
size: 20
|
||||
- file: "gfonts://Roboto"
|
||||
id: roboto_greek
|
||||
size: 20
|
||||
glyphs: ["\u0300", "\u00C5", "\U000000C7"]
|
||||
- file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf"
|
||||
id: monocraft
|
||||
size: 20
|
||||
|
@ -20,6 +32,17 @@ font:
|
|||
- file: $component_dir/Monocraft.ttf
|
||||
id: monocraft3
|
||||
size: 28
|
||||
- file: $component_dir/MatrixChunky8X.bdf
|
||||
id: special_font
|
||||
glyphs:
|
||||
- '"'
|
||||
- "'"
|
||||
- '#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz°'
|
||||
|
||||
- file: $component_dir/MatrixChunky8X.bdf
|
||||
id: default_font
|
||||
- file: $component_dir/x11.pcf
|
||||
id: pcf_font
|
||||
|
||||
i2c:
|
||||
scl: ${i2c_scl}
|
||||
|
@ -36,3 +59,4 @@ display:
|
|||
it.print(0, 40, id(monocraft), "Hello, World!");
|
||||
it.print(0, 60, id(monocraft2), "Hello, World!");
|
||||
it.print(0, 80, id(monocraft3), "Hello, World!");
|
||||
it.print(0, 100, id(roboto_greek), "Hello κόσμε!");
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
font:
|
||||
- file:
|
||||
type: gfonts
|
||||
family: "Roboto"
|
||||
weight: bold
|
||||
italic: true
|
||||
size: 32
|
||||
id: roboto32
|
||||
|
||||
- file: "gfonts://Roboto"
|
||||
id: roboto
|
||||
size: 20
|
||||
|
@ -9,6 +17,10 @@ font:
|
|||
- file: "gfonts://Roboto"
|
||||
id: roboto_web
|
||||
size: 20
|
||||
- file: "gfonts://Roboto"
|
||||
id: roboto_greek
|
||||
size: 20
|
||||
glyphs: ["\u0300", "\u00C5", "\U000000C7"]
|
||||
- file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf"
|
||||
id: monocraft
|
||||
size: 20
|
||||
|
@ -20,4 +32,26 @@ font:
|
|||
- file: $component_dir/Monocraft.ttf
|
||||
id: monocraft3
|
||||
size: 28
|
||||
- file: $component_dir/MatrixChunky8X.bdf
|
||||
id: special_font
|
||||
glyphs:
|
||||
- '"'
|
||||
- "'"
|
||||
- '#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz°'
|
||||
|
||||
- file: $component_dir/MatrixChunky8X.bdf
|
||||
id: default_font
|
||||
|
||||
display:
|
||||
- platform: sdl
|
||||
id: sdl_display
|
||||
dimensions:
|
||||
width: 800
|
||||
height: 600
|
||||
lambda: |-
|
||||
it.print(0, 0, id(roboto), "Hello, World!");
|
||||
it.print(0, 20, id(roboto_web), "Hello, World!");
|
||||
it.print(0, 40, id(roboto_greek), "Hello κόσμε!");
|
||||
it.print(0, 60, id(monocraft), "Hello, World!");
|
||||
it.print(0, 80, id(monocraft2), "Hello, World!");
|
||||
it.print(0, 100, id(monocraft3), "Hello, World!");
|
||||
|
|
BIN
tests/components/font/x11.pcf
Normal file
BIN
tests/components/font/x11.pcf
Normal file
Binary file not shown.
Loading…
Reference in a new issue