Converting TrueType Font into EPS
Please feel free to link to this page.
TOP PDF ver.
Powered by SmartDoc

Converting TrueType Font into EPS

初版 2003/7/1
Shohei NOBUHARA
TrueTypeフォントから任意の文字のアウトライン形状を取得し,epsで保存する.TeXと組み合わせると,フォントの見本帳作成などが可能になる.

※数式を正しく表示するには IE6 + MathPlayer プラグインか, Mozilla firefoxなどの Gecko エンジン搭載ブラウザが必要です. なお MathML を両ブラウザ向けに適切に変換するために,ASCIIMathMLに含まれる javascript コードを使用しています.

目次

1 はじめに

TrueTypeフォント[1][2][3]からアウトラインを取得し,epsで保存する.

2 アウトライン形状のデータ構造

TrueTypeフォントでは,そのアウトライン形状を2次のBスプライン曲線で表す.一方,Postscript系(Type1フォントなど)では,3次のBezier曲線でアウトライン形状を表現する.またOpenTypeでは,その内部形式としてType1, TrueType共に選択できる.従ってepsとしてTrueTypeフォントのアウトライン形状を保存するには,2次のBスプライン曲線を,3次のBezier曲線で表さなくてはならない.

まず2次のBスプラインとは,3つの制御点

P 0 , P 1 , P 2 (2.1)

を用いて,

P ( t ) B 2 , 0 ( t ) P 0 + B 1 , 1 ( t ) P 1 + B 0 , 2 ( t ) P 2 (2.2)

と定義される.ここで

0 t 1 (2.3)

であり,また

B n , m ( t ) n ! m ! ( n - m ) ! t m ( 1 - t ) n - m (2.4)

である.

一方,3次のBezier曲線とは,4つの制御点

Q 0 , Q 1 , Q 2 , Q 3 (2.5)

を用いて,

Q ( t ) B 3 , 0 ( t ) Q 0 + B 2 , 1 ( t ) Q 1 + B 1 , 2 ( t ) Q 2 + B 0 , 3 ( t ) Q 3 (2.6)

と定義される.

従って,

P ( t ) = Q ( t ) (2.7)

より,

Q 0 = P 0 (2.8)
Q 1 = 1 3 P 0 + 2 3 P 1 (2.9)
Q 2 = 2 3 P 1 + 1 3 P 2 (2.10)
Q 3 = P 3 (2.11)

とすれば,2次のBスプラインと等価な3次のBezier曲線が得られる.

3 パスの塗りつぶしと内外判定規則

たとえばアルファベットのBという文字から,そのアウトライン形状を取得すると,外側の輪郭1つと内側の輪郭2つ,計3つの閉じたパスが得られる.

この3つの閉じたパスから,正しくBという文字をレンダリングするには,外側の輪郭を表すパスは黒く塗りつぶし,内側の輪郭を表すパスは白く塗りつぶす(あるいはくり抜く)というように,適切に内外を判定し,塗りつぶす色を設定する必要がある.つまり,文字のアウトライン形状を複数の閉じたパスとして取得した場合,そのパスの内外を判定するアルゴリズムも併せて知る必要がある.

このような内外判定法,つまりある1つの閉じたパスが与えられ,平面がそのパスによって複数の領域(1)に区切られたとき,どの領域が内側であるのかを決める方法には,「非ゼロ巻数規則(non-zero winding number rule,非ゼロ規則)」と「偶奇規則(even-odd rule)」がある.一般に非ゼロ巻数規則の方がより多くの塗りつぶしを表現できることが多い.

  1. 5つ星を一筆書きした場合のように,パスが交差する場合には2つ以上の領域に分割されうる.

3.1 非ゼロ巻数規則

調べたい領域中の任意の1点から,任意の方向に向かって線をのばしたとする.このときに,0から数えはじめて,左から右へとパスが線を横切るならば+1,右から左へとパスが線を横切るならば-1とする.こうして無限遠までパスとの交差関係を調べ,最終的に0であれば,その領域は外側,0以外であれば内側とされる(2)

たとえば大きさの異なる同心円,すなわちドーナツがあったとき,内側と外側の円が同じ向き(たとえば時計回り)に描かれているならば,ドーナツの穴は内側と判定され,逆の向きに描かれているならばドーナツの穴は外側と判定される.従って,ドーナツの穴をくり抜くには,2つの円は互いに逆の向きに描く必要がある.

Postscriptでは,fill命令がこれに相当する.

  1. パスと交差せずに接してしまった場合は,別の点や方向を使って判定する.

3.2 偶奇規則

非ゼロ巻数規則と同様に,調べたい領域中の任意の1点から,任意の方向に向かって線をのばしたとする.このとき,パスと交差する回数が偶数であれば外側,奇数であれば内側と判定する.

この規則を用いると,ドーナツのような形状は,内側の円と外側の円を描く向きによらず,常に穴がくり抜かれる.

