フォントを学ぶ

しむどん 2025-02-22

普段フォントを使う時、フォントがある事が当たり前過ぎて使っている事を気にも止めないし、最近のツールを使う時にフォントが原因で困った状況になる事はあまりない。しかしTeXなど古くからあるツールを使う時には、読み込まれなかったりエラーしたりと、問題が発生する事がある。そんな状況にひとたび陥ると、フォントの基本的な知識が無さ過ぎてトラブルを解決できない。フォントの知識がないというのは、プログラマーとしてはやはり問題だろう。そこで今回はフォントがどういうものなのか、手で作りながら学ぶ事にした。

今回は古くて単純な形式であるType 1を取り扱う

フォントの形式はいくつかある。最近では OpenType がよく使われる。しかし今回はフォントの基本的な考え方を学びたい。そこでもはや使われる事のなくなった PostScript Type 1 フォントを扱う事にする。

Type 1に関連するツール

フォントは既にインストールされていたり、どこかからダウンロード、インストールをして利用する事が多いだろう。文字の見た目にこだわりたい場合、状況によってはフォントを自作する事もある。

独自フォントを作る場合、際、フォントを作成したり、形式を変換したりするツールを使う事になる。ここではType 1フォントを扱うためのツールを紹介する。

なお通常は、間違っても今回のようにフォントをテキストエディタで記述したりはしない。

FontForge

オープンソースかつクロスプラットフォームなフォント編集ソフトウェアであり、様々な形式のフォントを作る事ができる。今回も最初の検証で使用するフォントの制作にはFontForgeを使う。

T1utils

T1utilsはType 1フォントを扱うためのツールが実装されている。今回は、Homebrewでビルド済みバイナリをインストールする。

brew install t1utils
T1utilsをインストールする。

T1utilsには6つのプログラムがある。

プログラム 用途
t1ascii PFBファイルをPFA形式に変換する。
t1binary PFAファイルをPFB形式に変換する。
t1disasm Type 1フォントを人間が読める形式にする。
t1asm Type 1フォントの元となるデータから、Type 1を作る。
t1unmac Macintosh Type 1フォントファイルからPOSTリソースを抽出する。
t1mac PFAまたはPFB形式のType 1からMacintosh Type 1ファイルを作成する。

例えば暗号化済みの Type 1 フォントファイルを復号化し、ヒトが読めるようにするには、 t1disasm を使い以下のコマンドを実行する。

t1disasm -o Simple.pfa.plain Simple.pfa

1文字だけのフォントを作る

Type1 の仕様は後で眺める事になるが、何も知らず例もない状態で仕様書を読んでも理解する事はなかなか難しい。そこであらかじめ例となる小さなフォントをFontForgeで作る事にする。

FontForgeで作ったType 1

まずFontForgeを使って正しいフォントを生成しよう。新しいフォントを作成し、 A の文字のグリフだけを登録する。そして Generate Fonts のメニューからフォント生成ダイアログを表示し Type 1 形式を選択、フォントファイルを作成する。

今回は以下のファイルを Simple.pfa という名前で生成した。

