C++版的sfntly——字体子集化工具

作者: veaxen 分类: sfntly 发布时间: 2018-11-10 20:55

前言

前一段要做一个字体子集化服务,就找到了google提供的开源库sfntly,其中提供的sample就实现了字体文件的裁剪子集化问题。sfntly有C++和Java两个版本,其中Java版本提供的示例(sfnttool)比较完善,对字体的提取生成的字体库非常小,少量字符的情况下只有几K,而C++版本的同样字符的情况下至少要几百K,甚至超过1M。原因是Java版本生成的字体文件对cmap、glyph、PostScript等表都进行了重新的编排,只保留了要提取的字符的表信息,而C++版本是把不要的字符的glyph信息移除,字体文件的结构和其他变信息都没有改变,这样就没有实现最小提取了。

sfntly库的主要作用是对字体文件的完整解析,并生成各种tables,讲道理C++版本也可以做到和Java版本的同样的字体提取裁剪效果。

修改完善字体提取工具

参考Java版本的逻辑(此前没有接触过Java),我对C++版本的进行修改和完善,使其提取生成的字体文件达到Java版本的效果。

通过对C++版本的源码进行分析,发现subtly的实现逻辑比较利于修改,所以基于subtly,我们来实现一款字体裁剪工具。

glyph提取及重新编排

原本的逻辑是保留glyph的结构顺序,挖去不要的glyph信息,但是保留其位置,我们现在需要对glyph进行重新编排:

bool FontAssembler::AssembleGlyphAndLocaTables() {
    // 只截取部分修改和关注的代码

    int32_t new_glyphid = 0;
    for (GlyphIdSet::iterator it = resolved_glyph_ids->begin(),
         e = resolved_glyph_ids->end(); it != e; ++it) {
        // Get the glyph for this resolved_glyph_id.
        int32_t resolved_glyph_id = it->glyph_id();
        old_to_new_glyphid_[resolved_glyph_id] = new_glyphid++;         //建立旧->新的glyphid关系
        new_to_old_glyphid_.push_back(resolved_glyph_id);                   //建立新->旧的glyphid关系
        // ....其他代码....
    }
    //重新建立loca表,移除不要的loca表信息
    IntegerList loca_list;
    glyph_table_builder->GenerateLocaList(&loca_list);
    loca_table_builder->SetLocaList(&loca_list);

    // 重建maxp
    Ptr<ReadableFontData> rFontData = font_info_->GetTable(font_info_->fonts()->begin()->first, Tag::maxp)->ReadFontData();
    font_builder_->NewTableBuilder(Tag::maxp, rFontData);
    MaximumProfileTableBuilderPtr maxpBuilder =
        down_cast<MaximumProfileTable::Builder*>(font_builder_->GetTableBuilder(Tag::maxp));
    maxpBuilder->SetNumGlyphs(loca_table_builder->NumGlyphs());
    return true;
}

这里需要修改一下loca_table_builder->NumGlyphs(),参照java版本:

int32_t LocaTable::Builder::NumGlyphs() {
    //return LastGlyphIndex() - 1;
    return LastGlyphIndex() + 1;
}

cmap重新编排

