cmap テーブル

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

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

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

グリフインデックスは、それぞれのフォントごとに、独自の順番で割り当てられるので、基本的に、グリフインデックスはそのフォント固有の番号になります。

TrueType/OpenType では、一つのフォントに最大 65535 個のグリフが格納できるため、グリフインデックスは uint16 の型となり、0〜65534 までの値となります。
テーブルデータ
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。
なぜ個数を2倍しているかと言うと、以降の endCode,startCode,idDelta,idRangeOffset がすべて 2byte の配列だからです。
endCode の位置から startCode の位置へ移動する場合、segCountX2 (+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 reservedPad予約。0
uint16 startCode[segCount]各セグメントの開始文字コード (配列の最後 = 0xFFFF)
int16 idDelta[segCount]各セグメントの、Unicode 値からグリフインデックスを算出するための増減値
uint16 idRangeOffset[segCount]idDelta の値を使う場合は、0。
glyphIDArray の配列値を使う場合は、オフセット位置 (idRangeOffset の現在位置から、glyphIDArray 内の該当位置までのバイト数)。
uint16 glyphIDArray[(可変)]idRangeOffset が 0 でなかった時の、グリフインデックス配列を格納するデータ領域

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

  1. segCountX2 を2で割って、segCount (セグメント数) を取得します。
  2. endCode の配列から、c <= endCode[i] となる位置を見つけます。
    (c の値が対象となるセグメントを見つける)
  3. endCode の現在位置から、(segCountX2 + 2) byte を移動し、startCode[i] の値を参照します。
    c < startCode[i] の場合は、そのセグメントの範囲外なので、グリフインデックス = 0 となります。
    c >= startCode[i] の場合は、セグメントの範囲内なので、グリフインデックスが定義されています。
  4. startCode の現在位置から、segCountX2 byte を移動し、idDelta[i] の値を参照。
    さらに、idDelta の現在位置から、segCountX2 byte を移動し、idRangeOffset[i] の値を参照します。
  5. idRangeOffset の値が 0 の場合は、idDelta の値を使うので、「GID = (c + idDelta[i]) & 0xFFFF」となります。
  6. idRangeOffset の値が 0 でない場合は、その値をオフセット値として使うので、以下のように取得します。
    idRangeOffset の現在位置から、(idRangeOffset + (c - startCode) * 2) byte を移動して、glyphIDArray 内の uint16 値を取得。

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

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

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

idRangeOffset と glyphIDArray について
idRangeOffset が 0 でない場合、その値は、idRangeOffset[i] の位置を 0 とした時の、glyphIDArray 内へのオフセット位置 (バイト数) となっています。
また、その位置には、セグメントの startCode を先頭として、endCode までの GID の配列が格納されているので、(c - startCode[i]) の分も移動する必要があります (データは 2byte なので、バイト単位なら x 2 する)。
Format 12 : Unicode フル用
Unicode の U+10000〜U+10FFFF の範囲が含まれる場合に使います (データは、U+10000 未満の範囲も含む)。

フォント内には、Format 4 も同時に含まれている場合が多いですが、それは、Format 12 に対応していないソフトが、Format 4 の方で読み込めるようにするためのものです。
Unicode の BMP と U+10000〜 でデータが分かれているわけではないので、どちらか一方を使う必要があります。

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) の時のみ使われ、異体字セレクタを処理します。
今回は概要の説明のみとし、実際の処理は行いません。

異体字セレクタとは
異体字セレクタを使うと、漢字などにおいて、意味は同じでも字形が微妙に異なる文字を、「ベースの文字コード」の直後に「異体字セレクタ」のコードを置くことで、表現することが出来ます。
異体字セレクタは、「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 で扱う
通常は、Unicode で GID を取得したい場合がほとんどだと思うので、フォントを読み込む側は、「platformID = 0 or 3」のデータを読み込む必要があります。

Unicode でも Windows でも、内容は Unicode でのマッピングであることに変わりはないので、基本的にはどちらを使っても問題ありませんが、フォントによってはまれに、Unicode と Windows でデータが異なる場合があるので、platformID = 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|¡|¢|£|¤|¥|¦|§|¨|©|ª|«|¬|®|¯|°|±|
...