13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

こんにちは
株式会社うるるの山下です。

うるる (ULURU) Advent Calendar は19日目の記事でして、本日は1文字になろうと思います。

はじめに

Unicode(ユニコード)はご存知でしょうか。
きっと一度は耳にしたことのある単語だと思います。

Unicodeとは一言でいうと:

世界中の文字に番号(コードポイント)を割り当てた巨大な表1

です。
具体例を挙げると以下のようなものですね。

文字 Unicodeコードポイント
A U+0041
U+3042
U+5C71
U+4E0B
😀 U+1F600

今回はここに「山下」という見た目の1文字を登録してブラウザで表示させることがゴールです。

Unicodeの私用領域について

Unicodeが文字とコードポイントの組み合わせからなる巨大な表であることは既にお伝えしました。
具体例では5種類のみ挙げましたが、Unicodeは約111万文字分の領域が用意されており、そのうち約16万は既存の文字で予約されています。
残りの領域の中には、Unicode私用領域(Private Use Area, PUA) と呼ばれる特別な範囲が存在します。

Unicodeの私用領域(PUA)

Unicode 私用領域とは、その名の通り 誰でも自由に使ってよい領域 です。
ここに割り当てられたコードポイントについて、Unicode Consortium は

  • どんな文字なのか
  • どう表示されるべきか
  • 何を意味するのか

を一切定義しません。

代表的な私用領域は以下の3つです。

範囲 名称
U+E000–U+F8FF BMP私用領域
U+F0000–U+FFFFD 補助私用領域A
U+100000–U+10FFFD 補助私用領域B

今回は扱いやすい BMP私用領域(U+E000–U+F8FF) の中から、U+E000を使います。

U+E000 をそのまま表示してみる

まずはフォントを一切用意せず、素のHTMLで U+E000 を表示してみましょう。

多くの環境では、以下のような表示になります。

  • □(いわゆる「豆腐」)
  • 何も表示されない
  • 環境によっては ? や �

これは表示に失敗している状態です。
しかし、ここで重要なのはブラウザが「壊れた文字」を受け取っているわけではないという点です。

なぜ U+E000 は文字化けするのか

そのコードポイントに対応するフォントが存在しないからですね。

UnicodeではU+E000 は「私用領域のコードポイント」として有効だが、どんな字形かは定義されていないという状態です。

大枠の流れとしては

  1. ブラウザは U+E000 を「正しい文字」として解釈する
  2. 使えるフォントを探す
  3. どのフォントにもコードポイントに対応した字形が無い
  4. 仕方ないので「代替表示(豆腐)」を出す

ということになります。

フォント作成

ここまでで、

  • Unicode:文字に番号を振る
  • フォント:番号を「見た目」にする

という役割分担を見てきました。
この「フォント」を具体的なファイル形式として表したものの一つがTTF(TrueType Font)です。

TTFについて

構造は以下のようになっています。

[ Offset Table ]
   |
   +-- [ cmap ]  ← どのコードポイントがどのグリフか
   +-- [ glyf ]  ← グリフの形そのもの
   +-- [ loca ]  ← glyf の位置情報
   +-- [ head ]  ← フォント全体のメタ情報
   +-- [ hhea ]  ← 横書き用メトリクス
   +-- [ hmtx ]  ← 字幅など
   +-- [ maxp ]  ← 最大値情報
   +-- ...

(詳しくは https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6.html を確認してみてください。)

今重要な項目だけ触れます。

cmap

Unicodeコードポイント → グリフID

の対応情報が入っています。(グリフIDは後述しますが、字形情報のIDと考えてください)
概念的には以下のイメージです。

U+0041 → glyph #12   (A)
U+3042 → glyph #53   (あ)
U+E000 → glyph #999  (山下)

ブラウザやOSは、Unicodeのコードポイントからcmapの中を探して該当グリフIDを探すという動きをします。
ここで見つからなければ、そのフォントでは「表示不可」です。

glyf

glyfには、「どう描くか」が入っています。

TTFのグリフは、ビットマップではなくベクター(アウトライン)で定義されています。
具体的には、

  • 点(座標)
  • 二次ベジェ曲線(svgで使われるpathと同じです)

の組み合わせです。
拡大・縮小してもギザギザになりません。
「山下」を1文字として定義した場合、山のアウトライン下のアウトラインそれらを統合した1グリフがここに入ります。

loca

glyph ID → glyf内のオフセット

の対応情報を持っています。
glyfは可変長データなので各glyfのオフセットを知りたいわけですね。
ブラウザやOSは以下のように形・描き方を取得することができます。

  1. コードポイントを取得
  2. cmapでコードポイントのglyf IDを取得
  3. locaでglyf IDがglyfのどのオフセットから始まるのかを取得

他にもカーニングやヒンティング情報など面白い情報が詰まっていますが、今回は割愛します。

(余談)実データ

記事が寂しくなるのでもう少し踏み込んでTTFデータを見てみます。
とはいえ全て解説すると長くなるのでglyfの部分だけ解説させてください。

今回ですが、シンプルさ重視で「山下」はジェンガブロックで文字を作るように、1つの直線を1つの四角形で表現する方法にします。

x1,y1 = (120,200)
x2,y2 = (140,850)