Postscriptでは,eofill命令がこれに相当する.

4 Javaによる実装

以上をふまえ,Java[4]による実装を行う.Javaでは,java.awt.Fontクラスを使用することでフォントファイルから簡単にアウトラインをjava.awt.Shapeとして取得することができる.

なお以降では,

import java.awt.Font;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
import java.io.FileInputStream;

とする.

まずあるフォントファイルfont.ttfを読み込むには,

FileInputStream fin = new FileInputStream( "font.ttf" );
Font font = Font.createFont( Font.TRUETYPE_FONT, fin );
fin.close();

とする.

次にある文字列のアウトラインを取得するには,

GlyphVector glyph
  = font.createGlyphVector(
      new FontRenderContext(new AffineTransform(), false, false),
      "some text"
    );
Shape outline = glyph.getOutline();

とする.

あとはShapeからPathIteratorを取り出し,

PathIterator iter = outline.getPathIterator(null);
int type;
float coords[] = new float[6];
Postscript の newpath を発行
while (!iter.isDone()) {
  type = iter.currentSegment(coords);
    switch (type) {
    case PathIterator.SEG_CLOSE :
      Postscript の closepath を発行
    case PathIterator.SEG_CUBICTO :
      Postscript の curveto を発行
    case PathIterator.SEG_LINETO :
      Postscript の lineto を発行
    case PathIterator.SEG_MOVETO :
      Postscript の moveto を発行
    case PathIterator.SEG_QUADTO :
      2 次 B スプラインを Postscript の curveto に変換して発行
  }
  iter.next();
}
switch (iter.getWindingRule()) {
case PathIterator.WIND_EVEN_ODD :
  Postscript の eofill を発行
  break;
case PathIterator.WIND_NON_ZERO :
  Postscript fill を発行
  break;
default :
  エラー
  break;
}

のようにして,順次Postscriptに変換すればよい.見ての通り,JavaのGraphic関連の命令はPostscriptに強く影響を受けているため,ほぼ1対1に変換することができる.ポイントは,SEG_CLOSEのときにfillやeofillを発行するのではなく,単にclosepathによってパスを閉じるだけとし,最後にまとめてfill / eofillをする点で,先に述べたように,1つ1つの閉じたパスを見ただけでは,塗りつぶすのかくり抜くのかを判断できないからである.GlyphVector#getOutlineメソッドでは,"文字"ではなく"文字列全体"のアウトラインが取得できるが,このように複数の文字があったとしても,先の判定規則では正しく判定できる.

なお上の例では省略しているが,PathIteratorのSEG_CUBICTOでもPostscriptのcurvetoと同様に,"前回の終了点+今回の3点"で4つの制御点を構成する.これはSEG_LINETOもSEG_QUADTOも同様である.このうち,Postscript命令にそのまま対応するSEG_CUBICTOやSEG_LINETOでは特に問題ないが,SEG_QUADTOの場合はcurvetoに変換する際に"前回の終了点"から第2制御点(curvetoを発行するときにスタックの3番目に入っている点)を計算する必要があるため,結果的に常に"前回の終了点"を記憶しておく必要がある.

以上でTrueTypeのアウトラインを出力するコア部分は完成なので,あとはeps形式となるように先頭にヘッダとして

%!PS-Adobe-2.0 EPSF-1.2
%%BoundingBox: 0 0 20 20
gsave

などを付け,最後に

grestore
showpage

とすればよい.バウンディングボックスの大きさは,

Rectangle2D bbox = glyph.getVisualBounds();

としてGlyphVectorのインスタンスから取得できる.ただしPostscriptでは用紙の左下が原点で,右と上がXおよびY軸の正の方向なので,実際には

(int)Math.floor(bbox.getX()),
- (int)Math.ceil(bbox.getHeight()) - (int)Math.floor(bbox.getY()),
(int)Math.ceil(bbox.getWidth()),
(int)Math.ceil(bbox.getHeight())

の4つの値を順に指定することになる.

なお,フォントの大きさを変更するには,JavaレベルでFont#deriveFont()メソッドを使用することもできるが,Postscriptレベルでscale命令を使用してもよい.いずれにせよ,ベクタ形式であるため,スケーリングは大きな問題ではない.

5 Windows APIによる実装

Windowsの場合,GetGlyphOutline関数を用いることでフォントのアウトラインを取得できる.

参考文献

[1]Apple Computer, Inc. TrueType Reference Manual - The Font Engine. http://developer.apple.com/fonts/TTRefMan/RM02/Chap2.html
[2]TrueType Typography - info about TTF fonts and technology. TrueType Outlines. http://www.truetype.demon.co.uk/ttoutln.htm
[3]安沢 勝昭. フォント入門. http://www.interq.or.jp/www1/anzawa/doc/fontguide.htm
[4]Sun Microsystems, Inc. Java 2 Platform, Standard Edition. http://java.sun.com/