SVG画像の分割スクリプトをPythonに移植した話 - SVG Advent Calendar 2014-


この記事は、SVG Advent Calendar 2014の参加記事です。前回(20日)は @hashcc さんの「東京の鉄道路線図SVGを作りました&パブリックドメインで配布します」でした。
@hashccさんの力作の翌日を担当することになってしまいましたが、微力ながら相応の記事になるよう力を尽くす所存です。

今回は、ルーン文字フォント制作に使用している手製のSVG画像の分割スクリプトを使うことになった経緯、その再実装(リファクタ)に至る過程、そしてその結果について書かせていただきます。

RuneAMN_Pro_Blackletterより


オリジナルフォント作りと、SVG画像の分割スクリプト

わたしが現在公開しているRuneAMNシリーズフォントは、イラスト・デザイン向けに作られたルーン文字フォントです。普通のアルファベットを入れてフォントを適用するだけで簡単に、ルーン文字をデザインに使えるようになっています。
シリーズ構成は、独立したFree版とPro版(有償)があり、それぞれ重複しない独自の書体を収録しています。
他に、Project "daisy bell"の製品として、OlChikiAMN(オルチキ文字フリーフォント)も配布しています。

これらのフォントを制作する際、Illustratorで書体デザインを行っています。そして、SVG画像への書き出しを経由して、FontForgeに読み込ませてフォント化しています。
なぜフォントエディタの編集機能で完結させず、複雑なビルド手順を踏む選択をしたかといえば、FontForgeよりIllustratorのほうが、ベクタ画像のエディタとしては使い慣れており高機能だったという点に尽きます。
また、フォントのデザインの中には、フォントエディタでなければ事実上デザインが不可能なもの(Multiple Masterなど)もありますが、今回はそういったフォント機能を使う予定がなかったため、この手法を選択することができました。

SVG分割スクリプト(svg_splitter.pl)の機能拡張

フォントの制作プロセスにおいて、『すべてのグリフ(文字)を一枚のSVG画像に描いて、後から自動分割したい』という要求がありました。
すべてのグリフを1枚の画像に描きたかったのは、そのほうがファイルの管理が簡単で、デザインを統一しやすいためです。
自動分割というのは、スクリプトにより1枚の画像から、1文字につき1ファイルの状態に分割することができる、ということです。SVG画像の分割を、makeを使ったビルドプロセスに含めるために必須でした。

OlChikiAMN_Series_Fontsより


ところで、SVG画像の圧縮は熱心に研究されている一方で、SVG画像の分割は 、あまり試みられていないようです。
私が見た限りでは、

・Illustrator(スクリプト)は、アートボードで範囲を区切って書き出しても、範囲外の表示されないすべての要素もSVGファイルに含めたまま出力する。
・ InkScapeにはスクリプト化の方法が2種類ある。
  コマンドライン引数の方法では、詳細な制御ができない。
  Python拡張版(inkey)ではなぜかLGPL汚染が懸念されており、さらにAPIの網羅されたドキュメントを見つけることができない。
・Perl、PythonにはSVGを処理する多くのライブラリが存在するが、そのほとんどが「SVGを圧縮」するか「SVGをラスタ画像に書き出す」機能しか持っていない。
  (参考として、PythonのSVG関連ライブラリを調べた方が作った一覧がこちら。 )

結果的に、SVG画像を画像として扱う"まともな"分割器を見つけることができませんでした。

見つかったのは、mashabow@しろもじ氏の手書き文字「てきとうに書いてつくったフォント」用SVG分割器と、M+フォントのビルドシステムに含まれているSVG分割器でした。
このどちらも、SVGを画像ではなくXMLテキストとして解析した後で、SVG要素を独自に計算することでSVG画像の分割を実現しています。
M+の分割器は、SVGの多くの要素に対応していた反面、日本語フォント1万2000文字オーバーをすべて分割できる機能を持たせるために、複雑な構造となっています。(2ファイルに分割されており、内部動作もマルチスレッド化などがされている)。
一方、手書き文字フォントの分割器は、対応要素がpolygonだけである代わりに、1ファイルに完結しており動作も容易に追うことができました。
以上のことから、RuneAMNでは、手書き文字フォントの分割器を、拡張しながら使っていくことにしました。

リファクタ・PerlからPythonへの移植

このスクリプトは、RuneAssingMN_Free、OhChikiAMN_Free、RuneAMN_Proの制作に、機能拡張しながら使われました。ほぼすべての拡張が「その場しのぎのやっつけ仕事」で行われました。そのため、コードベースは混乱を極めており、コピーペーストされた同じ処理がいくつもの場所に分散しており、機能拡張とバグ修正は困難を極める状態になっていました。
例:
・SVG要素を切り出す際、配置位置の計算は、分割サイズで割って余りを出すだけ
 (マイナス値など、グリフ形状が文字の枠からはみ出す部分を持つことを考慮していない)
