feat(freetype): support variable weight fonts (#9844)

Signed-off-by: pengyiqiang <pengyiqiang@xiaomi.com>
Co-authored-by: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
VIFEX
2026-05-08 20:42:56 +02:00
committed by GitHub
co-authored by AndyEveritt cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
parent 41cc350d2b
commit 5debd9f0f1
18 changed files with 230 additions and 12 deletions
+11
View File
@@ -98,6 +98,17 @@ software, and can be set by using following values where style values are requir
These values can be combined, e.g.
<ApiLink name="LV_FREETYPE_FONT_STYLE_BOLD" display="LV_FREETYPE_FONT_STYLE_BOLD | LV_FREETYPE_FONT_STYLE_ITALIC" />.
<ApiLink name="LV_FREETYPE_FONT_STYLE_BOLD" /> is only supported with `LV_FREETYPE_FONT_RENDER_MODE_OUTLINE`,
unless the font is variable weight.
Variable weight fonts are supported. You can set the weight of the font by using
<ApiLink name="lv_freetype_font_create_with_info" /> and setting the `weight` field of
<ApiLink name="lv_font_info_t" />. The weight value follows CSS conventions (1-2000,
typical: 100-900 where 400 = normal, 700 = bold). If `weight` is 0 (default),
the weight is determined by the style: <ApiLink name="LV_FREETYPE_FONT_STYLE_BOLD" />
maps to 700, otherwise 400. If the font is not a variable weight font, the weight
setting will have no effect.
The FreeType extension also supports colored bitmap glyphs such as emojis. Note
that only bitmaps are supported at this time. Colored vector graphics cannot be
rendered. An example on how to draw a colored bitmap glyph is shown below.
+1
View File
@@ -178,6 +178,7 @@ struct _lv_font_info_t {
uint32_t size; /**< Font size in pixel*/
uint32_t render_mode; /**< Font rendering mode, see `lv_freetype_font_render_mode_t`*/
uint32_t style; /**< Font style, see `lv_freetype_font_style_t`*/
int32_t weight; /**< Font weight for variable fonts (range 1-2000, CSS-like values 100-900; 0 = default: 400 normal, 700 if bold style) */
lv_font_kerning_t kerning; /**< Font kerning, see `lv_font_kerning_t`*/
};
+4 -2
View File
@@ -60,7 +60,7 @@ typedef enum {
LV_FREETYPE_OUTLINE_CONIC_TO,
} lv_freetype_outline_type_t;
/* Only path string is required */
/** Only path string is required */
typedef const char lv_freetype_font_src_t;
LV_ATTRIBUTE_EXTERN_DATA extern const lv_font_class_t lv_freetype_font_class;
@@ -101,7 +101,9 @@ lv_font_t * lv_freetype_font_create_with_info(const lv_font_info_t * font_info);
* @param style font style(see lv_freetype_font_style_t for details).
* @return Created font, or NULL on failure.
*/
lv_font_t * lv_freetype_font_create(const char * pathname, lv_freetype_font_render_mode_t render_mode, uint32_t size,
lv_font_t * lv_freetype_font_create(const char * pathname,
lv_freetype_font_render_mode_t render_mode,
uint32_t size,
lv_freetype_font_style_t style);
/**
+1 -3
View File
@@ -390,9 +390,7 @@ static void freetype_outline_event_cb(lv_event_t * e)
lv_vector_path_quad_to(path, &ctrl_pnt1, &pnt);
break;
case LV_FREETYPE_OUTLINE_END:
/* It's not necessary to close the path and
* border start is handled above
*/
/* It's not necessary to close the path */
break;
}
break;
+1
View File
@@ -185,6 +185,7 @@ bool lv_font_info_is_equal(const lv_font_info_t * ft_info_1, const lv_font_info_
bool is_equal = (ft_info_1->size == ft_info_2->size
&& ft_info_1->style == ft_info_2->style
&& ft_info_1->render_mode == ft_info_2->render_mode
&& ft_info_1->weight == ft_info_2->weight
&& ft_info_1->kerning == ft_info_2->kerning
&& lv_strcmp(ft_info_1->name, ft_info_2->name) == 0);
+128
View File
@@ -21,6 +21,20 @@
#define ft_ctx LV_GLOBAL_DEFAULT()->ft_context
#define LV_FREETYPE_OUTLINE_REF_SIZE_DEF 128
/* Temporary stack buffer size for variable-font axes.
* Most fonts have only a few axes; use heap only when needed. */
#define LV_FREETYPE_MAX_STACK_AXES 8
/* Clamp for requested variable font weight before converting to 16.16.
* Fonts commonly use CSS-like 100..900, but some support extended ranges.
* Keeping this bounded avoids unreasonable inputs and keeps conversion safe. */
#define LV_FREETYPE_VAR_WEIGHT_MIN 1
#define LV_FREETYPE_VAR_WEIGHT_MAX 2000
#define LV_FREETYPE_VAR_WEIGHT_DEFAULT 0 /**< 0 = resolved from style: NORMAL(400) or BOLD(700) */
#define LV_FREETYPE_VAR_WEIGHT_NORMAL 400
#define LV_FREETYPE_VAR_WEIGHT_BOLD 700
/**< This value is from the FreeType's function `FT_GlyphSlot_Oblique` in `ftsynth.c` */
#define LV_FREETYPE_OBLIQUE_SLANT_DEF 0x0366A
@@ -51,6 +65,7 @@ static bool cache_node_cache_create_cb(lv_freetype_cache_node_t * node, void * u
static void cache_node_cache_free_cb(lv_freetype_cache_node_t * node, void * user_data);
static lv_cache_compare_res_t cache_node_cache_compare_cb(const lv_freetype_cache_node_t * lhs,
const lv_freetype_cache_node_t * rhs);
static bool lv_freetype_set_weight_if_variable(FT_Face face, int weight);
static lv_font_t * freetype_font_create_cb(const lv_font_info_t * info, const void * src);
static void freetype_font_delete_cb(lv_font_t * font);
@@ -135,6 +150,7 @@ void lv_freetype_init_font_info(lv_font_info_t * font_info)
font_info->class_p = &lv_freetype_font_class;
font_info->render_mode = LV_FREETYPE_FONT_RENDER_MODE_BITMAP;
font_info->style = LV_FREETYPE_FONT_STYLE_NORMAL;
font_info->weight = LV_FREETYPE_VAR_WEIGHT_DEFAULT;
font_info->kerning = LV_FONT_KERNING_NONE;
}
@@ -160,6 +176,9 @@ lv_font_t * lv_freetype_font_create_with_info(const lv_font_info_t * font_info)
.pathname = lv_freetype_req_face_id(ctx, pathname),
.style = font_info->style,
.render_mode = font_info->render_mode,
/* Normalize weight: DEFAULT(0) resolves to BOLD(700) or NORMAL(400) to avoid duplicate cache nodes */
.weight = font_info->weight > LV_FREETYPE_VAR_WEIGHT_DEFAULT ? font_info->weight
: ((font_info->style & LV_FREETYPE_FONT_STYLE_BOLD) ? LV_FREETYPE_VAR_WEIGHT_BOLD : LV_FREETYPE_VAR_WEIGHT_NORMAL),
};
bool cache_hitting = true;
@@ -442,8 +461,113 @@ static bool cache_node_cache_create_cb(lv_freetype_cache_node_t * node, void * u
lv_freetype_glyph_l1_init(node);
#endif
/* Apply variable font weight */
if(!lv_freetype_set_weight_if_variable(face, node->weight)) {
/* Only log when an explicit weight was requested, to avoid spamming logs
* for default weights on non-variable fonts. */
if(node->weight != LV_FREETYPE_VAR_WEIGHT_NORMAL && node->weight != LV_FREETYPE_VAR_WEIGHT_BOLD) {
LV_LOG_INFO("font '%s' does not support variable weight, requested weight %d ignored", node->pathname, node->weight);
}
}
return true;
}
static void lv_freetype_done_mm_var(FT_MM_Var * mm_var)
{
lv_freetype_context_t * ctx = lv_freetype_get_context();
LV_ASSERT_NULL(ctx);
LV_ASSERT_NULL(ctx->library);
FT_Error err = FT_Done_MM_Var(ctx->library, mm_var);
if(err != 0) {
FT_ERROR_MSG("FT_Done_MM_Var", err);
}
}
/* If the font is variable, set the 'wght' axis to the requested value. Returns true if applied. */
static bool lv_freetype_set_weight_if_variable(FT_Face face, int weight)
{
if(!face)
return false;
if(!FT_HAS_MULTIPLE_MASTERS(face))
return false;
FT_MM_Var * mm_var = NULL;
FT_Error mm_err = FT_Get_MM_Var(face, &mm_var);
if(mm_err != 0 || !mm_var || mm_var->num_axis == 0) {
if(mm_err != 0) {
FT_ERROR_MSG("FT_Get_MM_Var", mm_err);
}
else if(mm_var) {
/* FT_Get_MM_Var succeeded but no usable axes; free to avoid leak */
lv_freetype_done_mm_var(mm_var);
}
return false;
}
FT_UInt axis_count = mm_var->num_axis;
FT_Fixed coords_stack[LV_FREETYPE_MAX_STACK_AXES];
FT_Fixed * coords = coords_stack;
const bool use_heap = axis_count > LV_FREETYPE_MAX_STACK_AXES;
bool applied = false;
FT_ULong wght_tag;
int wght_index;
FT_UInt i;
if(use_heap) {
coords = (FT_Fixed *)lv_malloc(axis_count * sizeof(FT_Fixed));
LV_ASSERT_MALLOC(coords);
if(!coords) {
LV_LOG_ERROR("failed to allocate memory for %u font axes", axis_count);
lv_freetype_done_mm_var(mm_var);
return false;
}
}
FT_Error coord_err = FT_Get_Var_Design_Coordinates(face, axis_count, coords);
if(coord_err != 0) {
FT_ERROR_MSG("FT_Get_Var_Design_Coordinates", coord_err);
goto cleanup;
}
wght_tag = FT_MAKE_TAG('w', 'g', 'h', 't');
wght_index = -1;
for(i = 0; i < axis_count; i++) {
if(mm_var->axis[i].tag == wght_tag) {
wght_index = (int)i;
break;
}
}
if(wght_index >= 0) {
FT_Fixed min_v = mm_var->axis[wght_index].minimum;
FT_Fixed max_v = mm_var->axis[wght_index].maximum;
FT_Fixed target;
FT_Error set_err;
weight = LV_CLAMP(LV_FREETYPE_VAR_WEIGHT_MIN, weight, LV_FREETYPE_VAR_WEIGHT_MAX);
target = LV_CLAMP(min_v, FT_INT_TO_F16DOT16(weight), max_v);
coords[wght_index] = target;
set_err = FT_Set_Var_Design_Coordinates(face, axis_count, coords);
if(set_err != 0) {
FT_ERROR_MSG("FT_Set_Var_Design_Coordinates", set_err);
}
else {
applied = true;
}
}
cleanup:
if(use_heap) {
lv_free(coords);
}
lv_freetype_done_mm_var(mm_var);
return applied;
}
static void cache_node_cache_free_cb(lv_freetype_cache_node_t * node, void * user_data)
{
#if LV_FREETYPE_CACHE_FT_GLYPH_L1
@@ -471,6 +595,9 @@ static lv_cache_compare_res_t cache_node_cache_compare_cb(const lv_freetype_cach
if(lhs->style != rhs->style) {
return lhs->style > rhs->style ? 1 : -1;
}
if(lhs->weight != rhs->weight) {
return lhs->weight > rhs->weight ? 1 : -1;
}
int32_t cmp_res = lv_strcmp(lhs->pathname, rhs->pathname);
if(cmp_res != 0) {
@@ -482,6 +609,7 @@ static lv_cache_compare_res_t cache_node_cache_compare_cb(const lv_freetype_cach
static lv_font_t * freetype_font_create_cb(const lv_font_info_t * info, const void * src)
{
LV_ASSERT_NULL(info);
lv_font_info_t font_info = *info;
font_info.name = src;
return lv_freetype_font_create_with_info(&font_info);
+1 -1
View File
@@ -130,7 +130,7 @@ static bool freetype_glyph_outline_create_cb(lv_freetype_outline_node_t * node,
dsc->cache_node->face,
node->glyph_index,
dsc->cache_node->ref_size,
dsc->style & LV_FREETYPE_FONT_STYLE_BOLD ? 1 : 0);
(dsc->style & LV_FREETYPE_FONT_STYLE_BOLD) && !FT_HAS_MULTIPLE_MASTERS(dsc->cache_node->face) ? 1 : 0);
lv_mutex_unlock(&dsc->cache_node->face_lock);
if(!outline) {
+2
View File
@@ -27,6 +27,7 @@ extern "C" {
#include FT_IMAGE_H
#include FT_OUTLINE_H
#include FT_STROKER_H
#include FT_MULTIPLE_MASTERS_H
/*********************
* DEFINES
@@ -90,6 +91,7 @@ struct _lv_freetype_cache_node_t {
const char * pathname;
lv_freetype_font_style_t style;
lv_freetype_font_render_mode_t render_mode;
int32_t weight; /**< Variable font weight (range 1-2000, 0 = default; values are clamped) */
uint32_t ref_size; /**< Reference size for calculating outline glyph's real size.*/
Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 89 KiB

+81 -6
View File
@@ -61,6 +61,34 @@ static void test_freetype_with_render_mode(lv_freetype_font_render_mode_t render
LV_FREETYPE_FONT_STYLE_NORMAL);
TEST_ASSERT_NOT_NULL(font_normal_small);
lv_font_t * font_variable_default = lv_freetype_font_create(
"./src/test_files/fonts/Montserrat-VariableFont.ttf", render_mode, 24, LV_FREETYPE_FONT_STYLE_NORMAL);
TEST_ASSERT_NOT_NULL(font_variable_default);
lv_font_t * font_variable_bold = lv_freetype_font_create(
"./src/test_files/fonts/Montserrat-VariableFont.ttf", render_mode, 24, LV_FREETYPE_FONT_STYLE_BOLD);
TEST_ASSERT_NOT_NULL(font_variable_bold);
/* Use lv_font_info_t to set explicit weight */
lv_font_info_t info_thin;
lv_freetype_init_font_info(&info_thin);
info_thin.name = "./src/test_files/fonts/Montserrat-VariableFont.ttf";
info_thin.render_mode = render_mode;
info_thin.size = 24;
info_thin.style = LV_FREETYPE_FONT_STYLE_ITALIC;
info_thin.weight = 300;
lv_font_t * font_variable_thin = lv_freetype_font_create_with_info(&info_thin);
TEST_ASSERT_NOT_NULL(font_variable_thin);
lv_font_info_t info_thick;
lv_freetype_init_font_info(&info_thick);
info_thick.name = "./src/test_files/fonts/Montserrat-VariableFont.ttf";
info_thick.render_mode = render_mode;
info_thick.size = 24;
info_thick.weight = 1000;
lv_font_t * font_variable_thick = lv_freetype_font_create_with_info(&info_thick);
TEST_ASSERT_NOT_NULL(font_variable_thick);
/* Emoji is only supported in bitmap mode */
lv_font_t * font_emoji = lv_freetype_font_create("../examples/libs/freetype/NotoColorEmoji-32.subset.ttf",
LV_FREETYPE_FONT_RENDER_MODE_BITMAP,
@@ -68,8 +96,7 @@ static void test_freetype_with_render_mode(lv_freetype_font_render_mode_t render
LV_FREETYPE_FONT_STYLE_NORMAL);
TEST_ASSERT_NOT_NULL(font_emoji);
lv_font_t * font_path_error = lv_freetype_font_create("ERROR_PATH", render_mode, 24,
LV_FREETYPE_FONT_STYLE_NORMAL);
lv_font_t * font_path_error = lv_freetype_font_create("ERROR_PATH", render_mode, 24, LV_FREETYPE_FONT_STYLE_NORMAL);
TEST_ASSERT_NULL(font_path_error);
font_path_error = lv_freetype_font_create("", render_mode, 24, LV_FREETYPE_FONT_STYLE_NORMAL);
@@ -101,30 +128,70 @@ static void test_freetype_with_render_mode(lv_freetype_font_render_mode_t render
static lv_style_t style_normal_small;
lv_style_init(&style_normal_small);
lv_style_set_text_font(&style_normal_small, font_normal_small);
lv_style_set_text_align(&style_normal_small, LV_TEXT_ALIGN_CENTER);
static lv_style_t style_variable_default;
lv_style_init(&style_variable_default);
lv_style_set_text_font(&style_variable_default, font_variable_default);
lv_style_set_text_align(&style_variable_default, LV_TEXT_ALIGN_CENTER);
static lv_style_t style_variable_bold;
lv_style_init(&style_variable_bold);
lv_style_set_text_font(&style_variable_bold, font_variable_bold);
lv_style_set_text_align(&style_variable_bold, LV_TEXT_ALIGN_CENTER);
static lv_style_t style_variable_thin;
lv_style_init(&style_variable_thin);
lv_style_set_text_font(&style_variable_thin, font_variable_thin);
lv_style_set_text_align(&style_variable_thin, LV_TEXT_ALIGN_CENTER);
static lv_style_t style_variable_thick;
lv_style_init(&style_variable_thick);
lv_style_set_text_font(&style_variable_thick, font_variable_thick);
lv_style_set_text_align(&style_variable_thick, LV_TEXT_ALIGN_CENTER);
static lv_style_t style_normal_emoji;
lv_style_init(&style_normal_emoji);
lv_style_set_text_font(&style_normal_emoji, font_emoji);
lv_style_set_text_align(&style_normal_emoji, LV_TEXT_ALIGN_CENTER);
lv_obj_set_flex_flow(lv_screen_active(), LV_FLEX_FLOW_COLUMN);
/*Create a label with the new style*/
lv_obj_t * label0 = lv_label_create(lv_screen_active());
lv_obj_add_style(label0, &style_italic, 0);
lv_obj_set_width(label0, lv_obj_get_width(lv_screen_active()) - 20);
lv_label_set_text(label0, UNIVERSAL_DECLARATION_OF_HUMAN_RIGHTS_CN);
lv_obj_align(label0, LV_ALIGN_TOP_MID, 0, 10);
lv_obj_t * label1 = lv_label_create(lv_screen_active());
lv_obj_add_style(label1, &style_normal_with_outline, 0);
lv_obj_set_width(label1, lv_obj_get_width(lv_screen_active()) - 20);
lv_label_set_text(label1, UNIVERSAL_DECLARATION_OF_HUMAN_RIGHTS_EN);
lv_obj_align_to(label1, label0, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
lv_obj_t * label2 = lv_label_create(lv_screen_active());
lv_obj_add_style(label2, &style_normal_small, 0);
lv_obj_set_width(label2, lv_obj_get_width(lv_screen_active()) - 20);
lv_label_set_text(label2, UNIVERSAL_DECLARATION_OF_HUMAN_RIGHTS_JP);
lv_obj_align_to(label2, label1, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
lv_obj_t * label3 = lv_label_create(lv_screen_active());
lv_obj_add_style(label3, &style_variable_default, 0);
lv_obj_set_width(label3, lv_obj_get_width(lv_screen_active()) - 20);
lv_label_set_text(label3, "Variable Font Default Weight");
lv_obj_t * label4 = lv_label_create(lv_screen_active());
lv_obj_add_style(label4, &style_variable_bold, 0);
lv_obj_set_width(label4, lv_obj_get_width(lv_screen_active()) - 20);
lv_label_set_text(label4, "Variable Font Bold Weight");
lv_obj_t * label5 = lv_label_create(lv_screen_active());
lv_obj_add_style(label5, &style_variable_thin, 0);
lv_obj_set_width(label5, lv_obj_get_width(lv_screen_active()) - 20);
lv_label_set_text(label5, "Variable Font 300 Weight");
lv_obj_t * label6 = lv_label_create(lv_screen_active());
lv_obj_add_style(label6, &style_variable_thick, 0);
lv_obj_set_width(label6, lv_obj_get_width(lv_screen_active()) - 20);
lv_label_set_text(label6, "Variable Font 1000 Weight");
/* test emoji rendering
* emoji font does not contain normal characters, use fallback to render them */
@@ -134,19 +201,27 @@ static void test_freetype_with_render_mode(lv_freetype_font_render_mode_t render
lv_obj_add_style(label_emoji, &style_normal_emoji, 0);
lv_obj_set_width(label_emoji, lv_obj_get_width(lv_screen_active()) - 20);
lv_label_set_text(label_emoji, "FreeType Emoji test: 😀");
lv_obj_align_to(label_emoji, label2, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
TEST_ASSERT_EQUAL_SCREENSHOT(screenshot_name);
lv_obj_set_layout(lv_screen_active(), LV_LAYOUT_NONE);
lv_obj_clean(lv_screen_active());
lv_style_reset(&style_italic);
lv_style_reset(&style_normal_with_outline);
lv_style_reset(&style_normal_small);
lv_style_reset(&style_variable_default);
lv_style_reset(&style_variable_bold);
lv_style_reset(&style_variable_thin);
lv_style_reset(&style_variable_thick);
lv_style_reset(&style_normal_emoji);
lv_freetype_font_delete(font_italic);
lv_freetype_font_delete(font_normal);
lv_freetype_font_delete(font_normal_small);
lv_freetype_font_delete(font_emoji);
lv_freetype_font_delete(font_variable_default);
lv_freetype_font_delete(font_variable_bold);
lv_freetype_font_delete(font_variable_thin);
lv_freetype_font_delete(font_variable_thick);
}
void test_freetype_render_bitmap(void)