mirror of
https://github.com/esphome/esphome.git
synced 2024-11-29 18:24:13 +01:00
Add bitmap font support (#3573)
Co-authored-by: Otto Winter <otto@otto-winter.com>
This commit is contained in:
parent
ac3cdf487f
commit
3d0a85ee78
1 changed files with 130 additions and 12 deletions
|
@ -1,6 +1,7 @@
|
||||||
import functools
|
import functools
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
@ -9,6 +10,7 @@ from esphome import core
|
||||||
from esphome.components import display
|
from esphome.components import display
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
|
from esphome.helpers import copy_file_if_changed
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_FAMILY,
|
CONF_FAMILY,
|
||||||
CONF_FILE,
|
CONF_FILE,
|
||||||
|
@ -88,21 +90,33 @@ def validate_truetype_file(value):
|
||||||
return cv.file_(value)
|
return cv.file_(value)
|
||||||
|
|
||||||
|
|
||||||
def _compute_gfonts_local_path(value) -> Path:
|
def _compute_local_font_dir(name) -> Path:
|
||||||
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1"
|
|
||||||
base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN
|
base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN
|
||||||
h = hashlib.new("sha256")
|
h = hashlib.new("sha256")
|
||||||
h.update(name.encode())
|
h.update(name.encode())
|
||||||
return base_dir / h.hexdigest()[:8] / "font.ttf"
|
return base_dir / h.hexdigest()[:8]
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
TYPE_LOCAL = "local"
|
TYPE_LOCAL = "local"
|
||||||
|
TYPE_LOCAL_BITMAP = "local_bitmap"
|
||||||
TYPE_GFONTS = "gfonts"
|
TYPE_GFONTS = "gfonts"
|
||||||
LOCAL_SCHEMA = cv.Schema(
|
LOCAL_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_PATH): validate_truetype_file,
|
cv.Required(CONF_PATH): validate_truetype_file,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LOCAL_BITMAP_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_PATH): cv.file_,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
CONF_ITALIC = "italic"
|
CONF_ITALIC = "italic"
|
||||||
FONT_WEIGHTS = {
|
FONT_WEIGHTS = {
|
||||||
"thin": 100,
|
"thin": 100,
|
||||||
|
@ -185,6 +199,15 @@ 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 FILE_SCHEMA(data)
|
||||||
|
|
||||||
|
if value.endswith(".pcf") or value.endswith(".bdf"):
|
||||||
|
return FILE_SCHEMA(
|
||||||
|
{
|
||||||
|
CONF_TYPE: TYPE_LOCAL_BITMAP,
|
||||||
|
CONF_PATH: value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return FILE_SCHEMA(
|
return FILE_SCHEMA(
|
||||||
{
|
{
|
||||||
CONF_TYPE: TYPE_LOCAL,
|
CONF_TYPE: TYPE_LOCAL,
|
||||||
|
@ -197,6 +220,7 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
|
||||||
{
|
{
|
||||||
TYPE_LOCAL: LOCAL_SCHEMA,
|
TYPE_LOCAL: LOCAL_SCHEMA,
|
||||||
TYPE_GFONTS: GFONTS_SCHEMA,
|
TYPE_GFONTS: GFONTS_SCHEMA,
|
||||||
|
TYPE_LOCAL_BITMAP: LOCAL_BITMAP_SCHEMA,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -228,27 +252,121 @@ FONT_SCHEMA = cv.Schema(
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA)
|
CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA)
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
async def to_code(config):
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_bitmap_to_pillow_font(filepath):
|
||||||
|
from PIL import PcfFontFile, BdfFontFile
|
||||||
|
|
||||||
|
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
|
from PIL import ImageFont
|
||||||
|
|
||||||
conf = config[CONF_FILE]
|
# Convert bpf and pcf files to pillow fonts, first.
|
||||||
if conf[CONF_TYPE] == TYPE_LOCAL:
|
pil_font_path = convert_bitmap_to_pillow_font(filepath)
|
||||||
path = CORE.relative_config_path(conf[CONF_PATH])
|
|
||||||
elif conf[CONF_TYPE] == TYPE_GFONTS:
|
|
||||||
path = _compute_gfonts_local_path(conf)
|
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(str(path), config[CONF_SIZE])
|
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):
|
||||||
|
from PIL import ImageFont
|
||||||
|
|
||||||
|
try:
|
||||||
|
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}")
|
||||||
|
|
||||||
ascent, descent = font.getmetrics()
|
return TrueTypeFontWrapper(font)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
conf = config[CONF_FILE]
|
||||||
|
if conf[CONF_TYPE] == TYPE_LOCAL_BITMAP:
|
||||||
|
font = load_bitmap_font(CORE.relative_config_path(conf[CONF_PATH]))
|
||||||
|
elif conf[CONF_TYPE] == TYPE_LOCAL:
|
||||||
|
path = CORE.relative_config_path(conf[CONF_PATH])
|
||||||
|
font = load_ttf_font(path, config[CONF_SIZE])
|
||||||
|
elif conf[CONF_TYPE] == TYPE_GFONTS:
|
||||||
|
path = _compute_gfonts_local_path(conf)
|
||||||
|
font = load_ttf_font(path, config[CONF_SIZE])
|
||||||
|
else:
|
||||||
|
raise core.EsphomeError(f"Could not load font: unknown type: {conf[CONF_TYPE]}")
|
||||||
|
|
||||||
|
ascent, descent = font.getmetrics(config[CONF_GLYPHS])
|
||||||
|
|
||||||
glyph_args = {}
|
glyph_args = {}
|
||||||
data = []
|
data = []
|
||||||
for glyph in config[CONF_GLYPHS]:
|
for glyph in config[CONF_GLYPHS]:
|
||||||
mask = font.getmask(glyph, mode="1")
|
mask = font.getmask(glyph, mode="1")
|
||||||
_, (offset_x, offset_y) = font.font.getsize(glyph)
|
offset_x, offset_y = font.getoffset(glyph)
|
||||||
width, height = mask.size
|
width, height = mask.size
|
||||||
width8 = ((width + 7) // 8) * 8
|
width8 = ((width + 7) // 8) * 8
|
||||||
glyph_data = [0] * (height * width8 // 8)
|
glyph_data = [0] * (height * width8 // 8)
|
||||||
|
|
Loading…
Reference in a new issue