Add bitmap font support (#3573)

Co-authored-by: Otto Winter <otto@otto-winter.com>
This commit is contained in:
Mike Ryan 2022-08-18 15:49:52 -07:00 committed by GitHub
parent ac3cdf487f
commit 3d0a85ee78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -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)