%!PS-AdobeFont-1.0: Simple 001.000
%%Title: Simple
%Version: 001.000
%%CreationDate: Sat Feb 15 09:34:20 2025
%%Creator: sximada
%Copyright: Copyright (c) 2025, sximada
% 2025-2-15: Created with FontForge (http://fontforge.org)
% Generated by FontForge 20230101 (http://fontforge.sf.net/)
%%EndComments

10 dict begin
/FontType 1 def
/FontMatrix [0.001 0 0 0.001 0 0 ]readonly def
/FontName /Simple def
/FontBBox {88 69 736 693 }readonly def
/PaintType 0 def
/FontInfo 9 dict dup begin
 /version (001.000) readonly def
 /Notice (Copyright \050c\051 2025, sximada) readonly def
 /FullName (Simple) readonly def
 /FamilyName (Simple) readonly def
 /Weight (Regular) readonly def
 /ItalicAngle 0 def
 /isFixedPitch true def
 /UnderlinePosition -100 def
 /UnderlineThickness 50 def
end readonly def
/Encoding StandardEncoding def
currentdict end
currentfile eexec
743F8413F3636CA85A9FFEFB50B4BB273023E93DF9BCDBD9D94233FB53F690564BB08DDC
EBB71CBEB281119D6A53B04B685076931FB3FAC7CCA65C9A76B4734884B4E063CB19F157
E30150099D9188525ADAEBA3B96F33256F4D5015CEA845E051110AF02A49FCF9922BB024
6133D07C3F897C434C3239CDC2BC6D33DEE9F02A647F8339E816915FE82CB77DB7812FC1
3ED9625CF738CE569BD39FF57435C751BE1446E4E766B60D24096DFACC689AE89912967D
9B602C0366FEA8A786E39D067304A555DC4BE38FE1E3987391AC8AA49C87C89587266E8E
4047C9E02AE63E010CC6ED361DA451735CCE225416CD439D490CFF9FFB1E4FAD4B1E3875
6ADA65686ECFC20334FE80B8583B0632BA7A5CC0CBF299ABCEF992BC3AEBBB70E35559DE
847738F77728BDD93242187DA36BC19E3F916D8F509EF07715595A7F3B4B73650D22FEA3
6DEA73C30F1B53D0C622CEE65DB72E7108ABAC1555C38D5C6FD4DB33BFCB1182FBA3C3D9
454478D0A2DEA6CE9930C9998ED8EE0F9E8EAF7E906B61B6EDD4242908FE0CFAA256D8F5
E68DB6739E70B3D684C892F80510707B4249E952366F4EA9678D79358A17EE86DF2A6043
9F5D6911624CF8948E88C39B25EC27F1D94D5CD9548F4BEEAB9BBE75B16B263A0C960843
6B286D6641C07FD5DDDA84E16B67CC26C896D91AEEE7CABFA5DBD78BC615F88D9F95263C
5255E7BD574E837C3FCFCF713767A40AC1975A9F8FDB0B1817B5A3DA5584B1273A528E1E
71BAAAEF0C4D826DAB7C5A46A22722F780A16F276CE1877AD09938BFEFE101ED86EEB05A
0A2B5083D9D9C52B9A3D6943A7BF971580C802BDA49595445A5D11A953690A5A58CAFB72
66BEFFBEB6B7F470594491E60F998D97BA02B816ED28B4B87AF66FC36223A3B178E26F36
F75B06C5C3A470BBB035A2DB5E1B7CDE1F3A5B9DDE6A5207ACEA1E0F93A870003AAB232D
8FD541926C1A2662B2356CB859EA249985CD67B3ABEAC104E976B7ECE9DD1FDE13A58C5F
12F17BF271F9A2413631E46D6CC7485851637503A6265C8AD648772398DF71C0A51760E0
51D4E116F63C61F5B1AD8661699EADE16F4D9A5EBD33BC384D75497E5AB814AB2F809EA2
AAAB2AAA859835CA0C79B966D3608D7D208788EEBD9A1CA0996182DCEBECECBA84547FB2
BB29D13B50E195EEC06B16BC4F2EB8D973126B40BE298CC52F9DD9B6145B723E75E38FD8
66EBF3FDD47202DF2B5290BF30816BB7437FF88A0C271B47321C6F7ED586FFA998489421
87BF6C2D0BC780A66405AE09ECF2E091227C3E27FDC7C9B016A8C76606DA05A2B89FE4BB
8BA0B126CE74AA46C5B9D7C1C3C6BEF6D2E28C28AC5D81999D4E50310ECB3443DDCC629B
DD0140271CFAB491F640869580376FF475370403A9DE34457BA4
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
cleartomark

このフォントはAの文字のみを収録している。

作ったフォントをGhostscriptで印字する

作ったフォントが正しく使えるものなのか確認するために、Ghostscriptに読み込ませ印字してみよう。

通常Ghostscriptは所定の位置にフォントファイルを配置するが、今回は恒久的に使うものではないため、作業ディレクトリに配置し環境変数 GS_FONTPATH を使ってフォント探索パスを指定する。

環境変数 GS_FONTPATH を使うとGhostscriptがフォントを探す際のパスを指定する事ができる。今回使用したいフォントは作業ディレクトリに配置したため、 GS_FONTPATH にも作業ディレクトリへのパスを指定する。

export GS_FONTPATH=$PWD

次にGhostscriptからこのフォントをロードできるかを確認してみよう。Ghostscriptのインタプリタを起動する。

gs
GPL Ghostscript 10.04.0 (2024-09-18)
Copyright (C) 2024 Artifex Software, Inc.  All rights reserved.
This software is supplied under the GNU AGPLv3 and comes with NO WARRANTY:
see the file COPYING for details.
GS>

findfont を使用してフォントを読み込む。PostScriptは逆ポーランド記法のため命令が後ろに来る。

GS>/Simple findfont
Scanning /Users/dummy/1736848411 for fonts... 7 files, 7 scanned, 2 new fonts.
Can't find (or can't open) font file /Users/dummy/.ghostscript/share/ghostscript/10.04.0/Resource/Font//Users.
Can't find (or can't open) font file Simple.
Loading Simple font from /Users/dummy/1736848411/Simple.pfa... 3262648 1795586 2731920 1440096 1 done.

最後の行で Simple というフォントを Simple.pfa から読み込んでいる事が分かる。