bool FontAssembler::AssembleCMapTable() {
    //.......

    for (CharacterMap::iterator it = chars_to_glyph_ids->begin(),
         e = chars_to_glyph_ids->end(); it != e; ++it) {
        int32_t character = it->first;
        if (character != last_chararacter + 1) {  // new segment
            if (current_segment != NULL) {
                current_segment->set_end_count(last_chararacter);
                segment_list->push_back(current_segment);
            }
            current_segment =
                new CMapTable::CMapFormat4::Builder::
                Segment(character, -1, 0, last_offset);
        }
        int32_t old_glyphid = it->second.glyph_id();
        new_glyph_id_array->push_back(old_to_new_glyphid_[old_glyphid]);//新cmap->glyph的对应关系
        last_offset += DataSize::kSHORT;
        last_chararacter = character;
    }

     // 防止访问空指针,这里有个小bug,就是当current_segment为NULL,没有cmap信息
    if (current_segment != NULL){  
        // The last segment is still open.
        current_segment->set_end_count(last_chararacter);
        segment_list->push_back(current_segment);
    }
    // Updating the id_range_offset for every segment.
    for (int32_t i = 0, num_segs = segment_list->size(); i < num_segs; ++i) {
        Ptr<CMapTable::CMapFormat4::Builder::Segment> segment = segment_list->at(i);
        segment->set_id_range_offset(segment->id_range_offset()
                                     + (num_segs - i + 1) * DataSize::kSHORT);
    }
    // Adding the final, required segment.
    current_segment =
        new CMapTable::CMapFormat4::Builder::Segment(0xffff, 0xffff, 1, 0);
    new_glyph_id_array->push_back(0);                                           //边界处理
    segment_list->push_back(current_segment);
    // Writing the segments and glyph id array to the CMap
    cmap_builder->set_segments(segment_list);
    cmap_builder->set_glyph_id_array(new_glyph_id_array);
    delete segment_list;
    delete new_glyph_id_array;
    return true;
}

重建HorizontalMetricsTable

bool FontAssembler::AssembleHorizontalMetricsTable() {
    HorizontalMetricsTablePtr origMetrics =
        down_cast<HorizontalMetricsTable*>(font_info_->GetTable(0, Tag::hmtx));
    if (origMetrics == NULL) {
        return false;
    }

    std::vector<LongHorMetric> metrics;
    for (size_t i = 0; i < new_to_old_glyphid_.size(); ++i) {
        int32_t origGlyphId = new_to_old_glyphid_[i];
        int32_t advanceWidth = origMetrics->AdvanceWidth(origGlyphId);
        int32_t lsb = origMetrics->LeftSideBearing(origGlyphId);
        metrics.push_back(LongHorMetric{advanceWidth, lsb});
    }

    int32_t lastWidth = metrics.back().advanceWidth;
    int32_t numberOfHMetrics = (int32_t)metrics.size();
    while (numberOfHMetrics > 1 && metrics[numberOfHMetrics-2].advanceWidth == lastWidth) {
        numberOfHMetrics--;
    }
    int32_t size = 4 * numberOfHMetrics + 2 * ((int32_t)metrics.size() - numberOfHMetrics);
    WritableFontDataPtr data;
    data.Attach(WritableFontData::CreateWritableFontData(size));
    int32_t index = 0;
    int32_t advanceWidthMax = 0;
    for (int32_t i=0; i < numberOfHMetrics; ++i) {
        int32_t adw = metrics[i].advanceWidth;
        advanceWidthMax = std::max(adw, advanceWidthMax);
        index += data->WriteUShort(index, adw);
        index += data->WriteShort(index, metrics[i].lsb);
    }
    int32_t nMetric = (int32_t)metrics.size();
    for (int32_t j = numberOfHMetrics; j < nMetric; ++j) {
        index += data->WriteShort(index, metrics[j].lsb);
    }
    font_builder_->NewTableBuilder(Tag::hmtx, data);
    font_builder_->NewTableBuilder(Tag::hhea, font_info_->GetTable(0, Tag::hhea)->ReadFontData());
    HorizontalHeaderTableBuilderPtr hheaBuilder =
        down_cast<HorizontalHeaderTable::Builder*>(font_builder_->GetTableBuilder(Tag::hhea));
    hheaBuilder->SetNumberOfHMetrics(numberOfHMetrics);
    hheaBuilder->SetAdvanceWidthMax(advanceWidthMax);

    return true;
}

重建PostScriptTabble

C++版本的sfntly没有解析出postScript table,所以参照Java版本,我实现了对这个表的解析,然后对post table的进行裁剪,重新生成一个新的post table,但是对于ttf文件,post table不是必须的,可以直接移除掉,这样生成的字体文件会更小,但是这样生成的字体文件兼容性没那么好,比如我在谷歌浏览器中就不能使用移除post table的字体文件渲染出字体。


这里我将修改后的代码放在了github,来源于开源,回馈于开源:https://github.com/veaxen/fntsub

参考:
C++版本的sfntly库使用示例(一)
C++版本的sfntly库使用示例(二)

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.