・分割判定部は、SVG要素の数だけコピーペーストした
 (polygon/rect/circle etc...)

控えめに言ってこのスクリプトは公開することで私の(あって無いような)キャリアに傷をつけるのではないかと危惧するほど汚いfixやその場凌ぎのコードで満ちている。しかし、スクリプトがたとえ飛び出したクジラの内蔵の干物のようであったとしても、生成されたフォントの美しさはカケラも損なわれない。グリフのデザインは、1/10ピクセルも、まったく、狂わないからだ。ならばリファクタリングは時間の無駄と言える。
(Pythonによる再実装を行う前の、本記事の原稿より。)

もちろん、コードが汚いからといって、フォント自体は1ピクセルも変化したりはしないことは誰でもわかっています。コードが綺麗であろうがクジラの内蔵が飛び出した状態であろうが作られたフォントの美しさは全く損なわれることがないことは間違いありません。

しかし実のところ、svg_splitter.plの次の機能追加やバグフィックスは困難を極めるだろう。私は有名な「すべてを一から書き直したい病」にかかっており、誰かがこのスクリプトを使いたいと言ったならば、私は間違いなくすべてを書き直す。そしてその時はPythonを使う。フォントプログラミングとPythonは相性がよく、例えばFontForgeはネイティブ言語とは別にPythonスクリプトによる制御を提供している。また、PythonにはPerlと同じように、SVGのベースであるXMLを処理するためのライブラリがあらかじめ用意されている。余録として、将来Windows環境をサポートする際に、FontForgeのWindowsポート版がPythonインタプリタを持っていることが期待できる。
(Pythonによる再実装を行う前の、本記事の原稿より。)

RuneAMN_Free/Proのアップデートを決意した私は、そのためにはSVG分割スクリプトに新機能が必要であると判断しました。
そこで、以前から考えていた、リファクタを兼ねたPythonへの再実装を行うことにしました。




リファクタリングの結果

再実装中のPythonスクリプト版


Pythonへの移植を行ったため、当然ながら、すべてのコードが1から書きなおされました。
リファクタリングでは「新しい機能はひとつも追加しない」のが鉄則ですが、今回はいくつかの新機能・機能改善を含みます。
以下が、リファクタ作業の結果です。

・見通しの良いコード
   ・すべての機能は関数に分割された。
   ・新しいmain部は20行ほど
   ・新フォントのための新機能を容易に追加できる
   ・transform属性を処理する部分が関数に共通化された。
   ・アプリケーション・ハンガリアン記法になった。
リファクタリング以前、SVG分割器の機能は関数化されていませんでした。
度重なる機能追加により同じ機能があちこちに散らばり、処理の流れが簡単にはわからない状態になっていました。
また、変数名や関数名にもルールらしいものはなかったため、可読性が低く処理を追いかけることが困難になっていました。
リファクタリングの後、多くの部分が適切に関数化され、処理を以前より容易に把握することができるようになりました。
アプリケーション・ハンガリアンは(リファクタリングと共に)Joel on Softwareの受け売りですが、命名規約があることにより以前よりコードが読みやすくなったことは確かです。

・引数の処理を改善。プログラム内部でも、設定値が読みやすくなった。
・SVG要素の再配置の計算を正しい方法に直した
 「分割サイズで割って余りを出す」のではなく、「(分割セル上の行列位置×分割サイズ)を現在位置から引く」ことで、分割セルからはみ出したポイントなどを正しく分割後再配置できるようになりました。
・グループ要素(<g>)を再帰的に探索できるようになった。
 perl版では、対象1グループの中に含まれる(つまり1レイヤーの)SVG要素しか対象にすることができませんでした。
 そこで、グループを再帰的に探索するよう機能拡張しました。
 IllustratorやInkScapeで、書き出し前にレイヤ統合やグループ化解除をする必要がなくなった。
・ついでにいくつかの使っていないpolygonパスのコマンドに対応できた
  ほかの未実装部も容易に追加できるはずです。
・RuneAMN_Proを、.pl版と同じように、すべて問題なくビルドできる。
 汎用であることを心がけてはいますが、一方でこのスクリプトはdaisy bellのフォントプロジェクト専用であり、使われない機能は未実装のまま省略することができます。
 Python版がPerl版とまったく等価な出力が可能で、退行(リグレッション)していないことを確認できました。


また、余録として、
・分割スクリプトを通して、SVGの構文について学習することができた
・Python(Python3)処理系について学習することができた
PythonによるSVGの加工を学ぶことで、将来的にもっと高度なフォントデザイン支援を実現するための、足がかりにしようと考えています。
また、SVGはWebデザインなどの分野でこれからより使われるようになると思われ、理解しておいて損はないはずです。(そしてこれはSVG Advent Calender 2014の記事です。忘れてしまいそうになりますが。)

