ヒラギノ角ゴシックのCMapを読む

macOSに同梱されるヒラギノ角ゴシックの内部構造を覗いたところ、CMap に Platform ID 1 / **Format 2** が使用されていました。

## 少し丁寧な解説
フォントをレンダリングする場合、CMap と呼ばれるテーブルを利用して、Unicode 等の文字コードからフォント内部のグリフインデックス（**CID** / GID）を取得する必要があります。日本語フォントの CMap には、一般に Unicode に対応した下記サブテーブルが内包されており、これらをパースすることで容易に CID が得られます。

- Fotmat 4： 基本多言語面である U+0000―FFFF に対応
- Format 12： U+10000 以降に対応
- Format 14： IVS 等の異体字セレクタに対応

しかしながら、ヒラギノフォントに至っては、そうは問屋が卸さないようです。現行の macOS に同梱されているヒラギノフォントでは、上記フォーマットが含まれないものが存在し、代替として CMap Format 2 という謎のフォーマットが指定されていました。
Format 2 の仕様を読みながら、解析を試みます。

# Format 2 の概要
マイクロソフトが公開している OpenType® Specification [^1] を参照したところ、Format 2 はマルチバイト文字への対応の中で生じた、歴史的経緯によって残されているような印象を受けました。資料には下記の記述があります。

> This subtable format was created for “double-byte” encodings following the national character code standards used for Japanese, Chinese, and Korean characters. These code standards use a mixed 8-/16-bit encoding. This format is not commonly used today.

- CJK（日本語・中国語・韓国語）で使用されている各国の文字コード規格に準拠した 2 バイトエンコーディング用のサブテーブル。
- 8 ビットと 16 ビットが混在したエンコーディングが使用される。
- 現在ではあまり使われていない。

# フォーマット
同出典より、引き続きフォーマットを見ていきます。Format 2 のサブテーブルは、下記のデータ構造に従います。

## Format 2
| Type      | Name               | Description                                                                                                          |
| :-------- | :----------------- | :------------------------------------------------------------------------------------------------------------------- |
| uint16    | format             | Format 2 のとき 2                                                                                                    |
| uint16    | length             | サブテーブルのバイト長                                                                                               |
| uint16    | language           | 利用言語                     |
| uint16    | subHeaderKeys      | 上位バイトと subHeaders のマップ。subHeader のインデックスを8倍した値が含まれる。|
| SubHeader | subHeaders[]       | SubHeader の可変長配列                                                                                               |
| uint16    | glyphIdArray[]     | 2 バイト文字において、下位バイトへのマッピングに用いられる可変長配列                                                 |

- `subHeaderKeys` は、上位バイトと `SubHeader` レコードのマッピングに用いる。
- 2 バイト文字の場合、`SubHeader` は部分配列（後述）を介して 2 バイト目の値を対応付けるために使用される。
- 1 バイトの文字コードの場合は `subHeaders[0]` を読む。1 バイトの値が部分配列を介してマッピングされる。

## SubHeader
| Type   | Name          | Description                              |
| :----- | :------------ | :--------------------------------------- |
| uint16 | firstCode     | SubHeader において有効な最初の下位バイト |
| uint16 | entryCount    | SubHeader において有効な下位バイトの数   |
| int16  | idDelta       | 下記参照                                 |
| uint16 | idRangeOffset | 下記参照                                 |

- `firstCode` から下位バイトが始まり、`entryCount` の長さを持つ範囲（0―255 以内）が指定される。
- 下位バイトが部分範囲外のとき、グリフインデックス 0（グリフが見当たらない）にマッピングされる。
- 下位バイトが部分範囲内のとき、部分配列 `glyphIdArray`（長さ `entryCount`）のインデックスとなる。
- `idRangeOffset` は、`firstCode` の現れる `glyphIdArray` 要素が `idRangeOffset` の位置から何バイト先かを表す。
- 最終的に、部分配列から得られる値が 0 でない（グリフが存在しない）ときは、`idDelta` を加えると `glyphIndex` を得られる。