例えば上記のような四角形の情報7つをTTFのglyfの形式に沿ってセットしていくイメージです。

7つと言ったのは「山下」が
「山」で4つ
「下」で3つ
形7つの四角で表現できるからです。

実際にコードで書くとこんな感じです。

const sw int16 = 70 // stroke width

// 山
rect(120, 200, 120+sw, 850)
rect(260, 120, 260+sw, 900)
rect(400, 200, 400+sw, 850)
rect(120, 120, 470, 120+sw)

// 下
rect(540, 820, 920, 820+sw)
rect(700, 100, 700+sw, 900)
rect(740, 620, 920, 620+sw)

肝心のglyfのバイト列の構造は以下のようになっています。(Big Endian)

numberOfContours (int16)
xMin,yMin,xMax,yMax (int16×4)
endPtsOfContours[] (uint16 × numberOfContours)
instructionLength (uint16)
instructions (byte × instructionLength)
flags[] (byte × numPoints ※RLEあり)
xCoordinates(delta圧縮)
yCoordinates(delta圧縮)

numberOfContours

numberOfContoursは四角形の数なので00 07

xMin,yMin,xMax,yMax

xMin = 120
yMin = 100
xMax = 920
yMax = 900
これをint16 Big Endianで表現すると
120 = 00 78
100 = 00 64
920 = 03 98 (0x0398)
900 = 03 84 (0x0384)

endPtsOfContours

endPtsOfContoursは各四角形の終点が全ての四角形の点の中で何番目かというのをリストで持ちます。
今回は四角形を並べただけなので
1個目 rect:点 0..3 → 終点 3
2個目 rect:点 4..7 → 終点 7
3個目 rect:点 8..11 → 終点 11
4個目 rect:点 12..15 → 終点 15
5個目 rect:点 16..19 → 終点 19
6個目 rect:点 20..23 → 終点 23
7個目 rect:点 24..27 → 終点 27
→ [3, 7, 11, 15, 19, 23, 27]
uint16 Big Endianのbyte列で
00 03 00 07 00 0B 00 0F 00 13 00 17 00 1B
になります。

instructionLength

instructionLengthは今回は00 00 (=instructions自体存在しない)

flags

flagsには座標データをどう読めばよいかを示すビット列です。
説明が難しいのですが、

01 21 11 21

は、

点0:xもyも読む → 開始点
点1:xだけ読む → 横移動
点2:yだけ読む → 縦移動
点3:xだけ読む → 横に戻る

という読み取り方をします。
これは↓のルール(一部)に従っており、flags を読むだけで「この点は x を読む/読まない、y を読む/読まない」が即座に分かります。(後述のdelta圧縮で利用)

bit 名前 意味
0 0x01 ON_CURVE この点はオンカーブ点
4 0x10 X_SAME x差分が 0(または short)
5 0x20 Y_SAME y差分が 0(または short)

今回は全て四角形なので01 21 11 21 01 21 11 21 01 21 11 21 01 21 11 21 01 21 11 21 01 21 11になります(01 21 11 21が7つ)

xCoordinates yCoordinates(delta圧縮)

四角形の点の移動距離(差分)をx軸、y軸のそれぞれで計算します。
(「どこが四角形の最後か」はendPtsOfContoursから判断できます)

rect1: rect(120,200,190,850)
点: (120,200)(190,200)(190,850)(120,850)
dx/dy: (120,200)(70,0)(0,650)(-70,0)
flags: 01 21 11 21
xBytes: 00 78 00 46 FF BA
yBytes: 00 C8 02 8A

rect2: rect(260,120,330,900)
xBytes: 00 8C 00 46 FF BA
yBytes: FD 26 03 0C

....

最終的にglyf部分は以下のようになります。

0000: 00 07 00 78 00 64 03 98 03 84 00 03 00 07 00 0B
0010: 00 0F 00 13 00 17 00 1B 00 00 01 21 11 21 01 21
0020: 11 21 01 21 11 21 01 21 11 21 01 21 11 21 01 21
0030: 11 21 01 21 11 21 00 78 00 46 FF BA 00 8C 00 46
0040: FF BA 00 8C 00 46 FF BA FE E8 01 5E FE A2 01 A4
0050: 01 7C FE 84 00 A0 00 46 FF BA 00 28 00 B4 FF 4C
0060: 00 C8 02 8A FD 26 03 0C FD 44 02 8A FD 26 00 46
0070: 02 76 00 46 FC EA 03 20 FE E8 00 46

確認

実際にTTFを返すサーバを用意して1文字になれるのか確認してみます。

mux.HandleFunc("/fonts/yamashita.ttf", func(w http.ResponseWriter, r *http.Request) {
	ttf := buildYamashitaTTF() // 「山下」のデータが入ったTTF
	w.Header().Set("Content-Type", "font/ttf")
	_, _ = w.Write(ttf)
})
@font-face{
  font-family: YamashitaOneChar;
  src: url("/fonts/yamashita.ttf") format("truetype");
}

無事表示されました。
image.png

Wordにコピペして文字数を確認してみます。
ちゃんと1文字ですね。
image.png

  1. 「表」より「集合」という表現が適切

13
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?