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/feedback/* @ianchi | ||||
| esphome/components/fingerprint_grow/* @OnFreund @alexborro @loongyh | ||||
| esphome/components/font/* @clydebarrow @esphome/core | ||||
| esphome/components/fs3000/* @kahrendt | ||||
| esphome/components/ft5x06/* @clydebarrow | ||||
| 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); | ||||
| } | ||||
|  | ||||
| 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 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]; | ||||
|   int ret = vsnprintf(buffer, sizeof(buffer), format, arg); | ||||
|   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) { | ||||
| @@ -423,8 +425,8 @@ void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, Te | ||||
|       break; | ||||
|   } | ||||
| } | ||||
| void Display::print(int x, int y, BaseFont *font, Color color, const char *text) { | ||||
|   this->print(x, y, font, color, TextAlign::TOP_LEFT, 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, background); | ||||
| } | ||||
| void Display::print(int x, int y, BaseFont *font, TextAlign align, const char *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) { | ||||
|   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, ...) { | ||||
|   va_list arg; | ||||
|   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); | ||||
| } | ||||
| void Display::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) { | ||||
|   va_list arg; | ||||
|   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); | ||||
| } | ||||
| void Display::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) { | ||||
|   va_list arg; | ||||
|   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); | ||||
| } | ||||
| void Display::printf(int x, int y, BaseFont *font, const char *format, ...) { | ||||
|   va_list arg; | ||||
|   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); | ||||
| } | ||||
| void Display::set_writer(display_writer_t &&writer) { this->writer_ = writer; } | ||||
|   | ||||
| @@ -200,7 +200,7 @@ class BaseImage { | ||||
|  | ||||
| class BaseFont { | ||||
|  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; | ||||
| }; | ||||
|  | ||||
| @@ -327,8 +327,10 @@ class Display : public PollingComponent { | ||||
|    * @param color The color to draw the text with. | ||||
|    * @param align The alignment of the text. | ||||
|    * @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`. | ||||
|    * | ||||
| @@ -337,8 +339,9 @@ class Display : public PollingComponent { | ||||
|    * @param font The font to draw the text with. | ||||
|    * @param color The color to draw the text with. | ||||
|    * @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`. | ||||
|    * | ||||
| @@ -359,6 +362,20 @@ class Display : public PollingComponent { | ||||
|    */ | ||||
|   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`. | ||||
|    * | ||||
|    * @param x The x coordinate of the text alignment anchor point. | ||||
| @@ -610,7 +627,8 @@ class Display : public PollingComponent { | ||||
|  protected: | ||||
|   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); | ||||
|   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 clear_clipping_(); | ||||
|   | ||||
| @@ -10,7 +10,10 @@ import requests | ||||
| from esphome import core | ||||
| import esphome.config_validation as cv | ||||
| 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 ( | ||||
|     CONF_FAMILY, | ||||
|     CONF_FILE, | ||||
| @@ -22,26 +25,29 @@ from esphome.const import ( | ||||
|     CONF_PATH, | ||||
|     CONF_WEIGHT, | ||||
| ) | ||||
| from esphome.core import CORE, HexInt | ||||
|  | ||||
| from esphome.core import ( | ||||
|     CORE, | ||||
|     HexInt, | ||||
| ) | ||||
|  | ||||
| DOMAIN = "font" | ||||
| DEPENDENCIES = ["display"] | ||||
| MULTI_CONF = True | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core", "@clydebarrow"] | ||||
|  | ||||
| font_ns = cg.esphome_ns.namespace("font") | ||||
|  | ||||
| Font = font_ns.class_("Font") | ||||
| Glyph = font_ns.class_("Glyph") | ||||
| GlyphData = font_ns.struct("GlyphData") | ||||
|  | ||||
| CONF_BPP = "bpp" | ||||
| CONF_EXTRAS = "extras" | ||||
| CONF_FONTS = "fonts" | ||||
|  | ||||
| def validate_glyphs(value): | ||||
|     if isinstance(value, list): | ||||
|         value = cv.Schema([cv.string])(value) | ||||
|     value = cv.Schema([cv.string])(list(value)) | ||||
|  | ||||
|     def comparator(x, y): | ||||
| def glyph_comparator(x, y): | ||||
|     x_ = x.encode("utf-8") | ||||
|     y_ = y.encode("utf-8") | ||||
|  | ||||
| @@ -57,10 +63,37 @@ def validate_glyphs(value): | ||||
|         return 1 | ||||
|     raise cv.Invalid(f"Found duplicate glyph {x}") | ||||
|  | ||||
|     value.sort(key=functools.cmp_to_key(comparator)) | ||||
|  | ||||
| def validate_glyphs(value): | ||||
|     if isinstance(value, list): | ||||
|         value = cv.Schema([cv.string])(value) | ||||
|     value = cv.Schema([cv.string])(list(value)) | ||||
|  | ||||
|     value.sort(key=functools.cmp_to_key(glyph_comparator)) | ||||
|     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): | ||||
|     try: | ||||
|         import PIL | ||||
| @@ -79,16 +112,16 @@ def validate_pillow_installed(value): | ||||
|     return value | ||||
|  | ||||
|  | ||||
| FONT_EXTENSIONS = (".ttf", ".woff", ".otf") | ||||
|  | ||||
|  | ||||
| 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( | ||||
|             f"Please unzip the font archive '{value}' first and then use the .ttf files inside." | ||||
|         ) | ||||
|     if not value.endswith(".ttf"): | ||||
|         raise cv.Invalid( | ||||
|             "Only truetype (.ttf) files are supported. Please make sure you're " | ||||
|             "using the correct format or rename the extension to .ttf" | ||||
|         ) | ||||
|     if not any(map(value.lower().endswith, FONT_EXTENSIONS)): | ||||
|         raise cv.Invalid(f"Only {FONT_EXTENSIONS} files are supported.") | ||||
|     return cv.file_(value) | ||||
|  | ||||
|  | ||||
| @@ -233,7 +266,6 @@ def _file_schema(value): | ||||
|  | ||||
| FILE_SCHEMA = cv.Schema(_file_schema) | ||||
|  | ||||
|  | ||||
| DEFAULT_GLYPHS = ( | ||||
|     ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' | ||||
| ) | ||||
| @@ -245,12 +277,22 @@ FONT_SCHEMA = cv.Schema( | ||||
|         cv.Required(CONF_FILE): FILE_SCHEMA, | ||||
|         cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs, | ||||
|         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_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 | ||||
| # 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) | ||||
|  | ||||
|  | ||||
| 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): | ||||
|     from PIL import PcfFontFile, BdfFontFile | ||||
|     from PIL import ( | ||||
|         PcfFontFile, | ||||
|         BdfFontFile, | ||||
|     ) | ||||
|  | ||||
|     local_bitmap_font_file = _compute_local_font_dir(filepath) / os.path.basename( | ||||
|         filepath | ||||
| @@ -347,60 +413,82 @@ def load_ttf_font(path, size): | ||||
|     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): | ||||
|     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_to_font_map = {} | ||||
|     font_list = font_map[config[CONF_ID]] | ||||
|     glyphs = [] | ||||
|     for font in font_list: | ||||
|         glyphs.extend(font.glyphs) | ||||
|         for glyph in font.glyphs: | ||||
|             glyph_to_font_map[glyph] = font | ||||
|     glyphs.sort(key=functools.cmp_to_key(glyph_comparator)) | ||||
|     glyph_args = {} | ||||
|     data = [] | ||||
|     for glyph in config[CONF_GLYPHS]: | ||||
|         mask = font.getmask(glyph, mode="1") | ||||
|     bpp = config[CONF_BPP] | ||||
|     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) | ||||
|         width, height = mask.size | ||||
|         width8 = ((width + 7) // 8) * 8 | ||||
|         glyph_data = [0] * (height * width8 // 8) | ||||
|         glyph_data = [0] * ((height * width * bpp + 7) // 8) | ||||
|         pos = 0 | ||||
|         for y in range(height): | ||||
|             for x in range(width): | ||||
|                 if not mask.getpixel((x, y)): | ||||
|                     continue | ||||
|                 pos = x + y * width8 | ||||
|                 pixel = mask.getpixel((x, y)) // scale | ||||
|                 for bit_num in range(bpp): | ||||
|                     if pixel & (1 << (bpp - bit_num - 1)): | ||||
|                         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 | ||||
|  | ||||
|     rhs = [HexInt(x) for x in data] | ||||
|     prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) | ||||
|  | ||||
|     glyph_initializer = [] | ||||
|     for glyph in config[CONF_GLYPHS]: | ||||
|     for glyph in glyphs: | ||||
|         glyph_initializer.append( | ||||
|             cg.StructInitializer( | ||||
|                 GlyphData, | ||||
|                 ("a_char", glyph), | ||||
|                 ( | ||||
|                     "a_char", | ||||
|                     cg.RawExpression(f"(const uint8_t *){cpp_string_escape(glyph)}"), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "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_y", glyph_args[glyph][2]), | ||||
|                 ("width", glyph_args[glyph][3]), | ||||
|                 ("height", glyph_args[glyph][4]), | ||||
|                 ), | ||||
|                 ("offset_x", glyph_args[glyph].offset_x), | ||||
|                 ("offset_y", glyph_args[glyph].offset_y), | ||||
|                 ("width", glyph_args[glyph].width), | ||||
|                 ("height", glyph_args[glyph].height), | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|     glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer) | ||||
|  | ||||
|     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"; | ||||
|  | ||||
| void Glyph::draw(int x_at, int y_start, display::Display *display, Color color) const { | ||||
|   int scan_x1, scan_y1, scan_width, scan_height; | ||||
|   this->scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); | ||||
|  | ||||
|   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 { | ||||
| const uint8_t *Glyph::get_char() const { return this->glyph_data_->a_char; } | ||||
| // Compare the char at the string position with this char. | ||||
| // Return true if this char is less than or equal the other. | ||||
| bool Glyph::compare_to(const uint8_t *str) const { | ||||
|   // 1 -> this->char_ | ||||
|   // 2 -> str | ||||
|   for (uint32_t i = 0;; i++) { | ||||
| @@ -48,7 +29,7 @@ bool Glyph::compare_to(const char *str) const { | ||||
|   // this should not happen | ||||
|   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++) { | ||||
|     if (this->glyph_data_->a_char[i] == '\0') | ||||
|       return i; | ||||
| @@ -65,12 +46,13 @@ void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { | ||||
|   *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); | ||||
|   for (int i = 0; i < data_nr; ++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 hi = this->glyphs_.size() - 1; | ||||
|   while (lo != hi) { | ||||
| @@ -95,7 +77,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in | ||||
|   int x = 0; | ||||
|   while (str[i] != '\0') { | ||||
|     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) { | ||||
|       // Unknown char, skip | ||||
|       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; | ||||
|   *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 x_at = x_start; | ||||
|   int scan_x1, scan_y1, scan_width, scan_height; | ||||
|   while (text[i] != '\0') { | ||||
|     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) { | ||||
|       // Unknown char, skip | ||||
|       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]; | ||||
|     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; | ||||
|  | ||||
|     i += match_length; | ||||
|   | ||||
| @@ -10,7 +10,7 @@ namespace font { | ||||
| class Font; | ||||
|  | ||||
| struct GlyphData { | ||||
|   const char *a_char; | ||||
|   const uint8_t *a_char; | ||||
|   const uint8_t *data; | ||||
|   int offset_x; | ||||
|   int offset_y; | ||||
| @@ -22,13 +22,11 @@ class Glyph { | ||||
|  public: | ||||
|   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 char *str) const; | ||||
|   int match_length(const uint8_t *str) 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 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; | ||||
|   inline int get_baseline() { return this->baseline_; } | ||||
|   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_; } | ||||
|  | ||||
| @@ -61,6 +61,7 @@ class Font : public display::BaseFont { | ||||
|   std::vector<Glyph, ExternalRAMAllocator<Glyph>> glyphs_; | ||||
|   int baseline_; | ||||
|   int height_; | ||||
|   uint8_t bpp_;  // bits per pixel | ||||
| }; | ||||
|  | ||||
| }  // 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 | ||||
|   bit_order: lsb_first | ||||
|  | ||||
| font: | ||||
|   - file: "gfonts://Roboto" | ||||
|     id: roboto | ||||
|     size: 20 | ||||
|  | ||||
| display: | ||||
|   - platform: ili9xxx | ||||
|     id: displ8 | ||||
| @@ -61,6 +66,8 @@ display: | ||||
|     reset_pin: | ||||
|       number: GPIO48 | ||||
|       allow_other_uses: true | ||||
|     lambda: |- | ||||
|       it.printf(10, 100, id(roboto), Color(0x123456), COLOR_OFF, display::TextAlign::BASELINE, "%f", id(heap_free).state); | ||||
|  | ||||
| i2c: | ||||
|   scl: GPIO18 | ||||
| @@ -85,6 +92,7 @@ binary_sensor: | ||||
| sensor: | ||||
|   - platform: debug | ||||
|     free: | ||||
|       id: heap_free | ||||
|       name: "Heap Free" | ||||
|     block: | ||||
|       name: "Max Block Free" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user