cmap テーブル

cmap テーブルについて
「cmap テーブル」は、Unicode などの文字コードから、フォントに格納されているグリフのインデックス値を取得するためのデータです。
フォントファイルを使って文字を出力する場合は、必須となるデータです。

フォント内では、各グリフは、「グリフインデックス」によってその位置が定義されています。
(「グリフ ID (GID)」とも呼ばれます)

グリフインデックスは、配列の添字のようなものです。
グリフインデックス = 0 (.notdef)」は、フォント内にグリフが存在しないことを表します。

各グリフは、フォントファイルごとに、動的に連続して配置された状態で格納されています。
(あるフォントで、文字 'A' の GID が 10 だったとしても、別のフォントではグリフインデックスの値は異なります)
そのため、グリフインデックスは基本的に、そのファイル内のみの固有の番号となります。

OpenType では、一つのフォントに最大 65535 個のグリフが格納できるため、グリフインデックスの値は uint16 の型となります。
テーブルデータ
uint16 versionバージョン = 0
uint16 numTablesEncodingRecord テーブルの数
EncodingRecord encodingRecords[numTables]EncodingRecord テーブルの配列。
「platformID < encodingID < サブテーブルの language 値」の順でソートされている。
この3つの組み合わせは、cmap テーブル内に1つしか存在しない。

EncodingRecord
uint16 platformIDプラットフォームID。

0 Unicode
1 Macintosh
2 ISO (OpenType 1.3 で非推奨になった)
3 Windows
4 Custom
uint16 encodingIDエンコーディングID。
platformID によって値が異なる。
Offset32 offsetサブテーブルのオフセット位置 (cmap テーブルの先頭を 0 とする)

「サブテーブルのオフセット値」の位置にあるデータが、実際の変換データとなります。
encodingID
platformID = 0 (Unicode)
0Unicode 1.0 (非推奨)
1Unicode 1.1 (非推奨)
2ISO/IEC 10646 (非推奨)
3Unicode 2.0 (BMP のみ)
cmap format (0, 4, 6)
4Unicode 2.0 (full repertoire)
cmap format (0, 4, 6, 10, 12)
5Unicode Variation Sequences
cmap format (14)
6Unicode full repertoire
cmap format (0, 4, 6, 10, 12, 13)

platformID = 1 (Macintosh)
encodingID は 0 のみ。

platformID = 3 (Windows)
0 Symbol
1 Unicode BMP
2 ShiftJIS
3 PRC
4 Big5
5 Wansung
6 Johab
7-9 Reserved
10 Unicode full repertoire

platformID = 4 (Custom)
Windows NT との互換性用。
encodingID は 0〜255 で、元の Type 1 フォント (.PFM) の Windows 文字セット値。
サブテーブル
サブテーブルでは複数のフォーマットが存在し、各エンコーディングごとに適したフォーマットが使われます。

一般的なフォントは Unicode に対応しているので、ここでは、Unicode で使われるフォーマットのみを解説します。
Format 4 : Unicode BMP
Unicode BMP (U+0000〜U+FFFF) 用。
BMP は、「Basic Multilingual Plane (基本多言語面)」です。よく使う基本的な文字が含まれている領域です。

連続した Unicode 値を一つのセグメントとして、そのセグメントが複数格納されています。
セグメントは、endCode 値の小さい順にソートされています。

uint16 formatフォーマット番号 = 4
uint16 lenサブテーブルの長さ
uint16 languageMac 用。それ以外では 0
uint16 segCountX2segCount * 2
uint16 searchRangesegCount 以下の最大の2の累乗の2倍。
2 * (2 ^ floor(log2(segCount)))
uint16 entrySelectorlog2(searchRange / 2)
uint16 rangeShift2 * segCount - searchRange
uint16 endCode[segCount]各セグメントの終了文字コード (配列の最後 = 0xFFFF)
※ segCount は、最後の 0xFFFF も含む数となります。
uint16 reservedPad0
uint16 startCode[segCount]各セグメントの開始文字コード (配列の最後 = 0xFFFF)
int16 idDelta[segCount]各セグメントの、Unicode 値からグリフインデックスを算出するための増減値
uint16 idRangeOffset[segCount]glyphIDArray の値を使う場合は、オフセット位置。
idDelta を使う場合は、0。
uint16 glyphIDArray[(可変)]idRangeOffset が 0 でなかった時の、グリフインデックスを格納するデータ領域