# 解説と実践：ヒラギノフォントを読む
訳がわからないですね、以下、噛み砕きながら詳しく見ていきましょう。

まず、文字コードの話からです。ASCII 文字列は 0-255（1 バイト）の範囲で表すことが出来ますが、膨大な量の漢字を有する日本語は、到底 256 文字では表現できません。そこで連続する 2 バイトを消費することで、 255 × 255 = 65025 個のグリフが表現可能になります。これがマルチバイトの考え方です。
例えば「あ」は Shift_JIS で表すと **0x82A0** に位置するため、上位バイトが 0x82、下位バイトが 0xA0 となります。

今回問題となる Format 2 は、Apple がかつて Macintosh 時代に使用していた文字コードに対応した CMap と見做すことが出来ます。
旧来の MacOS では、**Shift_JIS** をベースとした MacJapanese なるエンコーディングが利用されていました。従って日本語フォントで Format 2 の CMap が内包される際は、Shift_JIS に概ね則った文字コードを想定する必要があります。

##「あ」のCIDを取得する
Format 2 では、`subHeaderKey` -> `subHeaders` -> `glyphIdArray` の順に探索を進め、最終的に得られた値がフォントのグリフインデックス（CID ないし GID） になります。

`subHeaderKey` は文字の上位 1 バイト（i とする） と対応し、`subHeaderKey[i] / 8` が取得すべき subHeaders のインデックスとなります。
1 バイト文字を読み込む場合は、`subHeaderKey[i] / 8 == 0` となり、下位バイトは存在しません。SubHeader テーブルを参照するまでもなく、`idRangeOffset[i]` が直接 CID となります。

今回は、ヒラギノ角ゴシック Pr6N W6（PSName: HiraKakuProN-W6）上で「あ」の CID を読み出す手順を考えます。
先述の通り、｢あ」の上位バイトは i=0x82 (=130) ですので、まず `subHeaderKey[130]` を読み出します。ヒラギノの場合、`subHeaderKey[130] = 16` が設定されていました。
続いて、16 / 2 = 8 より `subHeaders[2]` を取得します。下記の SubHeader が得られました。

```
firstCode: 79
entryCount: 163
idDelta: 0
idRangeOffset: 1244
```

このとき、`subHeader[2]` は上位バイト 0x82、下位バイトは 10 進法で 79―241、すなわち 0x4f―0xf1 を表すことになります。
Shift_JIS の文字コード表[^2]と照合すると、0x824F-0x82F1 は全角数字の０に始まり、全角アルファベット・ひらがな（ぁ―ん）までをカバーする範囲に該当します。

`idRangeOffset` は 1244 であるため、0x824F の CID は、`idRangeOffset` を起点にして 1244 バイト先に存在します。ヒラギノフォントの場合、`idRangeOffset` は CMap テーブル自体の先頭を 0 として 29210 バイト目に存在するため、29210 + 1244 = 30454 バイト目を参照します。

参照先の値を p とするとき、p=781 でした。p≠0 の場合は `idDelta` を加える必要がありますが、今回は `idDelta` =0 であるため、CID780 が 0x824f の CID になります。今回は「あ」を読み出したいため、30454 に さらに (0x82A0 - 0x824F) * 2 = 162 を加えた 30616 バイト目を読むと、**843** の値が得られました。
Adobe Japan1-6 の文字コレクション表[^3]を参照すると、843 は確かに「あ」のグリフが存在しています。従って、無事正しい CID が取得できたようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514760/234fbd9d-ad19-c3f0-0129-0afd08172adf.png)

ポインタ風に書くとこんな感じになるはずです。

```C
cid = *(&idRangeOffset + idRangeOffset + (codepoint - firstCode)) + idDelta
```