問題なさそうだ。それでは文字を印字するPostScriptを書く。

%!PS-Adobe-3.0
/Simple findfont 15 scalefont setfont
4 1 moveto
(A) show
showpage

main.gs

この main.gs をGhostscriptで処理し、PNG画像として生成してみよう。

gs -dNOPAUSE -dBATCH -r350 -g100x100 -sDEVICE=png16m -sOutputFile=output.png main.gs

成功すると output.png という名前でPNG画像が印字される。

https://pub.symdon.info/d1/af/d1af672b3154aae1256c063ea8d675096c27dbcd2dfd2156677ea3918e312fb9.png

Type1の仕様

仕様の公開については一悶着あったようだが1、仕様を確認したければ現在は https://adobe-type-tools.github.io/font-tech-notes/pdfs/T1_SPEC.pdf にType 1の仕様に関する文書が配置されている。今回はこの仕様書を適宜参照しながら形式を確認する。

PostScriptフォント

PostScriptフォントはPostScriptでフォントを記述する。PostScript自体はページを記述するための言語であるため、この言語を用いてフォントを記述することで、それまでのビットマップフォントなどと比較して高い品質のフォントを実現することができた。Type 1は、PostScriptを基盤として文字のアウトラインを定義するためのフォント形式であり、フォントデータを効率的に管理および記述するために設計されている。

フォントファイルの全体像

ここからはフォントファイルの中身の構造を確認する。 Type 1 では、いくつかのブロックでフォントを定義する。

%!FontType1-1.0
%---- 平文の冒頭部分 (仕様書ではASCIIと表記)
/FontInfo % 省略
          % 省略
eexec
%---- 暗号化された部分 (仕様書ではeexec encryptionと表記)
/Private     % 省略
/OtherSubrs  % 省略
/Subrs       % 省略
/CharStrings % 省略
% 省略
dup /FontName get exch definefont pop
mark currentfile closefile
---- 平文の後始末の部分 (仕様書ではASCIIと表記)
0000000000000000000000000000000000000000000000000000000000000000
        % 省略
0000000000000000000000000000000000000000000000000000000000000000
cleartomark
{restore}if

これらのブロックにPostScriptで適切な値を記述する。そのため頑張ればEmacsのようなテキストエディタでもフォントを書く事ができる2

フォント内に定義する辞書やシンボルの構造

コメント部、存在チェック、スタックのクリア処理など細かな所を一旦考えないとすると、 Type 1 はPostScriptで辞書を作っていく作業でもある。これらはどのような構造で、どのようなシンボルに束縛するかを仕様で決められている。それら1つずつについては後で確認していく事として、ここではおおまかな構造を眺めてみる。

- /FontInfo             :dict
  - /version            :string
  - /Notice             :string
  - /FullName           :string
  - /FamilyName         :string
  - /Weight             :string
  - /ItalicAngle        :number
  - /isFixedPitch       :boolean
  - /UnderlinePosition  :number
  - /UnderlineThickness :number
- /FontName             :name
- /Encoding             :array
- /PlainType            :integer
- /FontType             :integer
- /FontMatrix           :array
- /FontBBox             :array
- /UniqueID             :integer
- /Metrics              :dict
- /StrokeWidth          :number
- /Private              :dict
  - /RD                 :procedure
  - /ND                 :procedure
  - /NP                 :procedure
  - /Subrs              :array: サブルーチン。callsubで呼び出せる
  - /OtherSubrs         :array: Flex(柔軟な曲線補正)のサブルーチン
  - /UniqueID           :integer
  - /BlueValues         :array: 水平アラインメントの基準値
  - /OtherBlues         :array
  - /FamilyBlues        :array
  - /FamilyOtherBlues   :array
  - /BlueScale          :number
  - /BlueShift          :integer
  - /BlueFuzz           :integer
  - /StdHW              :array: 標準のストロークの太さ
  - /StdVW              :array: 標準のストロークの太さ
  - /StemSnapH          :array: ストロークのヒント情報
  - /StemSnapV          :array: ストロークのヒント情報
  - /ForceBold          :boolean
  - /LanguageGroup      :integer
  - /password           :integer
  - /lenIV              :integer
  - /MinFeature         :array
  - /RndStepUp          :boolean
- /CharStrings          :dict: グリフデータ
  - /A                  :charstring
  - /B                  :charstring
    ...                 :
  - /.notdef            :charstring
- /(FID)                :fontID

暗号化

Type 1 のフォントには eexec 暗号化と CharString 暗号化という仕組みが組み込まれており、部分的に暗号化されている。詳細な仕様は仕様書の第7章で解説されている。

