mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Font allow using google fonts directly (#3243)
This commit is contained in:
		| @@ -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}") | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user