変換手順
Unicode 文字を c とした場合の、グリフインデックスへの変換手順は以下のようになります。

  1. segCountX2 を2で割って、segCount (セグメント数) を取得します。
  2. endCode の配列から、c <= endCode[i] となる位置を見つけます。
    (c の値が対象となるセグメントを見つける)
  3. startCode[i] の値を参照します。
    c < startCode[i] の場合は、そのセグメントの範囲外なので、定義なし = グリフインデックス 0 となります。
    c >= startCode[i] の場合は、セグメント範囲内なので、グリフインデックスが定義されています。
  4. idRangeOffset[i] の値を参照します。
    値が 0 の場合は、「グリフインデックス = (c + idDelta[i]) & 0xFFFF」となります。
    値が 0 でない場合は、以下の式で算出します (C 言語)。
    glyphIndex = *(&idRangeOffset[i] + idRangeOffset[i] / 2 + (c - startCode[i]))
    

idDelta の値について
c + idDelta[i] の計算結果は、グリフインデックスの最大値 (65535) を超える場合があります。

というのも、文字コード c は uint16 で、idDelta は int16 (符号付き) であるため、例えば Unicode「60000」からグリフインデックス「100」を算出したい場合は、「60000 - 59900 = 100」としなければなりませんが、59900 の値は int16 では表現できません。

そこで、「(60000 + 5636) & 0xFFFF = 100」というように、範囲を超えるように加算した後、AND を行うことで、目的の数値を表現できるようになっています。

idRangeOffset と glyphIDArray について
▼ uint16 * での計算

glyphIndex = *(&idRangeOffset[i] + idRangeOffset[i] / 2 + (c - startCode[i]))

▼ バイトポインタでの表現

glyphIndex = *((uint16 *)((uint8 *)&idRangeOffset[i] + idRangeOffset[i] + (c - startCode[i]) * 2))

この計算が少しややこしいのですが、idRangeOffset の値は、idRangeOffset[i] の位置を 0 とした、glyphIDArray へのオフセット位置となっています。

そのため、まずは &idRangeOffset[i] で基点となるポインタ位置 (uint16 *) を取得し、そこに idRangeOffset[i] のオフセット位置を加えるのですが、ポインタの型が uint16 * なので、2で割ります。
また、glyphIDArray には、各セグメントごとに、「開始文字コード〜終了文字コード」までの、それに対応するグリフインデックス値が格納されているため、「c - startCode[i]」で、文字 c のグリフインデックス位置へ移動します。

最後に、「*((uint16 *)(ポインタ位置))」で、グリフインデックスを取得する式となっています。
Format 12 : Unicode (U+10000〜) 用
Unicode (U+10000〜U+10FFFF) 用。

uint32 の文字コードの連続した範囲を、連続したグリフインデックス値に変換します。

uint16 formatフォーマット番号 = 12
uint16 reserved予約 = 0
uint32 lenサブテーブルの長さ (※uint32)
uint32 language(※uint32)
uint32 numGroupsSequentialMapGroup の数
SequentialMapGroup [numGroups]グループデータ。startCharCode の小さい順に並んでいる。

SequentialMapGroup
uint32 startCharCode開始の文字コード
uint32 endCharCode終了の文字コード
uint32 startGlyphID開始文字コードに対応する、グリフインデックス値

「startCharCode〜endCharCode」の範囲の Unicode を、「startGlyphID + (c - startCharCode)」のグリフインデックスに変換します。
Format 14 : 異体字用
Unicode (platformID = 0) の時のみ使われ、異体字セレクタを処理します。
今回は概要の説明のみとし、実際の処理は行いません。

異体字セレクタとは
異体字セレクタを使うと、漢字などにおいて意味は同じでも字形が微妙に異なる文字を、「ベースの文字コード」+「異体字セレクタ」の2つのコードで表現することが出来ます。
異体字セレクタは、「Unicode Variation Sequences」、略して「UVS」と呼びます。

日本語では主に漢字で使われますが、他にも数学記号・モンゴル文字・絵文字などでも使われます。

<例>
U+82A6 U+E0100 → Adobe-Japan1 : CID 1142
U+82A6 E+E0101 → Adobe-Japan1 : CID 7961

