mirror of
https://github.com/esphome/esphome.git
synced 2024-12-22 05:24:53 +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/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,45 +25,75 @@ 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 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):
|
||||
if isinstance(value, list):
|
||||
value = cv.Schema([cv.string])(value)
|
||||
value = cv.Schema([cv.string])(list(value))
|
||||
|
||||
def 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}")
|
||||
|
||||
value.sort(key=functools.cmp_to_key(comparator))
|
||||
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
|
||||
glyph_data[pos // 8] |= 0x80 >> (pos % 8)
|
||||
glyph_args[glyph] = (len(data), offset_x, offset_y, width, height)
|
||||
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)
|
||||
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"
|
||||
|
|
Loading…
Reference in a new issue