なお、変換したいコードポイントが `firstCode` ― `firstCode + entryCount - 1` の範囲外である場合は、CID=0（欠落グリフ）となります。

## 実装
以上を踏まえて C# の実装に落とし込むと、以下のソースコードになりました。フォントの操作の部分に関しては割愛しています[^4]。

### フォントのパース
```C#
public class SubTableFormat2 : CMapSubTable
{
    public ushort Language { get; }
    public ushort[] SubHeaderKeys { get; }
    public List<SubHeader> SubHeaders { get; } = new();

    internal SubTableFormat2(FontBinaryReader reader)
    {
        reader.Position = 4;
        Language = _reader.ReadUInt16();
        SubHeaderKeys = new ushort[256];
        for (int i = 0; i < 256; i++)
        {
            SubHeaderKeys[i] = _reader.ReadUInt16();
        }
        int maxSubHeader = SubHeaderKeys.Max();
        for (int i = 0; i <= maxSubHeader; i++)
        {
            SubHeaders.Add(new SubHeader(_reader));
        }
    }
}

public class SubHeader
{
    public ushort FirstCode { get; }
    public ushort EntryCount { get; }
    public short IdDelta { get; }
    public ushort IdRangeOffset { get; }
    public ushort[] GlyphIdArray { get; }

    internal SubHeader(FontBinaryReader reader)
    {
        FirstCode = reader.ReadUInt16();
        EntryCount = reader.ReadUInt16();
        IdDelta = reader.ReadInt16();
        IdRangeOffset = reader.ReadUInt16();
        int pos = reader.Position;
        GlyphIdArray = new ushort[EntryCount];
        for (int i = 0; i < EntryCount; i++)
        {
            if (pos + IdRangeOffset + i * 2 < reader.Length)
            {
                reader.Position = pos + IdRangeOffset + (i - 1) * 2;
                GlyphIdArray[i] = reader.ReadUInt16();
                reader.Position = pos;
            }
        }
    }
}
```

### CIDへの変換
```C#
int UnicodeToCID(CMapTable table, int unicode)
{
    foreach (SubTable table in table.macintoshSubTables)
    {
        if (table is SubTableFormat2 format2)
        {
            # Unicode to Shift_JIS
            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
            Encoding shiftJis = Encoding.GetEncoding("Shift_JIS");
            byte[] bytes = Encoding.Unicode.GetBytes(Convert.ToChar(unicode).ToString());
            byte[] shiftJisBytes = Encoding.Convert(Encoding.Unicode, shiftJis, bytes);

            int subHeaderKey = format2.SubHeaderKeys[shiftJisBytes[0]];
            SubHeader subHeader = format2.SubHeaders[subHeaderKey / 8];
            int deltaIndex = shiftJisBytes.Last() - subHeader.FirstCode;
            if (0 <= deltaIndex && deltaIndex < subHeader.GlyphIdArray.Length)
            {
                int cid = subHeader.GlyphIdArray[deltaIndex];
                return cid + (cid != 0 ? subHeader.IdDelta : 0);
            }
        }
    }
    return 0;
}
```

## 雑感
実は Apple[^5] のドキュメントの方が読みやすかった。

[^1]: cmap - Character To Glyph Index Mapping Table (OpenType 1.9) - Typography | Microsoft Docs - https://docs.microsoft.com/en-us/typography/opentype/spec/cmap#format-4-segment-mapping-to-delta-values
[^2]: Shift_JIS 文字コード表 - https://seiai.ed.jp/sys/text/java/shiftjis_table.html
[^3]: Adobe Technical Note #5078: The Adobe-Japan1-6 Character Collection - https://www.adobe.com/content/dam/acom/en/devnet/font/pdfs/5078.Adobe-Japan1-6.pdf
[^4]: C# を含めた .NET 上では、バイト列は**リトルエンディアン**で扱われることに留意する必要があります。OpenType/TrueType のデータ構造は、全てビッグエンディアンで表されます。
[^5]: https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6cmap.html