Type 1 フォントは必ず PrivateCharString の辞書を含むが、2つの暗号化層はこれらを暗号化する。これは簡易な解析から、Private辞書内のヒント情報を部分的に保護する事を目的としている。また暗号文をASCII Hex形式とする事で、7ビットASCIIしか使用できないような通信チャネルでもデータを送信できる。

ここではeexec暗号化について見ていく。

eexec暗号化

先程掲載したフォントの例で eexec に続く部分から 0000...0000 cleartomark の部分の前にある16進数っぽい文字列の所は、eexec暗号化により暗号化されている。暗号化された16進数のデータを、16進数を表わす文字列(つまりASCII-Hex形式)でエンコードされている。

743F8413F3636CA85A9FFEFB50B4BB273023E93DF9BCDBD9D94233FB53F690564BB08DDC
暗号文の例

実際の暗号文はもっと長い。今回は実際の暗号文の冒頭部分を例に、 eexec 暗号化で処理された暗号文処理を順を追って手作業で復号化して理解を深める。

eexec 暗号化はいわゆるストリーム暗号と呼ばれる種類の暗号化処理が行われる。ストリーム暗号というのは、データを分割し、分割したデータを暗号化する際に計算された値が、次の暗号化処理の鍵などに影響するというものだ。復号化する際には、当然暗号化とは逆の処理が同じように行われる。 eexec 暗号化では1バイトごとに処理が行われる。先程の暗号文はASCII-Hex形式で表現されているため、2文字で1バイトのデータをを表している。例えば先頭の 74 は16進数で表すと 0x74 、10進数で表すと 116 という1バイトのデータを表現している。要は2文字で1バイトを表している。そこで2文字ずつに分割してみよう。

暗号文 暗号文 暗号文
ASCII-Hex 16進数 10進数
74 0x74 116
3F 0x3F 63
84 0x84 132
13 0x13 19
F3 0xF3 243
63 0x63 99
6C 0x6C 108
A8 0xA8 168
5A 0x5A 90
9F 0x9F 159

これを1行ずつ復号化する。

まず初期化処理として次を行う。

  1. c1に 52845 、c2に 22719 を設定する。この値は変化しない。
  2. rに 55665 を設定する。この値は復号化のための鍵の初期値であり、復号化処理が進むにつれて変化していく。

初期化処理で設定したc1、c2、rを使用し、次の手順で復号化を行う。

  1. cに暗号文を設定する。
  2. 暗号文cと鍵rの上位8ビットのXORを計算する。これが平文pとなる。
  3. 暗号文cと鍵rを加算し、c1を乗算し、c2を加算する。この数値の範囲を16ビットに抑える(ここでは 0xFFFF とのANDを計算している)。この値をrに設定し、次の復号化の鍵とする。

Pythonで簡易な実装をすると次のようになる。

c1 = 52845
c2 = 22719
r = 55665

c = 0x74  # 暗号文
p = c ^ (r >> 8); r = ((c + r) * c1 + c2) & 0xFFFF; print(f"{p} | {r}")

cの値を入れ替えながら、先程の表に復号化したデータとその際の鍵を追記する。

暗号文 暗号文 暗号文 復号文 復号文 次の復号鍵
ASCII-Hex 16進数 10進数 10進数 ASCII 10進数
74 0x74 116 173 (非ASCII) 25920
3F 0x3F 63 90 Z 49618
84 0x84 132 69 E 19293
13 0x13 19 88 X 38767
F3 0xF3 243 100 d 5753
63 0x63 99 117 u 7275
6C 0x6C 108 112 p 41546
A8 0xA8 168 10 (LF) 30153
5A 0x5A 90 47 / 53158
9F 0x9F 159 80 P 35872

最初の4バイトはデータの形式(ASCII-Hexかバイナリか)を判定するためのダミーの値を設定するという使用になっている。そのため最初の4バイトは捨てる。

5バイト目以降を実際に記述すると次のようになる。

dup
/P

元のファイルを T1utilst1disasm を使って復号化し、暗号化されていたであろう箇所を確認すると次のようになっている。

dup
/Private 8 dict dup begin

/PrivateP の文字まで、正しく復号化できている事を確認できた。

まとめ

フォントについての知識を学ぶために、古くから使われていた Type 1 フォントを取り上げ、Ghostscriptでの使い方とその内部の仕組みについて まとめた。 Type 1 フォントを操作するための T1Utils を使ったり、 eexec 暗号化されている部分を手作業で平文に復号化し、仕組みを理解した。フォントについて、少しだけ詳しくなった。まだ Type 1 について書ききれていない所もあるので、時間を作れたら追記していきたい。


2

まあEmacsの場合、頑張ればあらゆるファイル(例え実行可能ファイルであっても)を作れる。ただ今回は理解を深めるために手作りしているのであって、通常はそうするべきではない。