普段フォントを使う時、フォントがある事が当たり前過ぎて使っている事を気にも止めないし、最近のツールを使う時にフォントが原因で困った状況になる事はあまりない。しかしTeXなど古くからあるツールを使う時には、読み込まれなかったりエラーしたりと、問題が発生する事がある。そんな状況にひとたび陥ると、フォントの基本的な知識が無さ過ぎてトラブルを解決できない。フォントの知識がないというのは、プログラマーとしてはやはり問題だろう。そこで今回はフォントがどういうものなのか、手で作りながら学ぶ事にした。
フォントの形式はいくつかある。最近では OpenType
がよく使われる。しかし今回はフォントの基本的な考え方を学びたい。そこでもはや使われる事のなくなった PostScript Type 1
フォントを扱う事にする。
フォントは既にインストールされていたり、どこかからダウンロード、インストールをして利用する事が多いだろう。文字の見た目にこだわりたい場合、状況によってはフォントを自作する事もある。
独自フォントを作る場合、際、フォントを作成したり、形式を変換したりするツールを使う事になる。ここではType 1フォントを扱うためのツールを紹介する。
なお通常は、間違っても今回のようにフォントをテキストエディタで記述したりはしない。
オープンソースかつクロスプラットフォームなフォント編集ソフトウェアであり、様々な形式のフォントを作る事ができる。今回も最初の検証で使用するフォントの制作にはFontForgeを使う。
T1utilsはType 1フォントを扱うためのツールが実装されている。今回は、Homebrewでビルド済みバイナリをインストールする。
brew install 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
Type1
の仕様は後で眺める事になるが、何も知らず例もない状態で仕様書を読んでも理解する事はなかなか難しい。そこであらかじめ例となる小さなフォントをFontForgeで作る事にする。
まず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は所定の位置にフォントファイルを配置するが、今回は恒久的に使うものではないため、作業ディレクトリに配置し環境変数 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画像が印字される。
仕様の公開については一悶着あったようだが1、仕様を確認したければ現在は https://adobe-type-tools.github.io/font-tech-notes/pdfs/T1_SPEC.pdf にType 1の仕様に関する文書が配置されている。今回はこの仕様書を適宜参照しながら形式を確認する。
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
フォントは必ず Private
と CharString
の辞書を含むが、2つの暗号化層はこれらを暗号化する。これは簡易な解析から、Private辞書内のヒント情報を部分的に保護する事を目的としている。また暗号文をASCII Hex形式とする事で、7ビットASCIIしか使用できないような通信チャネルでもデータを送信できる。
ここでは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行ずつ復号化する。
まず初期化処理として次を行う。
52845
、c2に 22719
を設定する。この値は変化しない。55665
を設定する。この値は復号化のための鍵の初期値であり、復号化処理が進むにつれて変化していく。初期化処理で設定したc1、c2、rを使用し、次の手順で復号化を行う。
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
元のファイルを T1utils
の t1disasm
を使って復号化し、暗号化されていたであろう箇所を確認すると次のようになっている。
dup
/Private 8 dict dup begin
/Private
の P
の文字まで、正しく復号化できている事を確認できた。
フォントについての知識を学ぶために、古くから使われていた Type 1
フォントを取り上げ、Ghostscriptでの使い方とその内部の仕組みについて
まとめた。 Type 1
フォントを操作するための T1Utils
を使ったり、 eexec
暗号化されている部分を手作業で平文に復号化し、仕組みを理解した。フォントについて、少しだけ詳しくなった。まだ Type 1
について書ききれていない所もあるので、時間を作れたら追記していきたい。
まあEmacsの場合、頑張ればあらゆるファイル(例え実行可能ファイルであっても)を作れる。ただ今回は理解を深めるために手作りしているのであって、通常はそうするべきではない。