和田研フォントキットの肉付け処理を解き明かす - (1) -

efont-develにおける狩野さんの投稿がきっかけで和田研フォントキットに興味を持つようになった。和田研フォントキットとは文字の構成データを元にフォントを自動生成するものだ。オリジナルの和田研フォントキットはUtiLispというマイナーLisp処理系で実装されていたため、狩野さんがこれをCommon Lisp上に移植されたようである。efont-develでのメールを参考にしながら、フォントを生成し任意の文字をpsファイルとして出力できる所まではこぎつけた。

(setq my-name (list '太 '田 '一 '樹))
(out-to-ps-all (reverse my-name) 'mincho "name.ps")

名前の出力

うーむ、「樹」の字が汚いなぁ。しばらくソースを眺めている内にスケルトンに対する肉付け処理の部分が非常に気になったので、CodeBlogの練習も兼ねて読んでみる事にする。また、この部分のコードはECCSでお会いした田中先生のお書きになったコードだからという理由もある。言ってはなんだが、和田研フォントキットのコードの質は究めて低いと思う。コメントもロクにないし、データ構造も結構複雑だ。正直コードを読むのは萎えるのだが、まぁ頑張ってみよう。分からなかったら田中先生に直接会うっていう逃げの一手を打てるしな(今年からECCSの相談員を始めた)。


まずは肉付け処理を行っている箇所を特定する所から始める。迷わずgrep作戦だ。

debian% pwd
/home/kzk/dev-font/wadalab-fontkit/renderer
debian% grep 肉付け *
apply.l:; スケルトンへの肉付けを行う
apply.l:    (do ((ll lines (cdr ll)))   ; まだ肉付けしていないストローク全部
apply.l:      ; 各ストロークの肉付け結果を得る前にグローバル変数 local_gothicwidth,
apply.l:      ; ストロークに (defelement で定義された) 肉付け関数を適用する
lib.l:; 補間・肉付け処理
lib.l:; 線分 p0-p1 の周りに幅 width の肉付けを行う
lib.l:; 端点を指定されたエレメントに肉付けを行う lambda 式をシンボルの属性リストに
test.l:;; それを 400×400 にバランスよく収まるように調整し、肉付けに不要な情報を除去

apply.lのskeleton2list関数がそのものズバリの様だ。見た感じcar, cdrを多用するLispバリバリなソースコードなので、まずはデータ構造を把握する事が重要だ。skeleton2listに渡ってくる第一引数lの内容をprint関数を使用して表示させてみた。ここでサンプル文字として数字の「六」を選んだ。理由としては、縦・横・左斜め・右斜め・リンク(線と線の交わり)を全て含んでいる為だ。

(((17.008724 94.82636) (383.90768 94.82636) (201.42883 93.091 (LINK-OK T))
  (201.42883 15.0) (135.34949 172.92671) (98.54841 289.2855) (15.000006 385.0)
  (230.83337 169.1732) (263.65594 285.53195) (353.17212 383.1232))
((YOKO (0 1) (LINK 2)) (TATE (3 2)) (HIDARI (4 5 6)) (MIGI (7 8 9)))
(YUNIT . 281.51324) (XUNIT . 146.0526) (XLIMIT 0 400) (CENTER . 200.0)) 

ぱっと見、car部分が点のリスト・cadrが線のリストである様に見える。cddrは何かのパラメータの様だ。これを踏まえてskeleton2list関数を読み解いて行くことにする。全体的に見て明らかにコメントが少ないので少し萎えるが、頑張ってみよう。

in apply.l

;                                                                                                                                             
; スケルトンへの肉付けを行う                                                                                                                  
;                                                                                                                                             
;   l: (展開済みの)スケルトン定義                                                                                                             
;   tag: 書体名 ('mincho, 'gothic, 'maru)                                                                                                     
;                                                                                                                                             
(defun skeleton2list (l tag)
  ; 仮想的なエレメント xlimit, ylimitを取り除く                                                                                               
;  (setq l (rm-limit l))                                                                                                                      
  ; 書体固有のスケルトン変形関数が定義されている場合は呼び出す                                                                                
  (let ((func (get-def 'typehook tag)))
    (and func (setq l (funcall func l))))
  (let ((linkpoints nil)
        (linelist nil)
        (outline nil)
        (points (floatlist(car l)))
        (part nil)(type nil)(cpoint nil)(lpoint nil)(partpoint nil)
        (widthratio nil)(flag nil)(link nil)(part1 nil)(part2 nil)
        (tmpline nil)(type3 nil)
        (type1 nil)(type2 nil)(cross nil)(kazari nil)(part3 nil)
        (lines (cadr l)))
    ; 配列linkpointsの初期化                                                                                                                  
    ; ((2 (link)) (1 (link)) (0 (link))) のようになる。                                                                                       
    (do ((ll points (cdr ll))
         (linkcount 0 (1+ linkcount)))
      ((atom ll))
      (push (list linkcount (list 'link)) linkpoints))
    (print linkpoints)

最初の二行は特殊処理であるように思われるので飛ばす。次のletの初期化初期化処理で重要なのはpoints変数とlines変数だ。それぞれ先ほどのデータ構造のcar部分、cadr部分が格納されている。つまり、先ほどのデータ構造の推定が正しい事が分かった。


次はlinkpointsの初期化。コメントの通りだ。Common Lispは始めてだったが、do文の役割を理解したらこの部分は簡単簡単。てか、コメントに書いてあるし。数字の「六」の場合は十個点が有ったので次のようになる。

((9 (LINK)) (8 (LINK)) (7 (LINK)) (6 (LINK)) (5 (LINK)) (4 (LINK)) (3 (LINK))
 (2 (LINK)) (1 (LINK)) (0 (LINK))) 

次に進んでみよう。少し長くなる。

    (do ((ll lines (cdr ll)))   ; まだ肉付けしていないストローク全部                                                                          
      ((atom ll))
      (setq part (car ll))      ; 1 本のストローク                                                                                            
      (setq type (car part))    ; ストロークのタイプ (ten, yoko, tate, ...)                                                                   
;      (setq npoint (get type 'npoint))                                                                                                       
      (setq cpoint (cadr part)) ; ストローク(折れ線)を構成する点番号のリスト                                                                  
      (setq lpoint (assq 'link (cddr part))) ; リンク点の点番号のリスト                                                                       
      (setq lpoint (cond (lpoint (cdr lpoint))))
      (setq partpoint nil)
      ; ストロークを可変幅にするための変更 (2005-04-30)。                                                                                     
      ; 各ストロークの肉付け結果を得る前にグローバル変数 local_gothicwidth,                                                                   
      ; local_minchowidth を一時的に書き換えるという ad hoc な対処法による。                                                                  
      (setq widthratio (cdr (or (assq 'widthratio (cddr part))
                                '(widthratio . 1.0))))
      (setq local_gothicwidth (* gothicwidth widthratio))
      (setq local_minchcwidth (* minchowidth widthratio))

      ; partpoint に、ストロークを構成する点の座標を順に並べたリストを格納                                                                    
      (do ((lll cpoint (cdr lll)))
        ((atom lll))
;       (push (point-n  (car lll) points) partpoint)                                                                                          
        (push (nth (car lll) points) partpoint))
      (setq partpoint (nreverse partpoint))
      ; ストロークに (defelement で定義された) 肉付け関数を適用する                                                                           
;; tag に対するプロパティが未定義のときのため(石井)                                                                                           
;; if を使わないように直す(田中)                                                                                                              
      (setq tmpline
            (let* ((funcname (get-def type tag))
                   (result (cond (funcname
                                  (funcall funcname
                                           partpoint(cddr part)))
                                 (t
                                  (print (list 'undefined tag))
                                  (funcall (get type 'mincho)
                                           partpoint(cddr part))))))
              `(lines ,result)))


この部分は冒頭のdo文から分かる様に一本一本の線について処理している事が分かる。一本のストロークは、(type (cpoint lpoint) hogehoge)という形で現される事がlet文の初期化処理で分かる。コメントに書いてあるように、cpointは線を構成する点のリストでlpointはリンク(結合点)のリストである。先ほどのデータ構造と見比べればはっきりと理解できるだろう。


次のブロックは4-30日(おととい)に追加されたadhocなコードの様なので、パス。


次にpartpointに点のデータを格納する。cpointには何番めの点かという情報しか入っていないので、それを実データに変換する。例えば(0 1) を( (17.008724 94.82636) (383.90768 94.82636)) に変換する。


最後のブロックはとりあえずの肉づけを施している部分であるようだ。typeとtagから対応する関数を取得し、それを実行しているようだ。例えばtype = YOKO && tag = minchoの場合、mincho.lで定義されている (defelement mincho yoko)の部分のコードが実行されるのを確認した。resultを表示させてみた所、次のようになった。

(LINES
 (((ANGLE 17.00909 107.826355) (ANGLE 383.90402 107.82636))
  ((ANGLE 17.00909 81.826355) (ANGLE 383.90402 81.82636)))) 

この部分でなんらかの変換を行っているのは間違い無いので、apply.lは一時置いておいてmincho.lの(defelement mincho yoko)の部分のコードを追う事しよう。

in mincho.l

(defelement mincho yoko
  (let* ((dotsize (meshwidth (* mw tateyokoratio)))
         (w (normwidth dotsize))
         (p0 (gridy (car points) dotsize))
         (p1 (gridy (cadr points) dotsize)))
    (line2 p0 p1 w)))

ぱっとみ重要そうなのはline2関数だ。他は些細な部分とみた。line2関数はlib.lで定義されている。

;                                                                                                                                             
; 線分 p0-p1 の周りに幅 width の肉付けを行う                                                                                                  
;                                                                                                                                             
(defun line2 (p0 p1 width &optional (dlist '(nil nil)))
  (let* ((diff (diff2 p1 p0))
         (l0 (normlen2 width (rot270 diff))))
    `(((angle .,(plus2 p0 l0))
       (angle .,(plus2 p1 l0))
       .,(car dlist))
      ((angle .,(diff2 p0 l0))
       (angle .,(diff2 p1 l0))
       .,(cadr dlist)))))

ビンゴ。grepをかけてみた所、diff2, plus2関数は同じくlib.lで定義されている関数で、単なるベクトルの引き算、足し算の様だ。同じくrot270はベクトルを-90度回転する関数とのコメントが有る。これを踏まえてこのコードを眺めると、肉づけ処理が見えてくる。具体的には次の図の様になっている。



p0 -> p1ベクトルに直行する長さwidthのベクトルl0をp0, p1から足し引きする事で、長さwidthの肉づけを直線p0 -> p1に行った時の四隅の座標が求まるという訳だ。


この様にして作成されたtmplineを以下のようにlinelistにpushし、保存しておく。

(push tmpline linelist)

(続く)