UVS には2つの種類があり、
Standardized Variation Sequence (SVS) = 標準化された異体字シーケンス」と、
Ideographic Variation Sequence (IVS) = 漢字異体字シーケンス」があります。
IVS は漢字専用で、SVS は漢字以外や CJK 互換漢字で使われます。

SVS では、異体字セレクタとして「U+FE00〜U+FE0F (VS1〜VS16 : 16個)」が使われ、
IVS では、「U+E0100〜U+E01EF (VS17〜VS256 : 240個)」が使われます。

日本語フォントでは、主に Adobe-Japan1 で定義されている IVS に対応しているものが多いですが、フリーフォントなどでは、異体字セレクタに対応していないものもあります。

また、フォントが対応していても、ソフトウェアの方で異体字セレクタの処理を行っていなければ、表示できません。
FreeType の場合は、FT_Face_GetCharVariantIndex() 関数に2つの Unicode 値を渡して、グリフインデックスを取得する必要があります。

StandardizedVariants.txt (SVS リスト)
Ideographic Variation Database (IVS リスト)
プログラム 1 (cmap テーブルの一覧)
まずは、cmap の一覧を表示してみます。

>> 08a_cmap_list.c

---- cmap  ----

version: 0
numTables: 6

[platformID: 0 (Unicode) | encodingID: 3 | offset: 27425]
= format:4 (Unicode BMP)

[platformID: 0 (Unicode) | encodingID: 4 | offset: 64489]
= format:12 (Unicode full)

[platformID: 0 (Unicode) | encodingID: 5 | offset: 52]
= format:14 (Unicode UVS)

[platformID: 1 (Mac) | encodingID: 1 | offset: 27413]
= format:6 

[platformID: 3 (Windows) | encodingID: 1 | offset: 27425]
= format:4 (Unicode BMP)

[platformID: 3 (Windows) | encodingID: 10 | offset: 64489]
= format:12 (Unicode full)

「源ノ角ゴシック」や「源ノ明朝」など、UVS に対応しているフォントでは、大体上記のような構成になっています。

UVS に対応していない場合は format 14 のデータがなく、Unicode の U+10000 以上の範囲の文字がなければ format 12 のデータはありません。

また、フォントによっては、「platformID = 3 (Mac)」のデータがない場合や、「platformID = 0 (Unicode)」のデータがない場合もあります。

Unicode でのマッピングを行いたい場合は、「platformID = 0 or 3」のデータを読み込みます。
platformID が異なっていても、内容は Unicode でのマッピングであることに変わりはないので、基本的にはどちらを使っても問題ありません。
実際、上記の例では、「platformID の 0 と 3」は、同じオフセット位置を示しています。

ただし、フォントによっては、「platformID の 0 と 3」でマッピングが異なる場合が稀にあるので、注意してください。
プログラム 2 (変換テーブルを作る)
グリフ置き換えを行う GSUB などのテーブルでは、文字はグリフインデックス値を使って処理されます。
(cmap で変換した GID を、別の GID へ置き換えるなど)

そのため、GSUB などのテーブルを解析する時に、そのグリフ ID が Unicode で何の文字であるかがわかると便利なので、cmap のデータを使って、「グリフ ID → 文字コード (Unicode)」への変換テーブルを作ってみます。

>> 08b_cmap_tbl.c

フォントファイルを指定して実行すると、結果を「output.txt」に出力します。
テキストファイルには、各グリフ ID に対応する Unicode 文字の一覧が書き込まれています。

ここでは、「platformID = 0 or 3」の、「Unicode BMP (format 4)」と「Unicode full (format 12)」のみ読み込んでいます。

----| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9| A| B| C| D| E| F|
0000|--| |!|"|#|$|%|&|'|(|)|*|+|,|‑|.|
0010|/|0|1|2|3|4|5|6|7|8|9|:|;|<|=|>|
0020|?|@|A|B|C|D|E|F|G|H|I|J|K|L|M|N|
0030|O|P|Q|R|S|T|U|V|W|X|Y|Z|[|\|]|^|
0040|_|`|a|b|c|d|e|f|g|h|i|j|k|l|m|n|
0050|o|p|q|r|s|t|u|v|w|x|y|z|{|||}|~|
0060|¡|¢|£|¤|¥|¦|§|¨|©|ª|«|¬|®|¯|°|±|
...