作者 ---- 小老虎(Numberwolf)
Github: https://github.com/numberwolf
多媒体/音视频/图形/播放器技术/跨平台技术架构
O 背景
我们常常会在 图片/视频等 二维多媒体素材中看到有文字的存在,同时也会在视频剪辑工具中给视频嵌入文字字幕操作。那么这些都是如何实现的呢?
—— 看起来很简单的东西,往往比想象中要复杂的多。
1 字体渲染基础原理
我们常见的字体嵌入图像中,往往需要准备一个TTF等格式的字体文件。(如果不选择往往是默认操作系统的字体)
而这些被设计好的字体,最终是以点阵的方式 存储于TTF字体文件中。也就是说,每一个字符都会被存入其中,如果当前字体设计不存在的文字,那么最终可能会以乱码的形式存在。
FreeType初始化
int frontSize = 36; // pixel
FT_Library ft;
FT_Face face;
if (FT_Init_FreeType(&ft)) {
printf("ERROR::FREETYPE: FT_New_Memory_Face Could not init FreeType Library\n");
}
if (FT_New_Memory_Face(ft, (const FT_Byte *)_fontData, _dataSize, 0, &face)) {
printf("ERROR::FREETYPE: FT_New_Memory_Face Failed to load font\n");
}
FT_Set_Pixel_Sizes(face, 0, frontSize);
FT_Face 为索引串讲数据结构
FT_Face类
一个外观对象对应单个字体外观,即一个特定风格的特定外观类型,例如Arial和Arial Italic是两个不同的外观。
一个外观对象通常使用FT_New_Face()来创建,这个函数接受如下参数:一个FT_Library句柄,一个表示字体文件的C文件路径名,一个决定从文件中装载外观的索引(一个文件中可能有不同的外观),和FT_Face句柄的地址,它返回一个错误码。
FT_Error FT_New_Face( FT_Library library,
const char* filepathname,
FT_Long face_index,
FT_Face* face);
函数调用成功,返回0,face参数将被设置成一个非NULL值。
外观对象包含一些用来描述全局字体数据的属性,可以被客户程序直接访问。例如外观中字形的数量、外观家族的名称、风格名称、EM大小等,详见FT_FaceRec定义。
typedef struct FT_FaceRec_
{
FT_Long num_faces;
FT_Long face_index;
FT_Long face_flags;
FT_Long style_flags;
FT_Long num_glyphs; // font total count
FT_String* family_name;
FT_String* style_name;
FT_Int num_fixed_sizes;
FT_Bitmap_Size* available_sizes;
FT_Int num_charmaps;
FT_CharMap* charmaps;
FT_Generic generic;
/*# The following member variables (down to `underline_thickness`) */
/*# are only relevant to scalable outlines; cf. @FT_Bitmap_Size */
/*# for bitmap fonts. */
FT_BBox bbox;
FT_UShort units_per_EM;
FT_Short ascender;
FT_Short descender;
FT_Short height;
FT_Short max_advance_width;
FT_Short max_advance_height;
FT_Short underline_position;
FT_Short underline_thickness;
// 字形槽的目的是提供一个地方,可以很容易地一个个地装入字形映象,而不管它的格式(位图、向量轮廓或其他)
FT_GlyphSlot glyph; // glyph link list
// 每个FT_Face对象都有一个或多个FT_Size对象,一个尺寸对象用来存放指定字符宽度和高度的特定数据,每个新创建的外观对象有一个尺寸,可以通过face->size直接访问
FT_Size size;
...
} FT_FaceRec
可以观察到 FT_GlyphSlotRec
是以一个 链表的形式存储的数据
typedef struct FT_GlyphSlotRec_
{
FT_Library library;
FT_Face face;
FT_GlyphSlot next; // linklist node 字行槽是个链表的数据结构
FT_UInt glyph_index; /* new in 2.10; was reserved previously */
FT_Generic generic;
...
} FT_GlyphSlotRec
每个glyph字符在字槽对应一个索引
glyph_index
常使用方法来确定某个字符在 glyph 链表是否存在,查找对应
glyph_index
wchar_t t = str[n]; // 确定字符
FT_UInt glyph_index;
glyph_index = FT_Get_Char_Index(face, t); // 确定字符槽索引
FT_Error error = FT_Load_Glyph( face, glyph_index, FT_LOAD_DEFAULT ); // 加载
if ( error ) continue;
error = FT_Render_Glyph( face->glyph, ft_render_mode_normal ); // 渲染
if ( error ) continue;
FT_CharMap类
FT_CharMap类型用来作为一个字符地图对象的句柄,一个字符图是一种表或字典,用来将字符码从某种编码转换成字体的字形索引。
单个外观可能包含若干字符图,每个对应一个指定的字符指令系统,例如Unicode、Apple Roman、Windows codepages等等。
每个FT_CharMap对象包含一个platform和encoding属性,用来标识它对应的字符指令系统。每个字体格式都提供它们自己的FT_CharMapRec的继承类型并实现它们。
一个face对象包含一个或多个字符表(charmap),字符表是用来转换字符码到字形索引的。例如,很多TrueType字体包含两个字符表,一个用来转换Unicode字符码到字形索引,另一个用来转换Apple Roman编码到字形索引。这样的字体既可以用在Windows(使用Unicode)和Macintosh(使用Apple Roman)。同时要注意,一个特定的字符表可能没有覆盖完字体里面的全部字形。
typedef struct FT_CharMapRec_
{
FT_Face face;
FT_Encoding encoding;
FT_UShort platform_id;
FT_UShort encoding_id;
} FT_CharMapRec;
具体Encoding可以参考下放枚举
当新建一个face对象时,它默认选择Unicode字符表。如果字体没包含Unicode字符表,FreeType会尝试在字形名的基础上模拟一个。注意,如果字形名是不标准的那么模拟的字符表有可能遗漏某些字形。对于某些字体,包括符号字体和旧的亚洲手写字体,Unicode模拟是不可能的。
typedef enum FT_Encoding_
{
FT_ENC_TAG( FT_ENCODING_NONE, 0, 0, 0, 0 ),
FT_ENC_TAG( FT_ENCODING_MS_SYMBOL, 's', 'y', 'm', 'b' ),
FT_ENC_TAG( FT_ENCODING_UNICODE, 'u', 'n', 'i', 'c' ),
FT_ENC_TAG( FT_ENCODING_SJIS, 's', 'j', 'i', 's' ),
FT_ENC_TAG( FT_ENCODING_PRC, 'g', 'b', ' ', ' ' ),
FT_ENC_TAG( FT_ENCODING_BIG5, 'b', 'i', 'g', '5' ),
FT_ENC_TAG( FT_ENCODING_WANSUNG, 'w', 'a', 'n', 's' ),
FT_ENC_TAG( FT_ENCODING_JOHAB, 'j', 'o', 'h', 'a' ),
/* for backward compatibility */
FT_ENCODING_GB2312 = FT_ENCODING_PRC,
FT_ENCODING_MS_SJIS = FT_ENCODING_SJIS,
FT_ENCODING_MS_GB2312 = FT_ENCODING_PRC,
FT_ENCODING_MS_BIG5 = FT_ENCODING_BIG5,
FT_ENCODING_MS_WANSUNG = FT_ENCODING_WANSUNG,
FT_ENCODING_MS_JOHAB = FT_ENCODING_JOHAB,
FT_ENC_TAG( FT_ENCODING_ADOBE_STANDARD, 'A', 'D', 'O', 'B' ),
FT_ENC_TAG( FT_ENCODING_ADOBE_EXPERT, 'A', 'D', 'B', 'E' ),
FT_ENC_TAG( FT_ENCODING_ADOBE_CUSTOM, 'A', 'D', 'B', 'C' ),
FT_ENC_TAG( FT_ENCODING_ADOBE_LATIN_1, 'l', 'a', 't', '1' ),
FT_ENC_TAG( FT_ENCODING_OLD_LATIN_2, 'l', 'a', 't', '2' ),
FT_ENC_TAG( FT_ENCODING_APPLE_ROMAN, 'a', 'r', 'm', 'n' )
} FT_Encoding;
初始化Face
一个外观对象通常使用FT_New_Face()来创建,这个函数接受如下参数:一个FT_Library句柄,一个表示字体文件的C文件路径名,一个决定从文件中装载外观的索引(一个文件中可能有不同的外观),和FT_Face句柄的地址,它返回一个错误码。
FT_Library library, // 这个类型对应一个库的单一实例句柄 FT_Init_FreeType 新建
FT_EXPORT_DEF( FT_Error )
FT_New_Memory_Face( FT_Library library,
const FT_Byte* file_base, // 内存读入二进制TTF数据
FT_Long file_size,
FT_Long face_index,
FT_Face *aface )
{
FT_Open_Args args;
/* test for valid `library' and `face' delayed to `FT_Open_Face' */
if ( !file_base )
return FT_THROW( Invalid_Argument );
/*
#define FT_OPEN_MEMORY 0x1 // 内存
#define FT_OPEN_STREAM 0x2
#define FT_OPEN_PATHNAME 0x4 // 文件打开
#define FT_OPEN_DRIVER 0x8
#define FT_OPEN_PARAMS 0x10
*/
args.flags = FT_OPEN_MEMORY;
args.memory_base = file_base; // 指针指向 ttf 内存数据
args.memory_size = file_size;
args.stream = NULL;
// 调用内部方法
return ft_open_face_internal( library, &args, face_index, aface, 1 );
}
内部方法
static FT_Error
ft_open_face_internal( FT_Library library,
const FT_Open_Args* args,
FT_Long face_index, // 0
FT_Face *aface,
FT_Bool test_mac_fonts ) // 1
{
...
// #define FT_OPEN_DRIVER 0x8
// 打开加在 FT_Face
error = open_face( driver, &stream, external_stream, face_index,
num_params, params, &face );
// 如果成功,则将该Face加载faces_list 链表
if ( !error )
goto Success;
...
// goto
Success:
FT_TRACE4(( "FT_Open_Face: New face object, adding to list\n" ));
/* add the face object to its driver's list */
if ( FT_QNEW( node ) )
goto Fail;
node->data = face;
/* don't assume driver is the same as face->driver, so use */
/* face->driver instead. */
FT_List_Add( &face->driver->faces_list, node ); // 这里是一个链表存储
...
}
open_face方法 查找unicode charmap :
find_unicode_charmap
/**************************************************************************
*
* @Function:
* open_face
*
* @Description:
* This function does some work for FT_Open_Face().
*/
static FT_Error
open_face( FT_Driver driver,
FT_Stream *astream,
FT_Bool external_stream,
FT_Long face_index,
FT_Int num_params,
FT_Parameter* params,
FT_Face *aface )
{
...
if ( clazz->init_face )
error = clazz->init_face( *astream,
face,
(FT_Int)face_index,
num_params,
params );
*astream = face->stream; /* Stream may have been changed. */
if ( error )
goto Fail;
/* select Unicode charmap by default */
error2 = find_unicode_charmap( face ); // 查找unicode charmap
...
}
FT_Memory
所有内存管理操作通过基础层中3个特定例程完成,叫做
FT_Alloc
、FT_Realloc
、FT_Free
,每个函数需要一个FT_Memory
句柄作为它的第一个参数。它是一个用来描述当前内存池/管理器对象的指针。在库初始化时,在FT_Init_FreeType
中调用函数FT_New_Memory
创建一个内存管理器,这个函数位于ftsystem部件当中。
/**************************************************************************
*
* @struct:
* FT_MemoryRec
*
* @description:
* A structure used to describe a given memory manager to FreeType~2.
*
* @fields:
* user ::
* A generic typeless pointer for user data.
*
* alloc ::
* A pointer type to an allocation function.
*
* free ::
* A pointer type to an memory freeing function.
*
* realloc ::
* A pointer type to a reallocation function.
*
*/
struct FT_MemoryRec_
{
void* user;
FT_Alloc_Func alloc; // 分配
FT_Free_Func free; // 释放
FT_Realloc_Func realloc; // 再分配
};
FT_Done_Face 这是一个释放FT_Face
的函数, 内部FT_Memory
参与管理
可以看到
- 从driver的链表中寻查找对应FT_Face
之前是从
ft_open_face_internal
内部将Face添加进driver的链表(上文提到过)
-
从链表摘除Face
-
释放Face对应Node节点
-
FT_Memory 销毁Face
FT_EXPORT_DEF( FT_Error )
FT_Done_Face( FT_Face face )
{
FT_Error error;
FT_Driver driver;
FT_Memory memory;
FT_ListNode node;
...
driver = face->driver;
memory = driver->root.memory;
/* find face in driver's list */
node = FT_List_Find( &driver->faces_list, face ); // 从driver的链表中寻查找对应FT_Face
if ( node )
{
/* remove face object from the driver's list */
FT_List_Remove( &driver->faces_list, node ); // 从链表摘除Face
FT_FREE( node ); // 释放Face对应Node节点
/* now destroy the object proper */
destroy_face( memory, face, driver ); // FT_Memory 销毁Face
error = FT_Err_Ok;
}
...
FT_FREE
最终利用memory释放对应对象指针后,赋值为NULL
FT_BASE_DEF( void )
ft_mem_free( FT_Memory memory,
const void *P )
{
if ( P )
memory->free( memory, (void*)P ); // free
}
destroy_face
的释放过程
- 释放一些通用型数据,例如字形 API可能会给字符一个缓存
/* discard auto-hinting data */
if ( face->autohint.finalizer )
face->autohint.finalizer( face->autohint.data );
- 释放字符槽(是一个链表), 参考上文
FT_GlyphSlotRec
数据结构
while ( face->glyph )
FT_Done_GlyphSlot( face->glyph );
- 将FT_Size全部释放
FT_Size类
每个FT_Face对象都有一个或多个FT_Size对象,一个尺寸对象用来存放指定字符宽度和高度的特定数据,每个新创建的外观对象有一个尺寸,可以通过face->size直接访问。
尺寸对象的内容可以通过调用
FT_Set_Pixel_Sizes()
或FT_Set_Char_Size()
来改变。(参考上文FreeType初始化
)一个新的尺寸对象可以通过FT_New_Size()创建,通过FT_Done_Size()销毁,一般客户程序无需做这一步,它们通常可以使用每个FT_Face缺省提供的尺寸对象。
/* discard all sizes for this face */
FT_List_Finalize( &face->sizes_list,
(FT_List_Destructor)destroy_size,
memory,
driver );
face->size = NULL;
- 释放charmaps
/* discard charmaps */
destroy_charmaps( face, memory );
- 释放face 的format:字段
face->glyph->format
描述了字形槽中存储的字形图像的格式
/* finalize format-specific stuff */
if ( clazz->done_face )
clazz->done_face( face );
- 释放stream
字体文件总是通过FT_Stream对象读取,FT_StreamRec的定义位于公共文件<freetype/ftsystem.h>中,可以允许客户开发者提供自己的流实现。FT_New_Face()函数会自动根据他第二个参数,一个C路径名创建一个新的流对象。它通过调用由 ftsystem部件提供的FT_New_Stream()完成,后者时可替换的,在不同平台上,流的实现可能大不一样。
/* close the stream for this face if needed */
FT_Stream_Free(
face->stream,
( face->face_flags & FT_FACE_FLAG_EXTERNAL_STREAM ) != 0 );
face->stream = NULL;
- 释放
FT_FREE( face );
网友评论