项目中需要用到描边字体,原本以为是个简单的需求,可是绕了一圈发现awtk中并没有简单的设置去实现这个,向官方提了issue,得到的回答居然也是不懂,无奈之下,只好自己探究,写自定义插件。
听老板说之前的产品描边字体是用freetype实现的,正好自己最近在学linux开发板的操作,于是用AI写了个freetype写描边字体的代码,在开发板上实验了下,效果还行。本质思想就是提取字体的glyph位图,在垂直和水平方向各偏移一个像素画一遍黑色字体,形成3x3共9个绘制起点,最后在中心的绘制起点开始画一遍白色字体。
正好awtk有个font_get_glyph函数可以对标FT_Load_Char, 给出的文档中也有将字体转化为位图函数,免除了需要手动移植,适配freetype和兼容awtkAPI的麻烦。
static bitmap_t* bitmap_from_str(canvas_t* canvas, wchar_t* str, color_t tc) {
glyph_t g;
uint32_t x = 0;
uint32_t y = 0;
uint32_t i = 0;
uint32_t w = 0;
uint32_t h = 0;
uint32_t ox = 0;
font_vmetrics_t vm;
uint32_t* p = NULL;
uint32_t baseline = 0;
bitmap_t* bitmap = NULL;
uint32_t font_size = canvas->font_size;
font_manager_t* fm = canvas->font_manager;
font_t* font = font_manager_get_font(fm, canvas->font_name, font_size);
return_value_if_fail(font != NULL, NULL);
vm = font_get_vmetrics(font, font_size);
h = vm.ascent - vm.descent;
return_value_if_fail(h > 0, NULL);
baseline = vm.ascent;
for (i = 0; str[i]; i++) {
return_value_if_fail(font_get_glyph(font, str[i], font_size, &g) == RET_OK, NULL);
w += g.advance + 1;
}
bitmap = bitmap_create_ex(w, h, 0, BITMAP_FMT_RGBA8888);
return_value_if_fail(bitmap != NULL, NULL);
p = (uint32_t*)bitmap_lock_buffer_for_write(bitmap);
memset(p, 0x00, w * h * 4);
for (i = 0; str[i]; i++) {
return_value_if_fail(font_get_glyph(font, str[i], font_size, &g) == RET_OK, NULL);
for (y = 0; y < g.h; y++) {
for (x = 0; x < g.w; x++) {
int32_t dx = ox + g.x + x;
int32_t dy = baseline + g.y + y;
uint32_t* d = p + dy * w + dx;
const uint8_t* s = g.data + y * g.w + x;
tc.rgba.a = *s;
*d = tc.color;
}
}
ox += g.advance + 1;
}
bitmap_unlock_buffer(bitmap);
bitmap->flags |= BITMAP_FLAG_CHANGED;
return bitmap;
}
但是同样的逻辑搬到awtk上,效果就很不理想,过渡的区域经常绘制不整,再查了一圈,发现位图绘制到画布这种做法,字体位图从中心到边缘的过渡区域a值是逐渐下降的。
于是新加了个逻辑,用a值是不是255来区分是边缘还是字体,对于字体一律白色处理,边缘一律黑色处理,这下勉强能看,接近用freetype画的效果,但估计由于是通过字体的位图直接绘制的原因,清晰度很低,锯齿严重。
写到这里差不多忙活了一天,原本打算到此为止时,忽然又想到,awtk的canvas_draw_text_in_rect貌似是矢量绘制,没有上面的清晰度和锯齿问题,重复绘制的思想也可以拿到这里,于是再试了一次,效果立竿见影!没想到苦苦追寻一天,最后解决问题的只有寥寥几行代码。
虽然还有点小瑕疵,比如字号比较小的情况边框对比度不太均衡,但对于当前的需求也够用了。
static ret_t border_text_on_paint_self(widget_t* widget, canvas_t* c) {
border_text_t* border_text = BORDER_TEXT(widget);
style_t *style = widget->astyle;
int font_size = style_get_int(style, STYLE_ID_FONT_SIZE, 25);
const char *font_name = style_get_str(style, STYLE_ID_FONT_NAME, "default");
color_t text_color = color_init(255, 255, 255, 255);
color_t border_color = color_init(0, 0, 0, 255);
canvas_set_text_align(c, ALIGN_H_CENTER, ALIGN_V_MIDDLE);
canvas_set_font(c, font_name, font_size);
canvas_set_text_color(c, border_color);
for(int j = 0; j < 3; j++){
for(int i = 0; i < 3; i++){
rect_t r = rect_init(i, j, widget->w, widget->h);
canvas_draw_text_in_rect(c, widget->text.str, widget->text.size, &r);
}
}
canvas_set_text_color(c, text_color);
rect_t r = rect_init(1, 1, widget->w, widget->h);
canvas_draw_text_in_rect(c, widget->text.str, widget->text.size, &r);
return RET_OK;
}
自定义控件已开源,希望同样用这个平台开发的朋友不要再遭受相同的罪:
https://gitee.com/tracker647/awtk-widget-border-text
参考:
通义灵码
https://blog.codingnow.com/2013/09/edge_font.html
附原版freetype代码:
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/fb.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <wchar.h>
#include <sys/ioctl.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_GLYPH_H
int fd_fb;
struct fb_var_screeninfo var; /* Current var */
int screen_size;
unsigned char *fbmem;
unsigned int line_width;
unsigned int pixel_width;
/**********************************************************************
* 函数名称: lcd_put_pixel
* 功能描述: 在LCD指定位置上输出指定颜色(描点)
* 输入参数: x坐标,y坐标,颜色
* 输出参数: 无
* 返 回 值: 会
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/05/12 V1.0 zh(angenao) 创建
***********************************************************************/
void lcd_put_pixel(int x, int y, unsigned int color)
{
unsigned char *pen_8 = fbmem+y*line_width+x*pixel_width;
unsigned short *pen_16;
unsigned int *pen_32;
unsigned int red, green, blue;
pen_16 = (unsigned short *)pen_8;
pen_32 = (unsigned int *)pen_8;
switch (var.bits_per_pixel)
{
case 8:
{
*pen_8 = color;
break;
}
case 16:
{
/* 565 */
red = (color >> 16) & 0xff;
green = (color >> 8) & 0xff;
blue = (color >> 0) & 0xff;
color = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
*pen_16 = color;
break;
}
case 32:
{
*pen_32 = color;
break;
}
default:
{
printf("can't surport %dbpp\n", var.bits_per_pixel);
break;
}
}
}
void draw_bitmap(FT_Bitmap* bitmap, FT_Int x, FT_Int y, unsigned int color)
{
FT_Int i, j, p, q;
FT_Int x_max = x + bitmap->width;
FT_Int y_max = y + bitmap->rows;
for (j = y, q = 0; j < y_max; j++, q++) {
for (i = x, p = 0; i < x_max; i++, p++) {
if (i < 0 || j < 0 || i >= var.xres || j >= var.yres) continue;
if (bitmap->buffer[q * bitmap->width + p] != 0) {
lcd_put_pixel(i, j, color);
}
}
}
}
void draw_bitmap_with_border(FT_Bitmap* bitmap, FT_Int char_x, FT_Int char_y, int border_size, unsigned int text_color, unsigned int border_color)
{
// 绘制边框
for (int dx = -border_size; dx <= border_size; ++dx) {
for (int dy = -border_size; dy <= border_size; ++dy) {
if (dx == 0 && dy == 0) continue; // 跳过中心点
for (int j = 0; j < bitmap->rows; ++j) {
for (int i = 0; i < bitmap->width; ++i) {
if (bitmap->buffer[j * bitmap->width + i] != 0) {
int px = char_x + i + dx;
int py = char_y + j + dy;
if (px >= 0 && px < var.xres && py >= 0 && py < var.yres) {
lcd_put_pixel(px, py, border_color);
}
}
}
}
}
}
// 绘制文字
for (int j = 0; j < bitmap->rows; ++j) {
for (int i = 0; i < bitmap->width; ++i) {
if (bitmap->buffer[j * bitmap->width + i] != 0) {
int px = char_x + i;
int py = char_y + j;
if (px >= 0 && px < var.xres && py >= 0 && py < var.yres) {
lcd_put_pixel(px, py, text_color);
}
}
}
}
}
void draw_text_with_border(FT_Face face, const wchar_t* text, int x, int y, int border_size, unsigned int text_color, unsigned int border_color)
{
FT_GlyphSlot slot = face->glyph;
int pen_x = x; // 初始笔位置
int pen_y = y;
for (const wchar_t* p = text; *p; ++p)
{
if (FT_Load_Char(face, *p, FT_LOAD_RENDER))
{
continue; // 如果加载失败,跳过这个字符
}
// 计算当前字符的位置
int char_x = pen_x + slot->bitmap_left;
int char_y = pen_y - slot->bitmap_top;
// 绘制带有边框的字符
draw_bitmap_with_border(&slot->bitmap, char_x, char_y, border_size, text_color, border_color);
// 更新笔位置到下一个字符
pen_x += slot->advance.x >> 6; // 字符间距通常是 advance 的值除以 64
}
}
int main(int argc, char **argv)
{
wchar_t *chinese_str = L"Hello World!";
FT_Library library;
FT_Face face;
int error;
FT_Vector pen;
FT_GlyphSlot slot;
int font_size = 24;
const char *filepathname = NULL;
int lcd_x = 0, lcd_y = 0;
if (argc < 5)
{
printf("Usage : %s <font_file> [font_size] <lcd_x> <lcd_y>\n", argv[0]);
return -1;
}
if (argc == 5){
filepathname = argv[1];
font_size = strtoul(argv[2], NULL, 0);
lcd_x = strtoul(argv[3], NULL, 0);
lcd_y = strtoul(argv[4], NULL, 0);
}
fd_fb = open("/dev/fb0", O_RDWR);
if (fd_fb < 0)
{
printf("can't open /dev/fb0\n");
return -1;
}
if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
{
printf("can't get var\n");
return -1;
}
line_width = var.xres * var.bits_per_pixel / 8;
pixel_width = var.bits_per_pixel / 8;
screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
fbmem = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
if (fbmem == (unsigned char *)-1)
{
printf("can't mmap\n");
return -1;
}
/* 清屏: 全部设为白色 */
memset(fbmem, 0xcc, screen_size);
/* 显示矢量字体 */
error = FT_Init_FreeType( &library ); /* initialize library */
/* error handling omitted */
error = FT_New_Face( library, filepathname, 0, &face ); /* create face object */
/* error handling omitted */
slot = face->glyph;
FT_Set_Pixel_Sizes(face, font_size, 0);
/* 确定座标:
*/
//pen.x = 0;
//pen.y = 0;
/* set transformation */
//FT_Set_Transform( face, 0, &pen);
/* load glyph image into the slot (erase previous one) */
error = FT_Load_Char( face, chinese_str[0], FT_LOAD_RENDER );
if (error)
{
printf("FT_Load_Char error\n");
return -1;
}
// draw_bitmap( &slot->bitmap,
// var.xres/2,
// var.yres/2, 0x00000000);
// 使用白色字体和黑色边框
unsigned int white = 0x00FFFFFF; // 假设32位颜色模式下白色是0xFFFFFFFF
unsigned int black = 0x00000000; // 黑色是0x00000000
int border_size = 1; // 边框宽度
// 绘制带边框的文字
draw_text_with_border(face, chinese_str, lcd_x, lcd_y, border_size, white, black);
return 0;
}