[font] Add support for "glyphsets" (#7429)

Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
This commit is contained in:
Faidon Liambotis 2024-10-31 05:36:23 +02:00 committed by GitHub
parent 8b7e061f3a
commit a043022444
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 7771 additions and 129 deletions

View file

@ -40,25 +40,6 @@ RUN \
libcairo2=1.16.0-7 \ libcairo2=1.16.0-7 \
libmagic1=1:5.44-3 \ libmagic1=1:5.44-3 \
patch=2.7.6-7 \ 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 \ && rm -rf \
/tmp/* \ /tmp/* \
/var/{cache,log}/* \ /var/{cache,log}/* \
@ -97,15 +78,48 @@ RUN \
# tmpfs is for https://github.com/rust-lang/cargo/issues/8719 # tmpfs is for https://github.com/rust-lang/cargo/issues/8719
COPY requirements.txt requirements_optional.txt / COPY requirements.txt requirements_optional.txt /
RUN --mount=type=tmpfs,target=/root/.cargo if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ RUN --mount=type=tmpfs,target=/root/.cargo <<END-OF-RUN
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 \ # Fail on any non-zero status
&& pip3 install --break-system-packages --no-cache-dir /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \ set -e
&& rm /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \
&& export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \ if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]
fi; \ then
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo \ 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 \ pip3 install --break-system-packages --no-cache-dir /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl
--break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt 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 / COPY script/platformio_install_deps.py platformio.ini /
RUN /platformio_install_deps.py /platformio.ini --libraries RUN /platformio_install_deps.py /platformio.ini --libraries

View file

@ -1,3 +1,4 @@
from collections.abc import Iterable
import functools import functools
import hashlib import hashlib
import logging import logging
@ -5,6 +6,8 @@ import os
from pathlib import Path from pathlib import Path
import re import re
import freetype
import glyphsets
from packaging import version from packaging import version
import requests import requests
@ -43,6 +46,18 @@ GlyphData = font_ns.struct("GlyphData")
CONF_BPP = "bpp" CONF_BPP = "bpp"
CONF_EXTRAS = "extras" CONF_EXTRAS = "extras"
CONF_FONTS = "fonts" 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): def glyph_comparator(x, y):
@ -59,36 +74,106 @@ def glyph_comparator(x, y):
return -1 return -1
if len(x_) > len(y_): if len(x_) > len(y_):
return 1 return 1
raise cv.Invalid(f"Found duplicate glyph {x}") return 0
def validate_glyphs(value): def flatten(lists) -> list:
if isinstance(value, list): """
value = cv.Schema([cv.string])(value) Given a list of lists, flatten it to a single list of all elements of all lists.
value = cv.Schema([cv.string])(list(value)) 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 list(chain.from_iterable(lists))
return value
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
"""
font = FONT_CACHE[file]
def merge_glyphs(config): missing = [chr(x) for x in codepoints if font.get_char_index(x) == 0]
glyphs = [] if missing:
glyphs.extend(config[CONF_GLYPHS]) # Only list up to 10 missing glyphs
font_list = [(EFont(config[CONF_FILE], config[CONF_SIZE], config[CONF_GLYPHS]))] missing.sort(key=functools.cmp_to_key(glyph_comparator))
if extras := config.get(CONF_EXTRAS): count = len(missing)
extra_fonts = list( missing = missing[:10]
map( missing_str = "\n ".join(
lambda x: EFont(x[CONF_FILE], config[CONF_SIZE], x[CONF_GLYPHS]), extras f"{x} ({x.encode('unicode_escape')})" for x in missing
)
) )
font_list.extend(extra_fonts) if count > 10:
for extra in extras: missing_str += f"\n and {count - 10} more."
glyphs.extend(extra[CONF_GLYPHS]) message = f"Font {Path(file).name} is missing {count} glyph{'s' if count != 1 else ''}:\n {missing_str}"
validate_glyphs(glyphs) if warning:
font_map[config[CONF_ID]] = font_list _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 return config
@ -120,7 +205,7 @@ def validate_truetype_file(value):
) )
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 {FONT_EXTENSIONS} files are supported.")
return cv.file_(value) return CORE.relative_config_path(cv.file_(value))
TYPE_LOCAL = "local" 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" CONF_ITALIC = "italic"
FONT_WEIGHTS = { FONT_WEIGHTS = {
"thin": 100, "thin": 100,
@ -167,13 +256,13 @@ def _compute_local_font_path(value: dict) -> Path:
return base_dir / key return base_dir / key
def get_font_path(value, type) -> Path: def get_font_path(value, font_type) -> Path:
if type == TYPE_GFONTS: if font_type == TYPE_GFONTS:
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1" name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1"
return external_files.compute_local_file_dir(DOMAIN) / f"{name}.ttf" 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 _compute_local_font_path(value) / "font.ttf"
return None assert False
def download_gfont(value): def download_gfont(value):
@ -203,7 +292,7 @@ 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 value return FULLPATH_SCHEMA(path)
def download_web_font(value): def download_web_font(value):
@ -212,7 +301,7 @@ def download_web_font(value):
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 value return FULLPATH_SCHEMA(path)
EXTERNAL_FONT_SCHEMA = cv.Schema( EXTERNAL_FONT_SCHEMA = cv.Schema(
@ -225,7 +314,6 @@ EXTERNAL_FONT_SCHEMA = cv.Schema(
} }
) )
GFONTS_SCHEMA = cv.All( GFONTS_SCHEMA = cv.All(
EXTERNAL_FONT_SCHEMA.extend( EXTERNAL_FONT_SCHEMA.extend(
{ {
@ -259,10 +347,10 @@ def validate_file_shorthand(value):
} }
if weight is not None: if weight is not None:
data[CONF_WEIGHT] = weight[1:] data[CONF_WEIGHT] = weight[1:]
return FILE_SCHEMA(data) return font_file_schema(data)
if value.startswith("http://") or value.startswith("https://"): if value.startswith("http://") or value.startswith("https://"):
return FILE_SCHEMA( return font_file_schema(
{ {
CONF_TYPE: TYPE_WEB, CONF_TYPE: TYPE_WEB,
CONF_URL: value, CONF_URL: value,
@ -270,14 +358,15 @@ def validate_file_shorthand(value):
) )
if value.endswith(".pcf") or value.endswith(".bdf"): if value.endswith(".pcf") or value.endswith(".bdf"):
return FILE_SCHEMA( value = convert_bitmap_to_pillow_font(
{ CORE.relative_config_path(cv.file_(value))
CONF_TYPE: TYPE_LOCAL_BITMAP,
CONF_PATH: value,
}
) )
return {
CONF_TYPE: TYPE_LOCAL_BITMAP,
CONF_PATH: value,
}
return FILE_SCHEMA( return font_file_schema(
{ {
CONF_TYPE: TYPE_LOCAL, CONF_TYPE: TYPE_LOCAL,
CONF_PATH: value, 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): if isinstance(value, str):
return validate_file_shorthand(value) return validate_file_shorthand(value)
return TYPED_FILE_SCHEMA(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" CONF_RAW_GLYPH_ID = "raw_glyph_id"
FONT_SCHEMA = cv.Schema( FONT_SCHEMA = cv.Schema(
{ {
cv.Required(CONF_ID): cv.declare_id(Font), cv.Required(CONF_ID): cv.declare_id(Font),
cv.Required(CONF_FILE): FILE_SCHEMA, cv.Required(CONF_FILE): font_file_schema,
cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs, 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_SIZE, default=20): 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): cv.ensure_list( cv.Optional(CONF_EXTRAS, default=[]): cv.ensure_list(
cv.Schema( cv.Schema(
{ {
cv.Required(CONF_FILE): FILE_SCHEMA, cv.Required(CONF_FILE): font_file_schema,
cv.Required(CONF_GLYPHS): validate_glyphs, 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 # PIL doesn't provide a consistent interface for both TrueType and bitmap
@ -367,28 +460,20 @@ class BitmapFontWrapper:
mask = self.getmask(glyph, mode="1") mask = self.getmask(glyph, mode="1")
_, height = mask.size _, height = mask.size
max_height = max(max_height, height) max_height = max(max_height, height)
return (max_height, 0) return max_height, 0
class EFont: class EFont:
def __init__(self, file, size, glyphs): def __init__(self, file, size, codepoints):
self.glyphs = glyphs self.codepoints = codepoints
path = file[CONF_PATH]
self.name = Path(path).name
ftype = file[CONF_TYPE] ftype = file[CONF_TYPE]
if ftype == TYPE_LOCAL_BITMAP: if ftype == TYPE_LOCAL_BITMAP:
font = load_bitmap_font(CORE.relative_config_path(file[CONF_PATH])) self.font = load_bitmap_font(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)
else: else:
raise cv.Invalid(f"Could not load font: unknown type: {ftype}") self.font = load_ttf_font(path, size)
self.font = font self.ascent, self.descent = self.font.getmetrics(codepoints)
self.ascent, self.descent = font.getmetrics(glyphs)
def has_glyph(self, glyph):
return glyph in self.glyphs
def convert_bitmap_to_pillow_font(filepath): 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) 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: with open(local_bitmap_font_file, "rb") as fp:
try: try:
try: try:
@ -409,28 +495,22 @@ def convert_bitmap_to_pillow_font(filepath):
p = BdfFontFile.BdfFontFile(fp) p = BdfFontFile.BdfFontFile(fp)
# Convert to pillow-formatted fonts, which have a .pil and .pbm extension. # 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: except (SyntaxError, OSError) as err:
raise core.EsphomeError( raise core.EsphomeError(
f"Failed to parse as bitmap font: '{filepath}': {err}" f"Failed to parse as bitmap font: '{filepath}': {err}"
) )
local_pil_font_file = os.path.splitext(local_bitmap_font_file)[0] + ".pil" return str(local_pil_font_file)
return cv.file_(local_pil_font_file)
def load_bitmap_font(filepath): def load_bitmap_font(filepath):
from PIL import ImageFont from PIL import ImageFont
# Convert bpf and pcf files to pillow fonts, first.
pil_font_path = convert_bitmap_to_pillow_font(filepath)
try: try:
font = ImageFont.load(str(pil_font_path)) font = ImageFont.load(str(filepath))
except Exception as e: except Exception as e:
raise core.EsphomeError( raise core.EsphomeError(f"Failed to load bitmap font file: {filepath}: {e}")
f"Failed to load bitmap font file: {pil_font_path} : {e}"
)
return BitmapFontWrapper(font) return BitmapFontWrapper(font)
@ -441,7 +521,7 @@ def load_ttf_font(path, size):
try: try:
font = ImageFont.truetype(str(path), size) font = ImageFont.truetype(str(path), size)
except Exception as e: 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) return TrueTypeFontWrapper(font)
@ -456,14 +536,35 @@ class GlyphInfo:
async def to_code(config): async def to_code(config):
glyph_to_font_map = {} """
font_list = font_map[config[CONF_ID]] Collect all glyph codepoints, construct a map from a codepoint to a font file.
glyphs = [] Codepoints are either explicit (glyphs key in top level or extras) or part of a glyphset.
for font in font_list: Codepoints listed in extras use the extra font and override codepoints from glyphsets.
glyphs.extend(font.glyphs) Achieve this by processing the base codepoints first, then the extras
for glyph in font.glyphs: """
glyph_to_font_map[glyph] = font
glyphs.sort(key=functools.cmp_to_key(glyph_comparator)) # 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 = {} glyph_args = {}
data = [] data = []
bpp = config[CONF_BPP] bpp = config[CONF_BPP]
@ -473,10 +574,11 @@ async def to_code(config):
else: else:
mode = "L" mode = "L"
scale = 256 // (1 << bpp) scale = 256 // (1 << bpp)
for glyph in glyphs: # create the data array for all glyphs
font = glyph_to_font_map[glyph].font for codepoint in codepoints:
mask = font.getmask(glyph, mode=mode) font = point_font_map[codepoint]
offset_x, offset_y = font.getoffset(glyph) mask = font.font.getmask(codepoint, mode=mode)
offset_x, offset_y = font.font.getoffset(codepoint)
width, height = mask.size width, height = mask.size
glyph_data = [0] * ((height * width * bpp + 7) // 8) glyph_data = [0] * ((height * width * bpp + 7) // 8)
pos = 0 pos = 0
@ -487,31 +589,34 @@ async def to_code(config):
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[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 data += glyph_data
rhs = [HexInt(x) for x in data] rhs = [HexInt(x) for x in data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) 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 = [] glyph_initializer = []
for glyph in glyphs: for codepoint in codepoints:
glyph_initializer.append( glyph_initializer.append(
cg.StructInitializer( cg.StructInitializer(
GlyphData, GlyphData,
( (
"a_char", "a_char",
cg.RawExpression(f"(const uint8_t *){cpp_string_escape(glyph)}"), cg.RawExpression(
f"(const uint8_t *){cpp_string_escape(codepoint)}"
),
), ),
( (
"data", "data",
cg.RawExpression( 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_x", glyph_args[codepoint].offset_x),
("offset_y", glyph_args[glyph].offset_y), ("offset_y", glyph_args[codepoint].offset_y),
("width", glyph_args[glyph].width), ("width", glyph_args[codepoint].width),
("height", glyph_args[glyph].height), ("height", glyph_args[codepoint].height),
) )
) )
@ -521,7 +626,7 @@ async def to_code(config):
config[CONF_ID], config[CONF_ID],
glyphs, glyphs,
len(glyph_initializer), len(glyph_initializer),
font_list[0].ascent, base_font.ascent,
font_list[0].ascent + font_list[0].descent, base_font.ascent + base_font.descent,
bpp, bpp,
) )

View file

@ -17,6 +17,9 @@ aioesphomeapi==24.6.2
zeroconf==0.132.2 zeroconf==0.132.2
puremagic==1.27 puremagic==1.27
ruamel.yaml==0.18.6 # dashboard_import 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 # esp-idf requires this, but doesn't bundle it by default
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24

View file

@ -1,2 +1 @@
pillow==10.4.0
cairosvg==2.7.1 cairosvg==2.7.1

View file

@ -58,7 +58,7 @@ file_types = (
) )
cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc") cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc")
py_include = ("*.py",) py_include = ("*.py",)
ignore_types = (".ico", ".png", ".woff", ".woff2", "", ".ttf", ".otf") ignore_types = (".ico", ".png", ".woff", ".woff2", "", ".ttf", ".otf", ".pcf")
LINT_FILE_CHECKS = [] LINT_FILE_CHECKS = []
LINT_CONTENT_CHECKS = [] LINT_CONTENT_CHECKS = []

2
tests/components/font/.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
*.pcf -text

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,12 @@
font: font:
- file:
type: gfonts
family: "Roboto"
weight: bold
italic: true
size: 32
id: roboto32
- file: "gfonts://Roboto" - file: "gfonts://Roboto"
id: roboto id: roboto
size: 20 size: 20
@ -9,6 +17,10 @@ font:
- file: "gfonts://Roboto" - file: "gfonts://Roboto"
id: roboto_web id: roboto_web
size: 20 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" - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf"
id: monocraft id: monocraft
size: 20 size: 20
@ -20,6 +32,17 @@ font:
- file: $component_dir/Monocraft.ttf - file: $component_dir/Monocraft.ttf
id: monocraft3 id: monocraft3
size: 28 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: i2c:
scl: ${i2c_scl} scl: ${i2c_scl}
@ -36,3 +59,4 @@ display:
it.print(0, 40, id(monocraft), "Hello, World!"); it.print(0, 40, id(monocraft), "Hello, World!");
it.print(0, 60, id(monocraft2), "Hello, World!"); it.print(0, 60, id(monocraft2), "Hello, World!");
it.print(0, 80, id(monocraft3), "Hello, World!"); it.print(0, 80, id(monocraft3), "Hello, World!");
it.print(0, 100, id(roboto_greek), "Hello κόσμε!");

View file

@ -1,4 +1,12 @@
font: font:
- file:
type: gfonts
family: "Roboto"
weight: bold
italic: true
size: 32
id: roboto32
- file: "gfonts://Roboto" - file: "gfonts://Roboto"
id: roboto id: roboto
size: 20 size: 20
@ -9,6 +17,10 @@ font:
- file: "gfonts://Roboto" - file: "gfonts://Roboto"
id: roboto_web id: roboto_web
size: 20 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" - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf"
id: monocraft id: monocraft
size: 20 size: 20
@ -20,4 +32,26 @@ font:
- file: $component_dir/Monocraft.ttf - file: $component_dir/Monocraft.ttf
id: monocraft3 id: monocraft3
size: 28 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!");

Binary file not shown.