Posted at

OpenType フォントのカーニング情報をぶっこぬく

More than 3 years have passed since last update.

OpenType フォーマットのフォントファイルからカーニング情報を取り出したい事情があっていろいろ調べた作業ログ。


  • フォントファイルをいじるときにつかう定番ライブラリ freetype 2.6 では OpenType のカーニング情報がひきだせなかったので(フォントにもよると思うけど)、libotf (0.9.13) で OTF ファイルを解析して取り出す。

  • OTF のファイルフォーマットは Microsoft Typography - OpenType Specification にまとまってる。


  • The TrueType Font File OpenType は TrueType をベースにしてるので↑にないテーブル定義とかはこのへんも見る。

  • libotf の example に含まれてる otfdump コマンドを使うと、OTF ファイルの構造をツリー上にテキストでダンプしてくれるので↓、仕様書とくらべながら見ると理解しやすい。

(OTF

(OffsetTable
(sfnt-version 20308.21583)
(numTables 25)
(searchRange 256)
(enterSelector 4)
(rangeShift 384))
(Table 0 (tag "BASE" #x42415345)
(checkSum 6962C672) (offset #x00000430) (length: #x000001C8))
(Table 1 (tag "CFF " #x43464620)
(checkSum 0BDFCA60) (offset #x0034700C) (length: #x0080F295))
(Table 2 (tag "EBDT" #x45424454)
(checkSum E109E7DA) (offset #x00213B60) (length: #x001334AA))
(Table 3 (tag "EBLC" #x45424C43)
(checkSum 81D5AE36) (offset #x00002A58) (length: #x0001064C))
(Table 4 (tag "GPOS" #x47504F53)
(checkSum D6E4B8CC) (offset #x00036E04) (length: #x00015098))
(Table 5 (tag "GSUB" #x47535542)
...


libotf でカーニング情報を実際にとりだす

GPOS テーブルから取り出すのが正攻法っぽいのでそっからいく。(/Library/Fonts/ヒラギノ明朝 Pro W6.otf には kerx テーブルもあったけど、後方互換てきなこと?)


  1. Unicode から Glyph IDに変換。


  2. GPOS テーブルの ScriptList から処理対象 Glyph に対応する Script を選ぶ(←ここがよくわからんので、とりあえずリストのいちばん最初の Script にしとく。)

  3. Script のなかから対応する LangSys を選ぶ。(←これもよくわからんけど基本 1 個しかないのでそれで。)

  4. LangSys の下に、対応してる Feature の FeatureList 内における index を含んだリストがあるので、それをなめてく。


  5. kern feature の lookup table index から lookup table 取得。

  6. lookup のなかの sub table のなかの coverage 構造体のなかに対象の Glyph があればカーニング値がふくまれてる。

  7. coverage 内の glyph の index が pair set の index と対応してる。

  8. pair set 内の pair value record をだーっとみていって、second glyph がカーニングペアの id だったらそれが実際のカーニング値。

って、書いててわけわかんなくなってきたけど、コードに直すと↓

#include <iostream>

#include "otf.h"
#include <ft2build.h>
#include FT_FREETYPE_H

int glyph_contains(OTF_Coverage *coverage, OTF_GlyphID glyph_id) {
if (coverage->CoverageFormat == 1) {
for (int i = 0; i < coverage->Count; i++) {
if (coverage->table.GlyphArray[i] == glyph_id) {
return i;
}
}
} else if (coverage->CoverageFormat == 2) {
for (int i = 0; i < coverage->Count; i++) {
OTF_RangeRecord *range = coverage->table.RangeRecord + i;
for (int j = range->Start; j <= range->End; j++) {
if (j == glyph_id) {
return j;
}
}
}
}
// ふくまれてない
return -1;
}

OTF_PairValueRecord *get_pair_set(OTF_LookupSubTableGPOS *sub_table, OTF_GlyphID left, OTF_GlyphID right) {
// printf("Coverage Format: %d, Count: %d, left: %d, right: %d\n", sub_table->Coverage.CoverageFormat,
// sub_table->Coverage.Count, glyph_contains(&sub_table->Coverage, left), glyph_contains(&sub_table->Coverage,
// right));
int pair_set_index = glyph_contains(&sub_table->Coverage, left);
if (pair_set_index < 0) {
// この sub table には left の情報無い
return NULL;
}

OTF_PairSet *pair_set = &sub_table->u.pair1.PairSet[pair_set_index];
for (int i = 0; i < pair_set->PairValueCount; i++) {
if (pair_set->PairValueRecord[i].SecondGlyph == right) {
return &pair_set->PairValueRecord[i];
}
}

// right との情報無い
return NULL;
}

int get_kerning_value(OTF *otf, OTF_GlyphString *gstring) {
char name[5];
OTF_Tag kern = OTF_tag("kern");
// OTF_Tag palt = OTF_tag("palt");

// よくわからんので最初の Script
OTF_LangSys *lang_sys = &otf->gpos->ScriptList.Script[0].DefaultLangSys;
for (int i = 0; i < lang_sys->FeatureCount; i++) {
int featureIndex = lang_sys->FeatureIndex[i];
OTF_Feature *feature = &otf->gpos->FeatureList.Feature[featureIndex];
OTF_tag_name(feature->FeatureTag, name);
// printf("%d: %d, %s\n", i, featureIndex, name);
if (feature->FeatureTag != kern) continue;

// LookupListIndex ながさ 1 なので最初のやつ。複数はいってることってあるの?
unsigned int lookupIndex = feature->LookupListIndex[0];
// printf("%i: %s, %d\n", i, name, lookupIndex);
// このへんもリストの最初だけ
OTF_PairValueRecord *value = get_pair_set(&otf->gpos->LookupList.Lookup[lookupIndex].SubTable.gpos[0], gstring->glyphs[0].glyph_id, gstring->glyphs[1].glyph_id);
if (value) {
// XAdvance 以外に値はいってることあるの?
return value->Value1.XAdvance;
}
}
return 0;
}

int main(int argc, const char *argv[]) {
OTF *otf = OTF_open("/Library/Fonts/ヒラギノ明朝 Pro W6.otf");
OTF_get_table(otf, "GPOS");

OTF_GlyphString gstring;
gstring.size = 2;
gstring.used = 2;
gstring.glyphs = (OTF_Glyph *)calloc(gstring.size, sizeof(OTF_Glyph));
gstring.glyphs[0].c = 0x3042; // あ
gstring.glyphs[1].c = 0x3062; // ぢ
OTF_drive_cmap(otf, &gstring);
printf("Left glyph=0x%04x, Right glyph=0x%04x\n", gstring.glyphs[0].glyph_id, gstring.glyphs[1].glyph_id);

int kerning_value = get_kerning_value(otf, &gstring);
printf("Kerning value=%d\n", kerning_value);

free(gstring.glyphs);
OTF_close(otf);

return 0;
}

んで、これではまだ正確にポジショニングできなくて、kern feature の解説のとこに、


Feature interaction: If kern is activated, palt must also be activated if it exists. If palt is activated, there is no requirement that kern must also be activated.


ってあるように、palt があるならそれもちゃんと処理しないといけない。OTF 複雑すぎ、、、