mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	font: add anti-aliasing and other features (#6198)
* Pack glyph bits * Use unsigned chars for unicode strings. * Implement multi-bit glyphs * clang-format * Allow extra glyphs to be added to a font * Allow .otf and .woff file extensions * Add printf versions with background color; Add tests * Whitespace... * Move font test to new framework * CI fix * CI fix * CODEOWNERS * File extensions tested as case-insensitive
This commit is contained in:
		| @@ -122,6 +122,7 @@ esphome/components/factory_reset/* @anatoly-savchenkov | |||||||
| esphome/components/fastled_base/* @OttoWinter | esphome/components/fastled_base/* @OttoWinter | ||||||
| esphome/components/feedback/* @ianchi | esphome/components/feedback/* @ianchi | ||||||
| esphome/components/fingerprint_grow/* @OnFreund @alexborro @loongyh | esphome/components/fingerprint_grow/* @OnFreund @alexborro @loongyh | ||||||
|  | esphome/components/font/* @clydebarrow @esphome/core | ||||||
| esphome/components/fs3000/* @kahrendt | esphome/components/fs3000/* @kahrendt | ||||||
| esphome/components/ft5x06/* @clydebarrow | esphome/components/ft5x06/* @clydebarrow | ||||||
| esphome/components/ft63x6/* @gpambrozio | esphome/components/ft63x6/* @gpambrozio | ||||||
|   | |||||||
| @@ -319,17 +319,19 @@ void Display::filled_regular_polygon(int x, int y, int radius, int edges, Color | |||||||
|   regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, DRAWING_FILLED); |   regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, DRAWING_FILLED); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Display::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text) { | void Display::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text, Color background) { | ||||||
|   int x_start, y_start; |   int x_start, y_start; | ||||||
|   int width, height; |   int width, height; | ||||||
|   this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height); |   this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height); | ||||||
|   font->print(x_start, y_start, this, color, text); |   font->print(x_start, y_start, this, color, text, background); | ||||||
| } | } | ||||||
| void Display::vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, va_list arg) { |  | ||||||
|  | void Display::vprintf_(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, | ||||||
|  |                        va_list arg) { | ||||||
|   char buffer[256]; |   char buffer[256]; | ||||||
|   int ret = vsnprintf(buffer, sizeof(buffer), format, arg); |   int ret = vsnprintf(buffer, sizeof(buffer), format, arg); | ||||||
|   if (ret > 0) |   if (ret > 0) | ||||||
|     this->print(x, y, font, color, align, buffer); |     this->print(x, y, font, color, align, buffer, background); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Display::image(int x, int y, BaseImage *image, Color color_on, Color color_off) { | void Display::image(int x, int y, BaseImage *image, Color color_on, Color color_off) { | ||||||
| @@ -423,8 +425,8 @@ void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, Te | |||||||
|       break; |       break; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| void Display::print(int x, int y, BaseFont *font, Color color, const char *text) { | void Display::print(int x, int y, BaseFont *font, Color color, const char *text, Color background) { | ||||||
|   this->print(x, y, font, color, TextAlign::TOP_LEFT, text); |   this->print(x, y, font, color, TextAlign::TOP_LEFT, text, background); | ||||||
| } | } | ||||||
| void Display::print(int x, int y, BaseFont *font, TextAlign align, const char *text) { | void Display::print(int x, int y, BaseFont *font, TextAlign align, const char *text) { | ||||||
|   this->print(x, y, font, COLOR_ON, align, text); |   this->print(x, y, font, COLOR_ON, align, text); | ||||||
| @@ -432,28 +434,35 @@ void Display::print(int x, int y, BaseFont *font, TextAlign align, const char *t | |||||||
| void Display::print(int x, int y, BaseFont *font, const char *text) { | void Display::print(int x, int y, BaseFont *font, const char *text) { | ||||||
|   this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text); |   this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text); | ||||||
| } | } | ||||||
|  | void Display::printf(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, | ||||||
|  |                      ...) { | ||||||
|  |   va_list arg; | ||||||
|  |   va_start(arg, format); | ||||||
|  |   this->vprintf_(x, y, font, color, background, align, format, arg); | ||||||
|  |   va_end(arg); | ||||||
|  | } | ||||||
| void Display::printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) { | void Display::printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) { | ||||||
|   va_list arg; |   va_list arg; | ||||||
|   va_start(arg, format); |   va_start(arg, format); | ||||||
|   this->vprintf_(x, y, font, color, align, format, arg); |   this->vprintf_(x, y, font, color, COLOR_OFF, align, format, arg); | ||||||
|   va_end(arg); |   va_end(arg); | ||||||
| } | } | ||||||
| void Display::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) { | void Display::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) { | ||||||
|   va_list arg; |   va_list arg; | ||||||
|   va_start(arg, format); |   va_start(arg, format); | ||||||
|   this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg); |   this->vprintf_(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, arg); | ||||||
|   va_end(arg); |   va_end(arg); | ||||||
| } | } | ||||||
| void Display::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) { | void Display::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) { | ||||||
|   va_list arg; |   va_list arg; | ||||||
|   va_start(arg, format); |   va_start(arg, format); | ||||||
|   this->vprintf_(x, y, font, COLOR_ON, align, format, arg); |   this->vprintf_(x, y, font, COLOR_ON, COLOR_OFF, align, format, arg); | ||||||
|   va_end(arg); |   va_end(arg); | ||||||
| } | } | ||||||
| void Display::printf(int x, int y, BaseFont *font, const char *format, ...) { | void Display::printf(int x, int y, BaseFont *font, const char *format, ...) { | ||||||
|   va_list arg; |   va_list arg; | ||||||
|   va_start(arg, format); |   va_start(arg, format); | ||||||
|   this->vprintf_(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, arg); |   this->vprintf_(x, y, font, COLOR_ON, COLOR_OFF, TextAlign::TOP_LEFT, format, arg); | ||||||
|   va_end(arg); |   va_end(arg); | ||||||
| } | } | ||||||
| void Display::set_writer(display_writer_t &&writer) { this->writer_ = writer; } | void Display::set_writer(display_writer_t &&writer) { this->writer_ = writer; } | ||||||
|   | |||||||
| @@ -200,7 +200,7 @@ class BaseImage { | |||||||
|  |  | ||||||
| class BaseFont { | class BaseFont { | ||||||
|  public: |  public: | ||||||
|   virtual void print(int x, int y, Display *display, Color color, const char *text) = 0; |   virtual void print(int x, int y, Display *display, Color color, const char *text, Color background) = 0; | ||||||
|   virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0; |   virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -327,8 +327,10 @@ class Display : public PollingComponent { | |||||||
|    * @param color The color to draw the text with. |    * @param color The color to draw the text with. | ||||||
|    * @param align The alignment of the text. |    * @param align The alignment of the text. | ||||||
|    * @param text The text to draw. |    * @param text The text to draw. | ||||||
|  |    * @param background When using multi-bit (anti-aliased) fonts, blend this background color into pixels | ||||||
|    */ |    */ | ||||||
|   void print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text); |   void print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text, | ||||||
|  |              Color background = COLOR_OFF); | ||||||
|  |  | ||||||
|   /** Print `text` with the top left at [x,y] with `font`. |   /** Print `text` with the top left at [x,y] with `font`. | ||||||
|    * |    * | ||||||
| @@ -337,8 +339,9 @@ class Display : public PollingComponent { | |||||||
|    * @param font The font to draw the text with. |    * @param font The font to draw the text with. | ||||||
|    * @param color The color to draw the text with. |    * @param color The color to draw the text with. | ||||||
|    * @param text The text to draw. |    * @param text The text to draw. | ||||||
|  |    * @param background When using multi-bit (anti-aliased) fonts, blend this background color into pixels | ||||||
|    */ |    */ | ||||||
|   void print(int x, int y, BaseFont *font, Color color, const char *text); |   void print(int x, int y, BaseFont *font, Color color, const char *text, Color background = COLOR_OFF); | ||||||
|  |  | ||||||
|   /** Print `text` with the anchor point at [x,y] with `font`. |   /** Print `text` with the anchor point at [x,y] with `font`. | ||||||
|    * |    * | ||||||
| @@ -359,6 +362,20 @@ class Display : public PollingComponent { | |||||||
|    */ |    */ | ||||||
|   void print(int x, int y, BaseFont *font, const char *text); |   void print(int x, int y, BaseFont *font, const char *text); | ||||||
|  |  | ||||||
|  |   /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. | ||||||
|  |    * | ||||||
|  |    * @param x The x coordinate of the text alignment anchor point. | ||||||
|  |    * @param y The y coordinate of the text alignment anchor point. | ||||||
|  |    * @param font The font to draw the text with. | ||||||
|  |    * @param color The color to draw the text with. | ||||||
|  |    * @param background The background color to use for anti-aliasing | ||||||
|  |    * @param align The alignment of the text. | ||||||
|  |    * @param format The format to use. | ||||||
|  |    * @param ... The arguments to use for the text formatting. | ||||||
|  |    */ | ||||||
|  |   void printf(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, ...) | ||||||
|  |       __attribute__((format(printf, 8, 9))); | ||||||
|  |  | ||||||
|   /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. |   /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. | ||||||
|    * |    * | ||||||
|    * @param x The x coordinate of the text alignment anchor point. |    * @param x The x coordinate of the text alignment anchor point. | ||||||
| @@ -610,7 +627,8 @@ class Display : public PollingComponent { | |||||||
|  protected: |  protected: | ||||||
|   bool clamp_x_(int x, int w, int &min_x, int &max_x); |   bool clamp_x_(int x, int w, int &min_x, int &max_x); | ||||||
|   bool clamp_y_(int y, int h, int &min_y, int &max_y); |   bool clamp_y_(int y, int h, int &min_y, int &max_y); | ||||||
|   void vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, va_list arg); |   void vprintf_(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, | ||||||
|  |                 va_list arg); | ||||||
|  |  | ||||||
|   void do_update_(); |   void do_update_(); | ||||||
|   void clear_clipping_(); |   void clear_clipping_(); | ||||||
|   | |||||||
| @@ -10,7 +10,10 @@ import requests | |||||||
| from esphome import core | from esphome import core | ||||||
| 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.helpers import ( | ||||||
|  |     copy_file_if_changed, | ||||||
|  |     cpp_string_escape, | ||||||
|  | ) | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_FAMILY, |     CONF_FAMILY, | ||||||
|     CONF_FILE, |     CONF_FILE, | ||||||
| @@ -22,45 +25,75 @@ from esphome.const import ( | |||||||
|     CONF_PATH, |     CONF_PATH, | ||||||
|     CONF_WEIGHT, |     CONF_WEIGHT, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, HexInt | from esphome.core import ( | ||||||
|  |     CORE, | ||||||
|  |     HexInt, | ||||||
|  | ) | ||||||
|  |  | ||||||
| DOMAIN = "font" | DOMAIN = "font" | ||||||
| DEPENDENCIES = ["display"] | DEPENDENCIES = ["display"] | ||||||
| MULTI_CONF = True | MULTI_CONF = True | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@esphome/core", "@clydebarrow"] | ||||||
|  |  | ||||||
| font_ns = cg.esphome_ns.namespace("font") | font_ns = cg.esphome_ns.namespace("font") | ||||||
|  |  | ||||||
| Font = font_ns.class_("Font") | Font = font_ns.class_("Font") | ||||||
| Glyph = font_ns.class_("Glyph") | Glyph = font_ns.class_("Glyph") | ||||||
| GlyphData = font_ns.struct("GlyphData") | GlyphData = font_ns.struct("GlyphData") | ||||||
|  |  | ||||||
|  | CONF_BPP = "bpp" | ||||||
|  | CONF_EXTRAS = "extras" | ||||||
|  | CONF_FONTS = "fonts" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def glyph_comparator(x, y): | ||||||
|  |     x_ = x.encode("utf-8") | ||||||
|  |     y_ = y.encode("utf-8") | ||||||
|  |  | ||||||
|  |     for c in range(min(len(x_), len(y_))): | ||||||
|  |         if x_[c] < y_[c]: | ||||||
|  |             return -1 | ||||||
|  |         if x_[c] > y_[c]: | ||||||
|  |             return 1 | ||||||
|  |  | ||||||
|  |     if len(x_) < len(y_): | ||||||
|  |         return -1 | ||||||
|  |     if len(x_) > len(y_): | ||||||
|  |         return 1 | ||||||
|  |     raise cv.Invalid(f"Found duplicate glyph {x}") | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_glyphs(value): | def validate_glyphs(value): | ||||||
|     if isinstance(value, list): |     if isinstance(value, list): | ||||||
|         value = cv.Schema([cv.string])(value) |         value = cv.Schema([cv.string])(value) | ||||||
|     value = cv.Schema([cv.string])(list(value)) |     value = cv.Schema([cv.string])(list(value)) | ||||||
|  |  | ||||||
|     def comparator(x, y): |     value.sort(key=functools.cmp_to_key(glyph_comparator)) | ||||||
|         x_ = x.encode("utf-8") |  | ||||||
|         y_ = y.encode("utf-8") |  | ||||||
|  |  | ||||||
|         for c in range(min(len(x_), len(y_))): |  | ||||||
|             if x_[c] < y_[c]: |  | ||||||
|                 return -1 |  | ||||||
|             if x_[c] > y_[c]: |  | ||||||
|                 return 1 |  | ||||||
|  |  | ||||||
|         if len(x_) < len(y_): |  | ||||||
|             return -1 |  | ||||||
|         if len(x_) > len(y_): |  | ||||||
|             return 1 |  | ||||||
|         raise cv.Invalid(f"Found duplicate glyph {x}") |  | ||||||
|  |  | ||||||
|     value.sort(key=functools.cmp_to_key(comparator)) |  | ||||||
|     return value |     return value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | font_map = {} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def merge_glyphs(config): | ||||||
|  |     glyphs = [] | ||||||
|  |     glyphs.extend(config[CONF_GLYPHS]) | ||||||
|  |     font_list = [(EFont(config[CONF_FILE], config[CONF_SIZE], config[CONF_GLYPHS]))] | ||||||
|  |     if extras := config.get(CONF_EXTRAS): | ||||||
|  |         extra_fonts = list( | ||||||
|  |             map( | ||||||
|  |                 lambda x: EFont(x[CONF_FILE], config[CONF_SIZE], x[CONF_GLYPHS]), extras | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         font_list.extend(extra_fonts) | ||||||
|  |         for extra in extras: | ||||||
|  |             glyphs.extend(extra[CONF_GLYPHS]) | ||||||
|  |         validate_glyphs(glyphs) | ||||||
|  |     font_map[config[CONF_ID]] = font_list | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_pillow_installed(value): | def validate_pillow_installed(value): | ||||||
|     try: |     try: | ||||||
|         import PIL |         import PIL | ||||||
| @@ -79,16 +112,16 @@ def validate_pillow_installed(value): | |||||||
|     return value |     return value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | FONT_EXTENSIONS = (".ttf", ".woff", ".otf") | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_truetype_file(value): | def validate_truetype_file(value): | ||||||
|     if value.endswith(".zip"):  # for Google Fonts downloads |     if value.lower().endswith(".zip"):  # for Google Fonts downloads | ||||||
|         raise cv.Invalid( |         raise cv.Invalid( | ||||||
|             f"Please unzip the font archive '{value}' first and then use the .ttf files inside." |             f"Please unzip the font archive '{value}' first and then use the .ttf files inside." | ||||||
|         ) |         ) | ||||||
|     if not value.endswith(".ttf"): |     if not any(map(value.lower().endswith, FONT_EXTENSIONS)): | ||||||
|         raise cv.Invalid( |         raise cv.Invalid(f"Only {FONT_EXTENSIONS} files are supported.") | ||||||
|             "Only truetype (.ttf) files are supported. Please make sure you're " |  | ||||||
|             "using the correct format or rename the extension to .ttf" |  | ||||||
|         ) |  | ||||||
|     return cv.file_(value) |     return cv.file_(value) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -233,7 +266,6 @@ def _file_schema(value): | |||||||
|  |  | ||||||
| FILE_SCHEMA = cv.Schema(_file_schema) | FILE_SCHEMA = cv.Schema(_file_schema) | ||||||
|  |  | ||||||
|  |  | ||||||
| DEFAULT_GLYPHS = ( | DEFAULT_GLYPHS = ( | ||||||
|     ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' |     ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' | ||||||
| ) | ) | ||||||
| @@ -245,12 +277,22 @@ FONT_SCHEMA = cv.Schema( | |||||||
|         cv.Required(CONF_FILE): FILE_SCHEMA, |         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.Optional(CONF_BPP, default=1): cv.one_of(1, 2, 4, 8), | ||||||
|  |         cv.Optional(CONF_EXTRAS): cv.ensure_list( | ||||||
|  |             cv.Schema( | ||||||
|  |                 { | ||||||
|  |                     cv.Required(CONF_FILE): FILE_SCHEMA, | ||||||
|  |                     cv.Required(CONF_GLYPHS): validate_glyphs, | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |         ), | ||||||
|         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) | CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, merge_glyphs) | ||||||
|  |  | ||||||
|  |  | ||||||
| # PIL doesn't provide a consistent interface for both TrueType and bitmap | # 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. | # fonts. So, we use our own wrappers to give us the consistency that we need. | ||||||
| @@ -292,8 +334,32 @@ class BitmapFontWrapper: | |||||||
|         return (max_height, 0) |         return (max_height, 0) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class EFont: | ||||||
|  |     def __init__(self, file, size, glyphs): | ||||||
|  |         self.glyphs = glyphs | ||||||
|  |         ftype = file[CONF_TYPE] | ||||||
|  |         if ftype == TYPE_LOCAL_BITMAP: | ||||||
|  |             font = load_bitmap_font(CORE.relative_config_path(file[CONF_PATH])) | ||||||
|  |         elif ftype == TYPE_LOCAL: | ||||||
|  |             path = CORE.relative_config_path(file[CONF_PATH]) | ||||||
|  |             font = load_ttf_font(path, size) | ||||||
|  |         elif ftype == TYPE_GFONTS: | ||||||
|  |             path = _compute_gfonts_local_path(file) | ||||||
|  |             font = load_ttf_font(path, size) | ||||||
|  |         else: | ||||||
|  |             raise cv.Invalid(f"Could not load font: unknown type: {ftype}") | ||||||
|  |         self.font = font | ||||||
|  |         self.ascent, self.descent = font.getmetrics(glyphs) | ||||||
|  |  | ||||||
|  |     def has_glyph(self, glyph): | ||||||
|  |         return glyph in self.glyphs | ||||||
|  |  | ||||||
|  |  | ||||||
| def convert_bitmap_to_pillow_font(filepath): | def convert_bitmap_to_pillow_font(filepath): | ||||||
|     from PIL import PcfFontFile, BdfFontFile |     from PIL import ( | ||||||
|  |         PcfFontFile, | ||||||
|  |         BdfFontFile, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     local_bitmap_font_file = _compute_local_font_dir(filepath) / os.path.basename( |     local_bitmap_font_file = _compute_local_font_dir(filepath) / os.path.basename( | ||||||
|         filepath |         filepath | ||||||
| @@ -347,60 +413,82 @@ def load_ttf_font(path, size): | |||||||
|     return TrueTypeFontWrapper(font) |     return TrueTypeFontWrapper(font) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class GlyphInfo: | ||||||
|  |     def __init__(self, data_len, offset_x, offset_y, width, height): | ||||||
|  |         self.data_len = data_len | ||||||
|  |         self.offset_x = offset_x | ||||||
|  |         self.offset_y = offset_y | ||||||
|  |         self.width = width | ||||||
|  |         self.height = height | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     conf = config[CONF_FILE] |     glyph_to_font_map = {} | ||||||
|     if conf[CONF_TYPE] == TYPE_LOCAL_BITMAP: |     font_list = font_map[config[CONF_ID]] | ||||||
|         font = load_bitmap_font(CORE.relative_config_path(conf[CONF_PATH])) |     glyphs = [] | ||||||
|     elif conf[CONF_TYPE] == TYPE_LOCAL: |     for font in font_list: | ||||||
|         path = CORE.relative_config_path(conf[CONF_PATH]) |         glyphs.extend(font.glyphs) | ||||||
|         font = load_ttf_font(path, config[CONF_SIZE]) |         for glyph in font.glyphs: | ||||||
|     elif conf[CONF_TYPE] == TYPE_GFONTS: |             glyph_to_font_map[glyph] = font | ||||||
|         path = _compute_gfonts_local_path(conf) |     glyphs.sort(key=functools.cmp_to_key(glyph_comparator)) | ||||||
|         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]: |     bpp = config[CONF_BPP] | ||||||
|         mask = font.getmask(glyph, mode="1") |     if bpp == 1: | ||||||
|  |         mode = "1" | ||||||
|  |         scale = 1 | ||||||
|  |     else: | ||||||
|  |         mode = "L" | ||||||
|  |         scale = 256 // (1 << bpp) | ||||||
|  |     for glyph in glyphs: | ||||||
|  |         font = glyph_to_font_map[glyph].font | ||||||
|  |         mask = font.getmask(glyph, mode=mode) | ||||||
|         offset_x, offset_y = font.getoffset(glyph) |         offset_x, offset_y = font.getoffset(glyph) | ||||||
|         width, height = mask.size |         width, height = mask.size | ||||||
|         width8 = ((width + 7) // 8) * 8 |         glyph_data = [0] * ((height * width * bpp + 7) // 8) | ||||||
|         glyph_data = [0] * (height * width8 // 8) |         pos = 0 | ||||||
|         for y in range(height): |         for y in range(height): | ||||||
|             for x in range(width): |             for x in range(width): | ||||||
|                 if not mask.getpixel((x, y)): |                 pixel = mask.getpixel((x, y)) // scale | ||||||
|                     continue |                 for bit_num in range(bpp): | ||||||
|                 pos = x + y * width8 |                     if pixel & (1 << (bpp - bit_num - 1)): | ||||||
|                 glyph_data[pos // 8] |= 0x80 >> (pos % 8) |                         glyph_data[pos // 8] |= 0x80 >> (pos % 8) | ||||||
|         glyph_args[glyph] = (len(data), offset_x, offset_y, width, height) |                     pos += 1 | ||||||
|  |         glyph_args[glyph] = GlyphInfo(len(data), offset_x, offset_y, width, height) | ||||||
|         data += glyph_data |         data += glyph_data | ||||||
|  |  | ||||||
|     rhs = [HexInt(x) for x in data] |     rhs = [HexInt(x) for x in data] | ||||||
|     prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) |     prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) | ||||||
|  |  | ||||||
|     glyph_initializer = [] |     glyph_initializer = [] | ||||||
|     for glyph in config[CONF_GLYPHS]: |     for glyph in glyphs: | ||||||
|         glyph_initializer.append( |         glyph_initializer.append( | ||||||
|             cg.StructInitializer( |             cg.StructInitializer( | ||||||
|                 GlyphData, |                 GlyphData, | ||||||
|                 ("a_char", glyph), |                 ( | ||||||
|  |                     "a_char", | ||||||
|  |                     cg.RawExpression(f"(const uint8_t *){cpp_string_escape(glyph)}"), | ||||||
|  |                 ), | ||||||
|                 ( |                 ( | ||||||
|                     "data", |                     "data", | ||||||
|                     cg.RawExpression(f"{str(prog_arr)} + {str(glyph_args[glyph][0])}"), |                     cg.RawExpression( | ||||||
|  |                         f"{str(prog_arr)} + {str(glyph_args[glyph].data_len)}" | ||||||
|  |                     ), | ||||||
|                 ), |                 ), | ||||||
|                 ("offset_x", glyph_args[glyph][1]), |                 ("offset_x", glyph_args[glyph].offset_x), | ||||||
|                 ("offset_y", glyph_args[glyph][2]), |                 ("offset_y", glyph_args[glyph].offset_y), | ||||||
|                 ("width", glyph_args[glyph][3]), |                 ("width", glyph_args[glyph].width), | ||||||
|                 ("height", glyph_args[glyph][4]), |                 ("height", glyph_args[glyph].height), | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer) |     glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer) | ||||||
|  |  | ||||||
|     cg.new_Pvariable( |     cg.new_Pvariable( | ||||||
|         config[CONF_ID], glyphs, len(glyph_initializer), ascent, ascent + descent |         config[CONF_ID], | ||||||
|  |         glyphs, | ||||||
|  |         len(glyph_initializer), | ||||||
|  |         font_list[0].ascent, | ||||||
|  |         font_list[0].ascent + font_list[0].descent, | ||||||
|  |         bpp, | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -10,29 +10,10 @@ namespace font { | |||||||
|  |  | ||||||
| static const char *const TAG = "font"; | static const char *const TAG = "font"; | ||||||
|  |  | ||||||
| void Glyph::draw(int x_at, int y_start, display::Display *display, Color color) const { | const uint8_t *Glyph::get_char() const { return this->glyph_data_->a_char; } | ||||||
|   int scan_x1, scan_y1, scan_width, scan_height; | // Compare the char at the string position with this char. | ||||||
|   this->scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); | // Return true if this char is less than or equal the other. | ||||||
|  | bool Glyph::compare_to(const uint8_t *str) const { | ||||||
|   const unsigned char *data = this->glyph_data_->data; |  | ||||||
|   const int max_x = x_at + scan_x1 + scan_width; |  | ||||||
|   const int max_y = y_start + scan_y1 + scan_height; |  | ||||||
|  |  | ||||||
|   for (int glyph_y = y_start + scan_y1; glyph_y < max_y; glyph_y++) { |  | ||||||
|     for (int glyph_x = x_at + scan_x1; glyph_x < max_x; data++, glyph_x += 8) { |  | ||||||
|       uint8_t pixel_data = progmem_read_byte(data); |  | ||||||
|       const int pixel_max_x = std::min(max_x, glyph_x + 8); |  | ||||||
|  |  | ||||||
|       for (int pixel_x = glyph_x; pixel_x < pixel_max_x && pixel_data; pixel_x++, pixel_data <<= 1) { |  | ||||||
|         if (pixel_data & 0x80) { |  | ||||||
|           display->draw_pixel_at(pixel_x, glyph_y, color); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| const char *Glyph::get_char() const { return this->glyph_data_->a_char; } |  | ||||||
| bool Glyph::compare_to(const char *str) const { |  | ||||||
|   // 1 -> this->char_ |   // 1 -> this->char_ | ||||||
|   // 2 -> str |   // 2 -> str | ||||||
|   for (uint32_t i = 0;; i++) { |   for (uint32_t i = 0;; i++) { | ||||||
| @@ -48,7 +29,7 @@ bool Glyph::compare_to(const char *str) const { | |||||||
|   // this should not happen |   // this should not happen | ||||||
|   return false; |   return false; | ||||||
| } | } | ||||||
| int Glyph::match_length(const char *str) const { | int Glyph::match_length(const uint8_t *str) const { | ||||||
|   for (uint32_t i = 0;; i++) { |   for (uint32_t i = 0;; i++) { | ||||||
|     if (this->glyph_data_->a_char[i] == '\0') |     if (this->glyph_data_->a_char[i] == '\0') | ||||||
|       return i; |       return i; | ||||||
| @@ -65,12 +46,13 @@ void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { | |||||||
|   *height = this->glyph_data_->height; |   *height = this->glyph_data_->height; | ||||||
| } | } | ||||||
|  |  | ||||||
| Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) { | Font::Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp) | ||||||
|  |     : baseline_(baseline), height_(height), bpp_(bpp) { | ||||||
|   glyphs_.reserve(data_nr); |   glyphs_.reserve(data_nr); | ||||||
|   for (int i = 0; i < data_nr; ++i) |   for (int i = 0; i < data_nr; ++i) | ||||||
|     glyphs_.emplace_back(&data[i]); |     glyphs_.emplace_back(&data[i]); | ||||||
| } | } | ||||||
| int Font::match_next_glyph(const char *str, int *match_length) { | int Font::match_next_glyph(const uint8_t *str, int *match_length) { | ||||||
|   int lo = 0; |   int lo = 0; | ||||||
|   int hi = this->glyphs_.size() - 1; |   int hi = this->glyphs_.size() - 1; | ||||||
|   while (lo != hi) { |   while (lo != hi) { | ||||||
| @@ -95,7 +77,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in | |||||||
|   int x = 0; |   int x = 0; | ||||||
|   while (str[i] != '\0') { |   while (str[i] != '\0') { | ||||||
|     int match_length; |     int match_length; | ||||||
|     int glyph_n = this->match_next_glyph(str + i, &match_length); |     int glyph_n = this->match_next_glyph((const uint8_t *) str + i, &match_length); | ||||||
|     if (glyph_n < 0) { |     if (glyph_n < 0) { | ||||||
|       // Unknown char, skip |       // Unknown char, skip | ||||||
|       if (!this->get_glyphs().empty()) |       if (!this->get_glyphs().empty()) | ||||||
| @@ -118,12 +100,13 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in | |||||||
|   *x_offset = min_x; |   *x_offset = min_x; | ||||||
|   *width = x - min_x; |   *width = x - min_x; | ||||||
| } | } | ||||||
| void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text) { | void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text, Color background) { | ||||||
|   int i = 0; |   int i = 0; | ||||||
|   int x_at = x_start; |   int x_at = x_start; | ||||||
|  |   int scan_x1, scan_y1, scan_width, scan_height; | ||||||
|   while (text[i] != '\0') { |   while (text[i] != '\0') { | ||||||
|     int match_length; |     int match_length; | ||||||
|     int glyph_n = this->match_next_glyph(text + i, &match_length); |     int glyph_n = this->match_next_glyph((const uint8_t *) text + i, &match_length); | ||||||
|     if (glyph_n < 0) { |     if (glyph_n < 0) { | ||||||
|       // Unknown char, skip |       // Unknown char, skip | ||||||
|       ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); |       ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); | ||||||
| @@ -138,7 +121,41 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const Glyph &glyph = this->get_glyphs()[glyph_n]; |     const Glyph &glyph = this->get_glyphs()[glyph_n]; | ||||||
|     glyph.draw(x_at, y_start, display, color); |     glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); | ||||||
|  |  | ||||||
|  |     const uint8_t *data = glyph.glyph_data_->data; | ||||||
|  |     const int max_x = x_at + scan_x1 + scan_width; | ||||||
|  |     const int max_y = y_start + scan_y1 + scan_height; | ||||||
|  |  | ||||||
|  |     uint8_t bitmask = 0; | ||||||
|  |     uint8_t pixel_data = 0; | ||||||
|  |     float bpp_max = (1 << this->bpp_) - 1; | ||||||
|  |     for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) { | ||||||
|  |       for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) { | ||||||
|  |         uint8_t pixel = 0; | ||||||
|  |         for (int bit_num = 0; bit_num != this->bpp_; bit_num++) { | ||||||
|  |           if (bitmask == 0) { | ||||||
|  |             pixel_data = progmem_read_byte(data++); | ||||||
|  |             bitmask = 0x80; | ||||||
|  |           } | ||||||
|  |           pixel <<= 1; | ||||||
|  |           if ((pixel_data & bitmask) != 0) | ||||||
|  |             pixel |= 1; | ||||||
|  |           bitmask >>= 1; | ||||||
|  |         } | ||||||
|  |         if (pixel == bpp_max) { | ||||||
|  |           display->draw_pixel_at(glyph_x, glyph_y, color); | ||||||
|  |         } else if (pixel != 0) { | ||||||
|  |           float on = (float) pixel / bpp_max; | ||||||
|  |           float off = 1.0 - on; | ||||||
|  |           Color blended; | ||||||
|  |           blended.r = color.r * on + background.r * off; | ||||||
|  |           blended.g = color.r * on + background.g * off; | ||||||
|  |           blended.b = color.r * on + background.b * off; | ||||||
|  |           display->draw_pixel_at(glyph_x, glyph_y, blended); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|     x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; |     x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; | ||||||
|  |  | ||||||
|     i += match_length; |     i += match_length; | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ namespace font { | |||||||
| class Font; | class Font; | ||||||
|  |  | ||||||
| struct GlyphData { | struct GlyphData { | ||||||
|   const char *a_char; |   const uint8_t *a_char; | ||||||
|   const uint8_t *data; |   const uint8_t *data; | ||||||
|   int offset_x; |   int offset_x; | ||||||
|   int offset_y; |   int offset_y; | ||||||
| @@ -22,13 +22,11 @@ class Glyph { | |||||||
|  public: |  public: | ||||||
|   Glyph(const GlyphData *data) : glyph_data_(data) {} |   Glyph(const GlyphData *data) : glyph_data_(data) {} | ||||||
|  |  | ||||||
|   void draw(int x, int y, display::Display *display, Color color) const; |   const uint8_t *get_char() const; | ||||||
|  |  | ||||||
|   const char *get_char() const; |   bool compare_to(const uint8_t *str) const; | ||||||
|  |  | ||||||
|   bool compare_to(const char *str) const; |   int match_length(const uint8_t *str) const; | ||||||
|  |  | ||||||
|   int match_length(const char *str) const; |  | ||||||
|  |  | ||||||
|   void scan_area(int *x1, int *y1, int *width, int *height) const; |   void scan_area(int *x1, int *y1, int *width, int *height) const; | ||||||
|  |  | ||||||
| @@ -46,14 +44,16 @@ class Font : public display::BaseFont { | |||||||
|    * @param baseline The y-offset from the top of the text to the baseline. |    * @param baseline The y-offset from the top of the text to the baseline. | ||||||
|    * @param bottom The y-offset from the top of the text to the bottom (i.e. height). |    * @param bottom The y-offset from the top of the text to the bottom (i.e. height). | ||||||
|    */ |    */ | ||||||
|   Font(const GlyphData *data, int data_nr, int baseline, int height); |   Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp = 1); | ||||||
|  |  | ||||||
|   int match_next_glyph(const char *str, int *match_length); |   int match_next_glyph(const uint8_t *str, int *match_length); | ||||||
|  |  | ||||||
|   void print(int x_start, int y_start, display::Display *display, Color color, const char *text) override; |   void print(int x_start, int y_start, display::Display *display, Color color, const char *text, | ||||||
|  |              Color background) override; | ||||||
|   void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) override; |   void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) override; | ||||||
|   inline int get_baseline() { return this->baseline_; } |   inline int get_baseline() { return this->baseline_; } | ||||||
|   inline int get_height() { return this->height_; } |   inline int get_height() { return this->height_; } | ||||||
|  |   inline int get_bpp() { return this->bpp_; } | ||||||
|  |  | ||||||
|   const std::vector<Glyph, ExternalRAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; } |   const std::vector<Glyph, ExternalRAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; } | ||||||
|  |  | ||||||
| @@ -61,6 +61,7 @@ class Font : public display::BaseFont { | |||||||
|   std::vector<Glyph, ExternalRAMAllocator<Glyph>> glyphs_; |   std::vector<Glyph, ExternalRAMAllocator<Glyph>> glyphs_; | ||||||
|   int baseline_; |   int baseline_; | ||||||
|   int height_; |   int height_; | ||||||
|  |   uint8_t bpp_;  // bits per pixel | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace font | }  // namespace font | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								tests/components/font/test.esp32.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								tests/components/font/test.esp32.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | font: | ||||||
|  |   - file: "gfonts://Roboto" | ||||||
|  |     id: roboto | ||||||
|  |     size: 20 | ||||||
|  |     glyphs: "0123456789." | ||||||
|  |     extras: | ||||||
|  |       - file: "gfonts://Roboto" | ||||||
|  |         glyphs: ["\u00C4", "\u00C5", "\U000000C7"] | ||||||
|  |  | ||||||
|  | spi: | ||||||
|  |   clk_pin: 14 | ||||||
|  |   mosi_pin: 13 | ||||||
|  |  | ||||||
|  | display: | ||||||
|  |   - id: my_display | ||||||
|  |     platform: ili9xxx | ||||||
|  |     dimensions: 480x320 | ||||||
|  |     model: ST7796 | ||||||
|  |     cs_pin: 15 | ||||||
|  |     dc_pin: 21 | ||||||
|  |     reset_pin: 22 | ||||||
|  |     transform: | ||||||
|  |       swap_xy: true | ||||||
|  |       mirror_x: true | ||||||
|  |       mirror_y: true | ||||||
|  |     auto_clear_enabled: false | ||||||
|  |  | ||||||
| @@ -52,6 +52,11 @@ spi_device: | |||||||
|   mode: 3 |   mode: 3 | ||||||
|   bit_order: lsb_first |   bit_order: lsb_first | ||||||
|  |  | ||||||
|  | font: | ||||||
|  |   - file: "gfonts://Roboto" | ||||||
|  |     id: roboto | ||||||
|  |     size: 20 | ||||||
|  |  | ||||||
| display: | display: | ||||||
|   - platform: ili9xxx |   - platform: ili9xxx | ||||||
|     id: displ8 |     id: displ8 | ||||||
| @@ -61,6 +66,8 @@ display: | |||||||
|     reset_pin: |     reset_pin: | ||||||
|       number: GPIO48 |       number: GPIO48 | ||||||
|       allow_other_uses: true |       allow_other_uses: true | ||||||
|  |     lambda: |- | ||||||
|  |       it.printf(10, 100, id(roboto), Color(0x123456), COLOR_OFF, display::TextAlign::BASELINE, "%f", id(heap_free).state); | ||||||
|  |  | ||||||
| i2c: | i2c: | ||||||
|   scl: GPIO18 |   scl: GPIO18 | ||||||
| @@ -85,6 +92,7 @@ binary_sensor: | |||||||
| sensor: | sensor: | ||||||
|   - platform: debug |   - platform: debug | ||||||
|     free: |     free: | ||||||
|  |       id: heap_free | ||||||
|       name: "Heap Free" |       name: "Heap Free" | ||||||
|     block: |     block: | ||||||
|       name: "Max Block Free" |       name: "Max Block Free" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user