・SVG分割スクリプトのすべてを、自分でコントロールすることができる
機能追加のために行ったことにリファクタリングにより、フォント制作でしか使われないであろうSVGへの処理を、ビルドのコアに含めることが簡単にできるようになりました。これはフォント自体に影響を与えるものではないので、
『それがビジネスの核となる機能なら−−何が何でも自分でやることだ』
(これもJoel on Software ただし書籍)
の系になるのかどうかはわかりませんが。

ソースコードなど

main部分は、たったのこれだけになりました。
実際のコード部分は20行以下になっています。

if __name__ == '__main__':
 
args = docopt(__doc__)

 
# 設定ファイルを解析する
 
settings = getSettingsFromSettingFilePath(args["<listfile>"])
 
settings['output_dir'] = args['--output_dir']
 
pprint(settings)
 

 
# SVGファイルをxmlとして読み込む
 
tree = ET.parse(args["<src_image>"])
 
rootSvg = tree.getroot()
 
# SVGファイルの縦横サイズを取得する
 
widthSvg = getNumericFromStringHead(rootSvg.get("width"))
 
heightSvg = getNumericFromStringHead(rootSvg.get("height"))
 

 
# Todo: プリプロセスとして、非表示のグループとレイヤ名'*_ignoreSSSMN'を持つレイヤを除去
 
# Todo: 透明なsvg要素も除去する。タイミングは未定。


 
# SVGパスをレイヤ(グループ)ごとに分割器にかける(rootにも要素があるのでレイヤ扱いする)
 
dstsTable = [[0 for i in range(settings['col'])] for j in range(settings['row'])]
 
ravelGroup(dstsTable, settings, rootSvg)

 
writeSvgs(dstsTable, settings)


SVG要素の読み出し部分も、以下のように関数化され、整理されました。
さらに、再帰呼出しを使うことで複数レイヤに対応しました。

"""
 @brief xmlツリーからSVG要素を再帰的に検出して分割処理する
 @param 分割済みデータ連想配列
 @param 設定ファイル情報(分割設定)
 @param xmlツリー(グループ要素)
 @return 分割済みデータ連想配列

 
 
 
(Todo:エラー処理)
"
""
def ravelGroup(dstsTable, settings, svgGroup):
 
svgTransform = svgGroup.get('transform')
 
if svgTransform:
 
 
print('warning: not inplement transform attr.')
 

 
elems = list(svgGroup)
 
for elem in elems:
 
 
if'{http://www.w3.org/2000/svg}g' == elem.tag:
 
 
 
dstsTable = ravelGroup(dstsTable, settings, elem)
 
 
elif '{http://www.w3.org/2000/svg}path' == elem.tag:
 
 
 
dstsTable = ravelPath(dstsTable, settings, elem)
 
 
elif '{http://www.w3.org/2000/svg}rect' == elem.tag:
 
 
 
dstsTable = ravelRect(dstsTable, settings, elem)
 
 
elif '{http://www.w3.org/2000/svg}polygon' == elem.tag:
 
 
 
dstsTable = ravelPolygon(dstsTable, settings, elem)
 
 
elif '{http://www.w3.org/2000/svg}polyline' == elem.tag:
 
 
 
dstsTable = ravelPolyline(dstsTable, settings, elem)
 
 
elif '{http://www.w3.org/2000/svg}circle' == elem.tag:
 
 
 
dstsTable = ravelCircle(dstsTable, settings, elem)
 
 
else:
 
 
 
print("other tag:" + elem.tag)
 
return dstsTable


本スクリプトのコードは全体で559行あります。

最後に

GithubのRuneAssignMN_Series_Fonts他にて、フォント元画像、ビルドスクリプトなどのすべてを、自由なライセンスで公開しています。
また、RuneAMN_Pro_Series_Fontsについても、やはりGithubにてビルドスクリプトとテスト用のフォント元画像を公開しています。





今回紹介した、Python版のsvg_splitter.pyは、RuneAMNフォントの一部として、githubにてBSD class-2ライセンスで公開する予定です。
リリースの告知はフォントと同じにTwitterなどで行う予定です。もし今欲しいという方がいれば、連絡をいただければ個別に差し上げたり公開を早めたり、対応します。

Project "daisy bell"のフォント関連リリースについては、daisy bellのBOOTHまたはSourceForgeにて確認できます。
@MNukazawa でも告知しています。現在、 このtwitterアカウントでフォント制作過程を毎日報告しています。質問などありましたら気軽にどうぞ。

以上です。


明日のSVG Advent Calendar 2014syon さんの「SVGでキラキラをつくりたい(願望)」 です。よろしくお願いします。

TIGORA(ティゴラ)のトレッキング シューズ

 TIGORA(ティゴラ)の トレッキング シューズを買いました。 メインの靴がアシックスウォーキングで街歩き用なのですが、これまではこれで高尾山などの軽い山も登っていました。 今回、靴底があまりに摩耗したこともあってアシックスウォーキングを買い替えたのですが、ついでに消耗が激...