2019-12-07 18:28:55 +01:00
|
|
|
import functools
|
2022-03-28 01:07:48 +02:00
|
|
|
from pathlib import Path
|
|
|
|
import hashlib
|
2022-08-19 00:49:52 +02:00
|
|
|
import os
|
2022-03-28 01:07:48 +02:00
|
|
|
import re
|
2023-07-10 01:34:43 +02:00
|
|
|
from packaging import version
|
2022-03-28 01:07:48 +02:00
|
|
|
|
|
|
|
import requests
|
2019-12-07 18:28:55 +01:00
|
|
|
|
2019-02-13 16:54:02 +01:00
|
|
|
from esphome import core
|
|
|
|
import esphome.config_validation as cv
|
2019-04-17 12:06:00 +02:00
|
|
|
import esphome.codegen as cg
|
2024-03-11 10:03:39 +01:00
|
|
|
from esphome.helpers import (
|
|
|
|
copy_file_if_changed,
|
|
|
|
cpp_string_escape,
|
|
|
|
)
|
2022-03-28 01:07:48 +02:00
|
|
|
from esphome.const import (
|
|
|
|
CONF_FAMILY,
|
|
|
|
CONF_FILE,
|
|
|
|
CONF_GLYPHS,
|
|
|
|
CONF_ID,
|
|
|
|
CONF_RAW_DATA_ID,
|
|
|
|
CONF_TYPE,
|
|
|
|
CONF_SIZE,
|
|
|
|
CONF_PATH,
|
|
|
|
CONF_WEIGHT,
|
|
|
|
)
|
2024-03-11 10:03:39 +01:00
|
|
|
from esphome.core import (
|
|
|
|
CORE,
|
|
|
|
HexInt,
|
|
|
|
)
|
2022-03-28 01:07:48 +02:00
|
|
|
|
|
|
|
DOMAIN = "font"
|
2021-03-07 20:03:16 +01:00
|
|
|
DEPENDENCIES = ["display"]
|
2018-12-05 21:22:06 +01:00
|
|
|
MULTI_CONF = True
|
2018-08-18 21:40:59 +02:00
|
|
|
|
2024-03-11 10:03:39 +01:00
|
|
|
CODEOWNERS = ["@esphome/core", "@clydebarrow"]
|
|
|
|
|
2023-06-25 00:56:29 +02:00
|
|
|
font_ns = cg.esphome_ns.namespace("font")
|
|
|
|
|
|
|
|
Font = font_ns.class_("Font")
|
|
|
|
Glyph = font_ns.class_("Glyph")
|
|
|
|
GlyphData = font_ns.struct("GlyphData")
|
2018-08-18 21:40:59 +02:00
|
|
|
|
2024-03-11 10:03:39 +01:00
|
|
|
CONF_BPP = "bpp"
|
|
|
|
CONF_EXTRAS = "extras"
|
|
|
|
CONF_FONTS = "fonts"
|
|
|
|
|
|
|
|
|
|
|
|
def glyph_comparator(x, y):
|
|
|
|
x_ = x.encode("utf-8")
|
|
|
|
y_ = y.encode("utf-8")
|
|
|
|
|
|
|
|
for c in range(min(len(x_), len(y_))):
|
|
|
|
if x_[c] < y_[c]:
|
|
|
|
return -1
|
|
|
|
if x_[c] > y_[c]:
|
|
|
|
return 1
|
|
|
|
|
|
|
|
if len(x_) < len(y_):
|
|
|
|
return -1
|
|
|
|
if len(x_) > len(y_):
|
|
|
|
return 1
|
|
|
|
raise cv.Invalid(f"Found duplicate glyph {x}")
|
|
|
|
|
2018-08-18 21:40:59 +02:00
|
|
|
|
|
|
|
def validate_glyphs(value):
|
|
|
|
if isinstance(value, list):
|
2019-02-26 19:22:33 +01:00
|
|
|
value = cv.Schema([cv.string])(value)
|
|
|
|
value = cv.Schema([cv.string])(list(value))
|
2018-08-18 21:40:59 +02:00
|
|
|
|
2024-03-11 10:03:39 +01:00
|
|
|
value.sort(key=functools.cmp_to_key(glyph_comparator))
|
|
|
|
return value
|
2018-08-18 21:40:59 +02:00
|
|
|
|
|
|
|
|
2024-03-11 10:03:39 +01:00
|
|
|
font_map = {}
|
2018-08-18 21:40:59 +02:00
|
|
|
|
2024-03-11 10:03:39 +01:00
|
|
|
|
|
|
|
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_list.extend(extra_fonts)
|
|
|
|
for extra in extras:
|
|
|
|
glyphs.extend(extra[CONF_GLYPHS])
|
|
|
|
validate_glyphs(glyphs)
|
|
|
|
font_map[config[CONF_ID]] = font_list
|
|
|
|
return config
|
2018-08-18 21:40:59 +02:00
|
|
|
|
|
|
|
|
|
|
|
def validate_pillow_installed(value):
|
|
|
|
try:
|
|
|
|
import PIL
|
2020-09-16 12:12:40 +02:00
|
|
|
except ImportError as err:
|
2021-03-07 20:03:16 +01:00
|
|
|
raise cv.Invalid(
|
|
|
|
"Please install the pillow python package to use this feature. "
|
2024-01-15 00:48:02 +01:00
|
|
|
'(pip install "pillow==10.2.0")'
|
2021-03-07 20:03:16 +01:00
|
|
|
) from err
|
2018-08-18 21:40:59 +02:00
|
|
|
|
2024-01-15 00:48:02 +01:00
|
|
|
if version.parse(PIL.__version__) != version.parse("10.2.0"):
|
2021-03-07 20:03:16 +01:00
|
|
|
raise cv.Invalid(
|
2024-01-15 00:48:02 +01:00
|
|
|
"Please update your pillow installation to 10.2.0. "
|
|
|
|
'(pip install "pillow==10.2.0")'
|
2021-03-07 20:03:16 +01:00
|
|
|
)
|
2018-08-18 21:40:59 +02:00
|
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
2024-03-11 10:03:39 +01:00
|
|
|
FONT_EXTENSIONS = (".ttf", ".woff", ".otf")
|
|
|
|
|
|
|
|
|
2018-08-18 21:40:59 +02:00
|
|
|
def validate_truetype_file(value):
|
2024-03-11 10:03:39 +01:00
|
|
|
if value.lower().endswith(".zip"): # for Google Fonts downloads
|
2021-03-07 20:03:16 +01:00
|
|
|
raise cv.Invalid(
|
2021-09-19 19:22:28 +02:00
|
|
|
f"Please unzip the font archive '{value}' first and then use the .ttf files inside."
|
2021-03-07 20:03:16 +01:00
|
|
|
)
|
2024-03-11 10:03:39 +01:00
|
|
|
if not any(map(value.lower().endswith, FONT_EXTENSIONS)):
|
|
|
|
raise cv.Invalid(f"Only {FONT_EXTENSIONS} files are supported.")
|
2018-09-23 18:58:41 +02:00
|
|
|
return cv.file_(value)
|
2018-08-18 21:40:59 +02:00
|
|
|
|
|
|
|
|
2022-08-19 00:49:52 +02:00
|
|
|
def _compute_local_font_dir(name) -> Path:
|
2022-03-28 01:07:48 +02:00
|
|
|
h = hashlib.new("sha256")
|
|
|
|
h.update(name.encode())
|
2023-09-11 23:26:48 +02:00
|
|
|
return Path(CORE.data_dir) / DOMAIN / h.hexdigest()[:8]
|
2022-08-19 00:49:52 +02:00
|
|
|
|
|
|
|
|
|
|
|
def _compute_gfonts_local_path(value) -> Path:
|
|
|
|
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1"
|
|
|
|
return _compute_local_font_dir(name) / "font.ttf"
|
2022-03-28 01:07:48 +02:00
|
|
|
|
|
|
|
|
|
|
|
TYPE_LOCAL = "local"
|
2022-08-19 00:49:52 +02:00
|
|
|
TYPE_LOCAL_BITMAP = "local_bitmap"
|
2022-03-28 01:07:48 +02:00
|
|
|
TYPE_GFONTS = "gfonts"
|
|
|
|
LOCAL_SCHEMA = cv.Schema(
|
|
|
|
{
|
|
|
|
cv.Required(CONF_PATH): validate_truetype_file,
|
|
|
|
}
|
|
|
|
)
|
2022-08-19 00:49:52 +02:00
|
|
|
|
|
|
|
LOCAL_BITMAP_SCHEMA = cv.Schema(
|
|
|
|
{
|
|
|
|
cv.Required(CONF_PATH): cv.file_,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2022-03-28 01:07:48 +02:00
|
|
|
CONF_ITALIC = "italic"
|
|
|
|
FONT_WEIGHTS = {
|
|
|
|
"thin": 100,
|
|
|
|
"extra-light": 200,
|
|
|
|
"light": 300,
|
|
|
|
"regular": 400,
|
|
|
|
"medium": 500,
|
|
|
|
"semi-bold": 600,
|
|
|
|
"bold": 700,
|
|
|
|
"extra-bold": 800,
|
|
|
|
"black": 900,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def validate_weight_name(value):
|
|
|
|
return FONT_WEIGHTS[cv.one_of(*FONT_WEIGHTS, lower=True, space="-")(value)]
|
|
|
|
|
|
|
|
|
|
|
|
def download_gfonts(value):
|
2023-10-29 20:05:18 +01:00
|
|
|
name = (
|
|
|
|
f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}"
|
|
|
|
)
|
|
|
|
url = f"https://fonts.googleapis.com/css2?family={name}"
|
2022-03-28 01:07:48 +02:00
|
|
|
|
|
|
|
path = _compute_gfonts_local_path(value)
|
|
|
|
if path.is_file():
|
|
|
|
return value
|
|
|
|
try:
|
2022-08-31 07:01:36 +02:00
|
|
|
req = requests.get(url, timeout=30)
|
2022-03-28 01:07:48 +02:00
|
|
|
req.raise_for_status()
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
raise cv.Invalid(
|
|
|
|
f"Could not download font for {name}, 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)
|
|
|
|
try:
|
2022-08-31 07:01:36 +02:00
|
|
|
req = requests.get(ttf_url, timeout=30)
|
2022-03-28 01:07:48 +02:00
|
|
|
req.raise_for_status()
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
raise cv.Invalid(f"Could not download ttf file for {name} ({ttf_url}): {e}")
|
|
|
|
|
|
|
|
path.parent.mkdir(exist_ok=True, parents=True)
|
|
|
|
path.write_bytes(req.content)
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
GFONTS_SCHEMA = cv.All(
|
|
|
|
{
|
|
|
|
cv.Required(CONF_FAMILY): cv.string_strict,
|
|
|
|
cv.Optional(CONF_WEIGHT, default="regular"): cv.Any(
|
|
|
|
cv.int_, validate_weight_name
|
|
|
|
),
|
|
|
|
cv.Optional(CONF_ITALIC, default=False): cv.boolean,
|
|
|
|
},
|
|
|
|
download_gfonts,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def validate_file_shorthand(value):
|
|
|
|
value = cv.string_strict(value)
|
|
|
|
if value.startswith("gfonts://"):
|
|
|
|
match = re.match(r"^gfonts://([^@]+)(@.+)?$", value)
|
|
|
|
if match is None:
|
|
|
|
raise cv.Invalid("Could not parse gfonts shorthand syntax, please check it")
|
|
|
|
family = match.group(1)
|
|
|
|
weight = match.group(2)
|
|
|
|
data = {
|
|
|
|
CONF_TYPE: TYPE_GFONTS,
|
|
|
|
CONF_FAMILY: family,
|
|
|
|
}
|
|
|
|
if weight is not None:
|
|
|
|
data[CONF_WEIGHT] = weight[1:]
|
|
|
|
return FILE_SCHEMA(data)
|
2022-08-19 00:49:52 +02:00
|
|
|
|
|
|
|
if value.endswith(".pcf") or value.endswith(".bdf"):
|
|
|
|
return FILE_SCHEMA(
|
|
|
|
{
|
|
|
|
CONF_TYPE: TYPE_LOCAL_BITMAP,
|
|
|
|
CONF_PATH: value,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2022-03-28 01:07:48 +02:00
|
|
|
return FILE_SCHEMA(
|
|
|
|
{
|
|
|
|
CONF_TYPE: TYPE_LOCAL,
|
|
|
|
CONF_PATH: value,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
TYPED_FILE_SCHEMA = cv.typed_schema(
|
|
|
|
{
|
|
|
|
TYPE_LOCAL: LOCAL_SCHEMA,
|
|
|
|
TYPE_GFONTS: GFONTS_SCHEMA,
|
2022-08-19 00:49:52 +02:00
|
|
|
TYPE_LOCAL_BITMAP: LOCAL_BITMAP_SCHEMA,
|
2022-03-28 01:07:48 +02:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _file_schema(value):
|
|
|
|
if isinstance(value, str):
|
|
|
|
return validate_file_shorthand(value)
|
|
|
|
return TYPED_FILE_SCHEMA(value)
|
|
|
|
|
|
|
|
|
|
|
|
FILE_SCHEMA = cv.Schema(_file_schema)
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
DEFAULT_GLYPHS = (
|
2024-01-09 00:12:28 +01:00
|
|
|
' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
|
2021-03-07 20:03:16 +01:00
|
|
|
)
|
2021-05-23 22:24:54 +02:00
|
|
|
CONF_RAW_GLYPH_ID = "raw_glyph_id"
|
2018-08-18 21:40:59 +02:00
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
FONT_SCHEMA = cv.Schema(
|
|
|
|
{
|
|
|
|
cv.Required(CONF_ID): cv.declare_id(Font),
|
2022-03-28 01:07:48 +02:00
|
|
|
cv.Required(CONF_FILE): FILE_SCHEMA,
|
2021-03-07 20:03:16 +01:00
|
|
|
cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs,
|
|
|
|
cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1),
|
2024-03-11 10:03:39 +01:00
|
|
|
cv.Optional(CONF_BPP, default=1): cv.one_of(1, 2, 4, 8),
|
|
|
|
cv.Optional(CONF_EXTRAS): cv.ensure_list(
|
|
|
|
cv.Schema(
|
|
|
|
{
|
|
|
|
cv.Required(CONF_FILE): FILE_SCHEMA,
|
|
|
|
cv.Required(CONF_GLYPHS): validate_glyphs,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
),
|
2021-03-07 20:03:16 +01:00
|
|
|
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
2021-05-23 22:24:54 +02:00
|
|
|
cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData),
|
2021-03-07 20:03:16 +01:00
|
|
|
}
|
|
|
|
)
|
2018-08-18 21:40:59 +02:00
|
|
|
|
2024-03-11 10:03:39 +01:00
|
|
|
CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, merge_glyphs)
|
|
|
|
|
2018-08-18 21:40:59 +02:00
|
|
|
|
2022-08-19 00:49:52 +02:00
|
|
|
# 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.
|
2018-08-18 21:40:59 +02:00
|
|
|
|
2022-08-19 00:49:52 +02:00
|
|
|
|
|
|
|
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(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(glyph, **kwargs)
|
|
|
|
|
|
|
|
def getmetrics(self, glyphs):
|
|
|
|
max_height = 0
|
|
|
|
for glyph in glyphs:
|
|
|
|
mask = self.getmask(glyph, mode="1")
|
|
|
|
_, height = mask.size
|
|
|
|
if height > max_height:
|
|
|
|
max_height = height
|
|
|
|
return (max_height, 0)
|
|
|
|
|
|
|
|
|
2024-03-11 10:03:39 +01:00
|
|
|
class EFont:
|
|
|
|
def __init__(self, file, size, glyphs):
|
|
|
|
self.glyphs = glyphs
|
|
|
|
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 == TYPE_GFONTS:
|
|
|
|
path = _compute_gfonts_local_path(file)
|
|
|
|
font = load_ttf_font(path, size)
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2022-08-19 00:49:52 +02:00
|
|
|
def convert_bitmap_to_pillow_font(filepath):
|
2024-03-11 10:03:39 +01:00
|
|
|
from PIL import (
|
|
|
|
PcfFontFile,
|
|
|
|
BdfFontFile,
|
|
|
|
)
|
2022-08-19 00:49:52 +02:00
|
|
|
|
|
|
|
local_bitmap_font_file = _compute_local_font_dir(filepath) / os.path.basename(
|
|
|
|
filepath
|
|
|
|
)
|
|
|
|
|
|
|
|
copy_file_if_changed(filepath, local_bitmap_font_file)
|
|
|
|
|
|
|
|
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_bitmap_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)
|
|
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
except Exception as e:
|
|
|
|
raise core.EsphomeError(
|
|
|
|
f"Failed to load bitmap font file: {pil_font_path} : {e}"
|
|
|
|
)
|
|
|
|
|
|
|
|
return BitmapFontWrapper(font)
|
|
|
|
|
|
|
|
|
|
|
|
def load_ttf_font(path, size):
|
2018-08-18 21:40:59 +02:00
|
|
|
from PIL import ImageFont
|
|
|
|
|
2022-08-19 00:49:52 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2024-03-11 10:03:39 +01:00
|
|
|
class GlyphInfo:
|
|
|
|
def __init__(self, data_len, offset_x, offset_y, width, height):
|
|
|
|
self.data_len = data_len
|
|
|
|
self.offset_x = offset_x
|
|
|
|
self.offset_y = offset_y
|
|
|
|
self.width = width
|
|
|
|
self.height = height
|
2018-12-05 21:22:06 +01:00
|
|
|
|
|
|
|
|
2024-03-11 10:03:39 +01:00
|
|
|
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))
|
2018-12-05 21:22:06 +01:00
|
|
|
glyph_args = {}
|
|
|
|
data = []
|
2024-03-11 10:03:39 +01:00
|
|
|
bpp = config[CONF_BPP]
|
|
|
|
if bpp == 1:
|
|
|
|
mode = "1"
|
|
|
|
scale = 1
|
|
|
|
else:
|
|
|
|
mode = "L"
|
|
|
|
scale = 256 // (1 << bpp)
|
|
|
|
for glyph in glyphs:
|
|
|
|
font = glyph_to_font_map[glyph].font
|
|
|
|
mask = font.getmask(glyph, mode=mode)
|
2022-08-19 00:49:52 +02:00
|
|
|
offset_x, offset_y = font.getoffset(glyph)
|
2018-12-05 21:22:06 +01:00
|
|
|
width, height = mask.size
|
2024-03-11 10:03:39 +01:00
|
|
|
glyph_data = [0] * ((height * width * bpp + 7) // 8)
|
|
|
|
pos = 0
|
2018-12-05 21:22:06 +01:00
|
|
|
for y in range(height):
|
|
|
|
for x in range(width):
|
2024-03-11 10:03:39 +01:00
|
|
|
pixel = mask.getpixel((x, y)) // 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[glyph] = GlyphInfo(len(data), offset_x, offset_y, width, height)
|
2018-12-05 21:22:06 +01:00
|
|
|
data += glyph_data
|
|
|
|
|
2019-04-17 12:06:00 +02:00
|
|
|
rhs = [HexInt(x) for x in data]
|
|
|
|
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
2018-12-05 21:22:06 +01:00
|
|
|
|
2021-05-23 22:24:54 +02:00
|
|
|
glyph_initializer = []
|
2024-03-11 10:03:39 +01:00
|
|
|
for glyph in glyphs:
|
2021-05-23 22:24:54 +02:00
|
|
|
glyph_initializer.append(
|
|
|
|
cg.StructInitializer(
|
|
|
|
GlyphData,
|
2024-03-11 10:03:39 +01:00
|
|
|
(
|
|
|
|
"a_char",
|
|
|
|
cg.RawExpression(f"(const uint8_t *){cpp_string_escape(glyph)}"),
|
|
|
|
),
|
2021-05-23 22:24:54 +02:00
|
|
|
(
|
|
|
|
"data",
|
2024-03-11 10:03:39 +01:00
|
|
|
cg.RawExpression(
|
|
|
|
f"{str(prog_arr)} + {str(glyph_args[glyph].data_len)}"
|
|
|
|
),
|
2021-05-23 22:24:54 +02:00
|
|
|
),
|
2024-03-11 10:03:39 +01:00
|
|
|
("offset_x", glyph_args[glyph].offset_x),
|
|
|
|
("offset_y", glyph_args[glyph].offset_y),
|
|
|
|
("width", glyph_args[glyph].width),
|
|
|
|
("height", glyph_args[glyph].height),
|
2021-05-23 22:24:54 +02:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
|
2018-12-05 21:22:06 +01:00
|
|
|
|
2021-05-23 22:24:54 +02:00
|
|
|
cg.new_Pvariable(
|
2024-03-11 10:03:39 +01:00
|
|
|
config[CONF_ID],
|
|
|
|
glyphs,
|
|
|
|
len(glyph_initializer),
|
|
|
|
font_list[0].ascent,
|
|
|
|
font_list[0].ascent + font_list[0].descent,
|
|
|
|
bpp,
|
2021-05-23 22:24:54 +02:00
|
|
|
)
|