C++版的sfntly——字体子集化工具
前言
前一段要做一个字体子集化服务,就找到了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
CCC
2019年2月13日 下午3:01
您好
我编译后会出现错误
error C2338: The C++ Standard doesn’t provide a hash for this type. C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\include\xstddef
我环境是VS2012
可以请问您使用的版本是多少吗?
CCC
2019年2月13日 下午3:28
您好
我编译后会出现错误error C2338: The C Standard doesn’t provide a hash for this type. C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\include\xstddef
环境是VS2012
可以请问您使用的版本是多少吗?
似乎是在font_assembler.cc
const static std::unordered_map<std::string, int32_t>* invertNameMap() {
static std::unordered_map<std::string, int32_t > nameMap;
for(int32_t i = 0; i < PostScriptTable::NUM_STANDARD_NAMES; ++i) {
nameMap[PostScriptTable::STANDARD_NAMES[i]] = i;
}
return &nameMap;
}
const std::unordered_map<std::string, int32_t >*
FontAssembler::INVERTED_STANDARD_NAMES = invertNameMap();
谢谢
veaxen
2019年2月19日 下午5:10
我这边是在Linux下编译的,没有支持windows,不过代码应该是可以在windows下编译的,需要启用C++11
fermi.wong
2019年12月9日 下午6:22
对于宋体之类有kern表没有解决啊,还是很大。