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:
Clyde Stubbs 2024-03-11 20:03:39 +11:00 committed by GitHub
parent 11b31483c3
commit e4df422798
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 282 additions and 113 deletions

View file

@ -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

View file

@ -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; }

View file

@ -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_();

View file

@ -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,
) )

View file

@ -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;

View file

@ -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

View 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

View file

@ -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"