mirror of
https://github.com/esphome/esphome.git
synced 2024-11-25 08:28:12 +01:00
download font from url on build (#5254)
Co-authored-by: guillempages <guillempages@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
This commit is contained in:
parent
d3a028f7fa
commit
2df9c30446
4 changed files with 122 additions and 43 deletions
|
@ -1,13 +1,15 @@
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import hashlib
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from packaging import version
|
from packaging import version
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from esphome import core
|
from esphome import core
|
||||||
|
from esphome import external_files
|
||||||
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 (
|
from esphome.helpers import (
|
||||||
|
@ -15,21 +17,26 @@ from esphome.helpers import (
|
||||||
cpp_string_escape,
|
cpp_string_escape,
|
||||||
)
|
)
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
|
__version__,
|
||||||
CONF_FAMILY,
|
CONF_FAMILY,
|
||||||
CONF_FILE,
|
CONF_FILE,
|
||||||
CONF_GLYPHS,
|
CONF_GLYPHS,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_RAW_DATA_ID,
|
CONF_RAW_DATA_ID,
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
|
CONF_REFRESH,
|
||||||
CONF_SIZE,
|
CONF_SIZE,
|
||||||
CONF_PATH,
|
CONF_PATH,
|
||||||
CONF_WEIGHT,
|
CONF_WEIGHT,
|
||||||
|
CONF_URL,
|
||||||
)
|
)
|
||||||
from esphome.core import (
|
from esphome.core import (
|
||||||
CORE,
|
CORE,
|
||||||
HexInt,
|
HexInt,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "font"
|
DOMAIN = "font"
|
||||||
DEPENDENCIES = ["display"]
|
DEPENDENCIES = ["display"]
|
||||||
MULTI_CONF = True
|
MULTI_CONF = True
|
||||||
|
@ -125,20 +132,10 @@ def validate_truetype_file(value):
|
||||||
return cv.file_(value)
|
return cv.file_(value)
|
||||||
|
|
||||||
|
|
||||||
def _compute_local_font_dir(name) -> Path:
|
|
||||||
h = hashlib.new("sha256")
|
|
||||||
h.update(name.encode())
|
|
||||||
return Path(CORE.data_dir) / DOMAIN / 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_LOCAL_BITMAP = "local_bitmap"
|
||||||
TYPE_GFONTS = "gfonts"
|
TYPE_GFONTS = "gfonts"
|
||||||
|
TYPE_WEB = "web"
|
||||||
LOCAL_SCHEMA = cv.Schema(
|
LOCAL_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_PATH): validate_truetype_file,
|
cv.Required(CONF_PATH): validate_truetype_file,
|
||||||
|
@ -169,21 +166,64 @@ def validate_weight_name(value):
|
||||||
return FONT_WEIGHTS[cv.one_of(*FONT_WEIGHTS, lower=True, space="-")(value)]
|
return FONT_WEIGHTS[cv.one_of(*FONT_WEIGHTS, lower=True, space="-")(value)]
|
||||||
|
|
||||||
|
|
||||||
def download_gfonts(value):
|
def _compute_local_font_path(value: dict) -> Path:
|
||||||
|
url = value[CONF_URL]
|
||||||
|
h = hashlib.new("sha256")
|
||||||
|
h.update(url.encode())
|
||||||
|
key = h.hexdigest()[:8]
|
||||||
|
base_dir = external_files.compute_local_file_dir(DOMAIN)
|
||||||
|
_LOGGER.debug("_compute_local_font_path: base_dir=%s", base_dir / key)
|
||||||
|
return base_dir / key
|
||||||
|
|
||||||
|
|
||||||
|
def get_font_path(value, type) -> Path:
|
||||||
|
if type == TYPE_GFONTS:
|
||||||
|
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1"
|
||||||
|
return external_files.compute_local_file_dir(DOMAIN) / f"{name}.ttf"
|
||||||
|
if type == TYPE_WEB:
|
||||||
|
return _compute_local_font_path(value) / "font.ttf"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def download_content(url: str, path: Path) -> None:
|
||||||
|
if not external_files.has_remote_file_changed(url, path):
|
||||||
|
_LOGGER.debug("Remote file has not changed %s", url)
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Remote file has changed, downloading from %s to %s",
|
||||||
|
url,
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
req = requests.get(
|
||||||
|
url,
|
||||||
|
timeout=external_files.NETWORK_TIMEOUT,
|
||||||
|
headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"},
|
||||||
|
)
|
||||||
|
req.raise_for_status()
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
raise cv.Invalid(f"Could not download from {url}: {e}")
|
||||||
|
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_bytes(req.content)
|
||||||
|
|
||||||
|
|
||||||
|
def download_gfont(value):
|
||||||
name = (
|
name = (
|
||||||
f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}"
|
f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}"
|
||||||
)
|
)
|
||||||
url = f"https://fonts.googleapis.com/css2?family={name}"
|
url = f"https://fonts.googleapis.com/css2?family={name}"
|
||||||
|
path = get_font_path(value, TYPE_GFONTS)
|
||||||
|
_LOGGER.debug("download_gfont: path=%s", path)
|
||||||
|
|
||||||
path = _compute_gfonts_local_path(value)
|
|
||||||
if path.is_file():
|
|
||||||
return value
|
|
||||||
try:
|
try:
|
||||||
req = requests.get(url, timeout=30)
|
req = requests.get(url, timeout=external_files.NETWORK_TIMEOUT)
|
||||||
req.raise_for_status()
|
req.raise_for_status()
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
f"Could not download font for {name}, please check the fonts exists "
|
f"Could not download font at {url}, please check the fonts exists "
|
||||||
f"at google fonts ({e})"
|
f"at google fonts ({e})"
|
||||||
)
|
)
|
||||||
match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text)
|
match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text)
|
||||||
|
@ -194,26 +234,48 @@ def download_gfonts(value):
|
||||||
)
|
)
|
||||||
|
|
||||||
ttf_url = match.group(1)
|
ttf_url = match.group(1)
|
||||||
try:
|
_LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
|
||||||
req = requests.get(ttf_url, timeout=30)
|
|
||||||
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)
|
download_content(ttf_url, path)
|
||||||
path.write_bytes(req.content)
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
GFONTS_SCHEMA = cv.All(
|
def download_web_font(value):
|
||||||
|
url = value[CONF_URL]
|
||||||
|
path = get_font_path(value, TYPE_WEB)
|
||||||
|
|
||||||
|
download_content(url, path)
|
||||||
|
_LOGGER.debug("download_web_font: path=%s", path)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
EXTERNAL_FONT_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_FAMILY): cv.string_strict,
|
|
||||||
cv.Optional(CONF_WEIGHT, default="regular"): cv.Any(
|
cv.Optional(CONF_WEIGHT, default="regular"): cv.Any(
|
||||||
cv.int_, validate_weight_name
|
cv.int_, validate_weight_name
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_ITALIC, default=False): cv.boolean,
|
cv.Optional(CONF_ITALIC, default=False): cv.boolean,
|
||||||
},
|
cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, cv.source_refresh),
|
||||||
download_gfonts,
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
GFONTS_SCHEMA = cv.All(
|
||||||
|
EXTERNAL_FONT_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_FAMILY): cv.string_strict,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
download_gfont,
|
||||||
|
)
|
||||||
|
|
||||||
|
WEB_FONT_SCHEMA = cv.All(
|
||||||
|
EXTERNAL_FONT_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_URL): cv.string_strict,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
download_web_font,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -233,6 +295,14 @@ def validate_file_shorthand(value):
|
||||||
data[CONF_WEIGHT] = weight[1:]
|
data[CONF_WEIGHT] = weight[1:]
|
||||||
return FILE_SCHEMA(data)
|
return FILE_SCHEMA(data)
|
||||||
|
|
||||||
|
if value.startswith("http://") or value.startswith("https://"):
|
||||||
|
return FILE_SCHEMA(
|
||||||
|
{
|
||||||
|
CONF_TYPE: TYPE_WEB,
|
||||||
|
CONF_URL: value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if value.endswith(".pcf") or value.endswith(".bdf"):
|
if value.endswith(".pcf") or value.endswith(".bdf"):
|
||||||
return FILE_SCHEMA(
|
return FILE_SCHEMA(
|
||||||
{
|
{
|
||||||
|
@ -254,6 +324,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,
|
TYPE_LOCAL_BITMAP: LOCAL_BITMAP_SCHEMA,
|
||||||
|
TYPE_WEB: WEB_FONT_SCHEMA,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -264,7 +335,7 @@ def _file_schema(value):
|
||||||
return TYPED_FILE_SCHEMA(value)
|
return TYPED_FILE_SCHEMA(value)
|
||||||
|
|
||||||
|
|
||||||
FILE_SCHEMA = cv.Schema(_file_schema)
|
FILE_SCHEMA = cv.All(_file_schema)
|
||||||
|
|
||||||
DEFAULT_GLYPHS = (
|
DEFAULT_GLYPHS = (
|
||||||
' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
|
' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
|
||||||
|
@ -288,7 +359,7 @@ FONT_SCHEMA = cv.Schema(
|
||||||
),
|
),
|
||||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||||
cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData),
|
cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData),
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, merge_glyphs)
|
CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, merge_glyphs)
|
||||||
|
@ -343,8 +414,8 @@ class EFont:
|
||||||
elif ftype == TYPE_LOCAL:
|
elif ftype == TYPE_LOCAL:
|
||||||
path = CORE.relative_config_path(file[CONF_PATH])
|
path = CORE.relative_config_path(file[CONF_PATH])
|
||||||
font = load_ttf_font(path, size)
|
font = load_ttf_font(path, size)
|
||||||
elif ftype == TYPE_GFONTS:
|
elif ftype in (TYPE_GFONTS, TYPE_WEB):
|
||||||
path = _compute_gfonts_local_path(file)
|
path = get_font_path(file, ftype)
|
||||||
font = load_ttf_font(path, size)
|
font = load_ttf_font(path, size)
|
||||||
else:
|
else:
|
||||||
raise cv.Invalid(f"Could not load font: unknown type: {ftype}")
|
raise cv.Invalid(f"Could not load font: unknown type: {ftype}")
|
||||||
|
@ -361,9 +432,9 @@ def convert_bitmap_to_pillow_font(filepath):
|
||||||
BdfFontFile,
|
BdfFontFile,
|
||||||
)
|
)
|
||||||
|
|
||||||
local_bitmap_font_file = _compute_local_font_dir(filepath) / os.path.basename(
|
local_bitmap_font_file = external_files.compute_local_file_dir(
|
||||||
filepath
|
DOMAIN,
|
||||||
)
|
) / os.path.basename(filepath)
|
||||||
|
|
||||||
copy_file_if_changed(filepath, local_bitmap_font_file)
|
copy_file_if_changed(filepath, local_bitmap_font_file)
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,9 @@ def has_remote_file_changed(url, local_file_path):
|
||||||
IF_MODIFIED_SINCE: local_modification_time_str,
|
IF_MODIFIED_SINCE: local_modification_time_str,
|
||||||
CACHE_CONTROL: CACHE_CONTROL_MAX_AGE + "3600",
|
CACHE_CONTROL: CACHE_CONTROL_MAX_AGE + "3600",
|
||||||
}
|
}
|
||||||
response = requests.head(url, headers=headers, timeout=NETWORK_TIMEOUT)
|
response = requests.head(
|
||||||
|
url, headers=headers, timeout=NETWORK_TIMEOUT, allow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"has_remote_file_changed: File %s, Local modified %s, response code %d",
|
"has_remote_file_changed: File %s, Local modified %s, response code %d",
|
||||||
|
|
|
@ -6,6 +6,17 @@ font:
|
||||||
extras:
|
extras:
|
||||||
- file: "gfonts://Roboto"
|
- file: "gfonts://Roboto"
|
||||||
glyphs: ["\u00C4", "\u00C5", "\U000000C7"]
|
glyphs: ["\u00C4", "\u00C5", "\U000000C7"]
|
||||||
|
- file: "gfonts://Roboto"
|
||||||
|
id: roboto_web
|
||||||
|
size: 20
|
||||||
|
- file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf"
|
||||||
|
id: monocraft
|
||||||
|
size: 20
|
||||||
|
- file:
|
||||||
|
type: web
|
||||||
|
url: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf"
|
||||||
|
id: monocraft2
|
||||||
|
size: 24
|
||||||
|
|
||||||
spi:
|
spi:
|
||||||
clk_pin: 14
|
clk_pin: 14
|
||||||
|
|
|
@ -812,11 +812,6 @@ image:
|
||||||
file: mdi:alert-outline
|
file: mdi:alert-outline
|
||||||
type: BINARY
|
type: BINARY
|
||||||
|
|
||||||
font:
|
|
||||||
- file: "gfonts://Roboto"
|
|
||||||
id: roboto
|
|
||||||
size: 20
|
|
||||||
|
|
||||||
graph:
|
graph:
|
||||||
- id: my_graph
|
- id: my_graph
|
||||||
sensor: ha_hello_world_temperature
|
sensor: ha_hello_world_temperature
|
||||||
|
|
Loading…
Reference in a new issue