[Freetype]字体渲染FreeType源码解析总结

作者 ---- 小老虎(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_AllocFT_ReallocFT_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 );
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容