mirror of
https://github.com/esphome/esphome.git
synced 2024-11-21 22:48:10 +01: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:
parent
11b31483c3
commit
e4df422798
8 changed files with 282 additions and 113 deletions
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue