mirror of
https://github.com/esphome/esphome.git
synced 2024-11-26 08:55:22 +01:00
Font allow using google fonts directly (#3243)
This commit is contained in:
parent
48584e94c4
commit
9a82057303
1 changed files with 147 additions and 4 deletions
|
@ -1,12 +1,29 @@
|
||||||
import functools
|
import functools
|
||||||
|
from pathlib import Path
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
from esphome import core
|
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.const import CONF_FILE, CONF_GLYPHS, CONF_ID, CONF_RAW_DATA_ID, CONF_SIZE
|
from esphome.const import (
|
||||||
|
CONF_FAMILY,
|
||||||
|
CONF_FILE,
|
||||||
|
CONF_GLYPHS,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_RAW_DATA_ID,
|
||||||
|
CONF_TYPE,
|
||||||
|
CONF_SIZE,
|
||||||
|
CONF_PATH,
|
||||||
|
CONF_WEIGHT,
|
||||||
|
)
|
||||||
from esphome.core import CORE, HexInt
|
from esphome.core import CORE, HexInt
|
||||||
|
|
||||||
|
|
||||||
|
DOMAIN = "font"
|
||||||
DEPENDENCIES = ["display"]
|
DEPENDENCIES = ["display"]
|
||||||
MULTI_CONF = True
|
MULTI_CONF = True
|
||||||
|
|
||||||
|
@ -71,6 +88,128 @@ def validate_truetype_file(value):
|
||||||
return cv.file_(value)
|
return cv.file_(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_gfonts_local_path(value) -> Path:
|
||||||
|
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1"
|
||||||
|
base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN
|
||||||
|
h = hashlib.new("sha256")
|
||||||
|
h.update(name.encode())
|
||||||
|
return base_dir / h.hexdigest()[:8] / "font.ttf"
|
||||||
|
|
||||||
|
|
||||||
|
TYPE_LOCAL = "local"
|
||||||
|
TYPE_GFONTS = "gfonts"
|
||||||
|
LOCAL_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_PATH): validate_truetype_file,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
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):
|
||||||
|
wght = value[CONF_WEIGHT]
|
||||||
|
if value[CONF_ITALIC]:
|
||||||
|
wght = f"1,{wght}"
|
||||||
|
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}"
|
||||||
|
url = f"https://fonts.googleapis.com/css2?family={value[CONF_FAMILY]}:wght@{wght}"
|
||||||
|
|
||||||
|
path = _compute_gfonts_local_path(value)
|
||||||
|
if path.is_file():
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
req = requests.get(url)
|
||||||
|
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:
|
||||||
|
req = requests.get(ttf_url)
|
||||||
|
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)
|
||||||
|
return FILE_SCHEMA(
|
||||||
|
{
|
||||||
|
CONF_TYPE: TYPE_LOCAL,
|
||||||
|
CONF_PATH: value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
TYPED_FILE_SCHEMA = cv.typed_schema(
|
||||||
|
{
|
||||||
|
TYPE_LOCAL: LOCAL_SCHEMA,
|
||||||
|
TYPE_GFONTS: GFONTS_SCHEMA,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _file_schema(value):
|
||||||
|
if isinstance(value, str):
|
||||||
|
return validate_file_shorthand(value)
|
||||||
|
return TYPED_FILE_SCHEMA(value)
|
||||||
|
|
||||||
|
|
||||||
|
FILE_SCHEMA = cv.Schema(_file_schema)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_GLYPHS = (
|
DEFAULT_GLYPHS = (
|
||||||
' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
|
' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
|
||||||
)
|
)
|
||||||
|
@ -79,7 +218,7 @@ 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): validate_truetype_file,
|
cv.Required(CONF_FILE): FILE_SCHEMA,
|
||||||
cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs,
|
cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs,
|
||||||
cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1),
|
cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1),
|
||||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||||
|
@ -93,9 +232,13 @@ CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
from PIL import ImageFont
|
from PIL import ImageFont
|
||||||
|
|
||||||
path = CORE.relative_config_path(config[CONF_FILE])
|
conf = config[CONF_FILE]
|
||||||
|
if conf[CONF_TYPE] == TYPE_LOCAL:
|
||||||
|
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(path, config[CONF_SIZE])
|
font = ImageFont.truetype(str(path), config[CONF_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}")
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue