📝

ase2ttf - Asepriteからフォントを作成するツール

に公開

Asepriteファイルからフォントを作成するためのツール「ase2ttf」をリリースしました!

https://ase2ttf.com/

上のサイトを使って簡単にピクセルフォントが作成できます。また、あわせてコマンドラインから同等の機能を扱えるように専用のCLIツールを配布しているほか、Rustで書かれたCLIツールおよびコア部分をOSSとして公開しています。

https://github.com/nuskey8/ase2ttf

使い方に関してはnoteの方で詳しく解説しているので、そちらを参考にしていただけると。

https://note.com/nuskey/n/ne2db29729d94

Zennの方では技術周りの話をまとめていきます。

モチベーション

特にゲーム開発者の方などは、ドット絵のグラフィックに合うフォントがなかなか見つからない、ということがよくあるのではないかと思います。画面の統一感を出すためにも雰囲気に合ったフォントを選びたいところですが、ドット絵向きのフォントの選択肢はそこまで多くありません。また、世界観に合った特殊な文字を使いたい場合などにも独自のフォントを用意できると便利でしょう。

また、ドット絵フォントの作成に関しては既にPixelTypeという有料のソフトが存在します。

https://store.steampowered.com/app/2127930/PixelType/?l=japanese

PixelTypeは透過pngからフォントを作れるため、作成するソフトを選ばないという利点があります。ただ個人的には設定項目の多さやGUIがあまり好みではなく、また結局はAsepriteを使うことがほとんどなので、それなら直接変換できた方が便利だろう、と。

というわけで作ったのがこのase2ttfになります。ソフトのインストールも必要なく、ブラウザでファイルをアップロードしてポチポチするだけでフォントが出来上がるので非常に快適です。

内部の仕組み

ase2ttfのコアであるasepriteファイルからttfへの変換はRustを使って作っています。私が普段書いてる言語はC#なんですが、こういう低レベルなバイナリ操作に関してはやはりRustの方がライブラリも揃っていて書きやすいかなー、と。実際ほとんどの処理はライブラリ任せで済んでいるので、かなり簡単に作れました。

Asepriteファイル

ase2ttfは入力としてAsepriteファイルを利用します。このAsepriteファイルについては、詳細なバイナリ仕様がGitHub上で公開されています。

https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md

そのためバイナリから自前で解析をすることも比較的簡単にできたりします。とはいえ、一からそれをやるのは大変なので、今回はasefileというクレートを利用しました。これを使うと、こんな感じで簡単にAsepriteファイルを扱うことができます。

use std::path::Path;

use asefile::AsepriteFile;
use image::{self, ImageFormat};

fn main() {
    let file = Path::new("input.aseprite");
    let ase = AsepriteFile::read_file(&file).unwrap();
    for frame in 0..ase.num_frames() {
        let output = format!("output_{}.png", frame);
        let img = ase.frame(frame).image();
        img.save_with_format(output, ImageFormat::Png).unwrap();
    }
}

素直なAPIで解析ができてとても良いですね。

グリフの構築

Asepriteファイルから取得できるのはRGBAで表現された画像データまでです。これをフォントとして使うには、画像データから輪郭を抽出してベクターに変換する必要があります。例えば、以下のような文字があった場合。

この画像からase2ttfを用いて生成したグリフを見てみましょう。フォントの内部データの確認にはFontDrop!というサイトが便利です。

https://fontdrop.info/

実際に確認してみると以下のようになります。

こんな感じで、外周と内側の穴を囲うようなグリフが生成されています。追加で不要な中間点を取り除けばバイナリサイズを結構削れるんですが、フォントとしては問題なく動くのでとりあえず放置してます...そのうち直したい...

やっていることはゴリ押しでピクセル部分の輪郭抽出を行い、そこからパスを構築しているだけです。とりあえずは動作の安定性を優先して開発を進めていたので、最適化の余地はまだかなりあります。が、実用上問題ない程度の速度ではあるので、ひとまずは十分でしょう。

ttfバイナリの構築

さて、ここからが大変な作業です。asepriteからグリフの情報を構築した後は、フォントとして利用可能な形式に変換します。今回はTTFを採用したため、取得した情報からTTFバイナリを自力で組み立てていく必要があります。

ttfをRustで扱うクレートはいくつも存在しますが、今回はGoogle謹製のフォント処理ライブラリ「Skrifa」の一部である「write_fonts」というクレートを採用しました。

https://docs.rs/write-fonts/latest/write_fonts/

SkrifaはGoogleがFreeTypeの代替として開発したフォント処理のためのライブラリです。もともとChromeはフォント処理にFreeTypeを用いていましたが、C/C++のライブラリに起因するメモリ周りの脆弱性から、セキュリティの問題が数多く起こってきました。そのため、メモリ安全な言語であるRustでFreeTypeを代替しよう、という目的の元に作られたのがSkrifaになります。

この辺りの経緯はChromeの開発者向けブログに記事として上がっているので、興味がある方はぜひ読んでみると良いでしょう。

https://developer.chrome.com/blog/memory-safety-fonts?hl=ja

write_fontsはOpenType/TrueTypeフォントの構築に特化したクレートで、FontBuilderを用いてフォントを組み立てていくことができます。

let mut builder = write_fonts::FontBuilder::new();
builder.add_table(...).unwrap();
let bin = builder.build();

OpenType/TrueTypeフォントはcmapOS/2などの複数のテーブルの組み合わせで出来ています。全てのテーブルが必須というわけではなく、必要最低限のテーブルさえ追加すればフォントとしての動作はするようになります。

OpenTypeについてはこちらのサイトがとてもわかりやすくまとめてくれていたので、まずはこちらを読みつつ、わからないことがあれば都度仕様を読む、という感じで実装を進めて行きました。

https://azelpg.gitlab.io/azsky2/note/prog/opentype/index.html

ちなみに、OpenType/TrueTypeの仕様はMicrosoftAppleによって公開されています。ただまあ...いきなりこれを読むと確実に心が折れるので、まずは大まかな構造を把握してから読むことをお勧めします...

CLIツール

ase2ttfは元々CLIツールとして開発していたこともあり、フロントエンドと同様の機能を持つCLIツールをcrates.ioで公開しています。

https://crates.io/crates/ase2ttf

利用可能なオプションは以下の通り。オプションはサイトのそれとほとんど対応しているので、使い方に迷うことはないでしょう。

$ ase2ttf -h
A Command-line tool for creating pixel fonts from Aseprite files

Usage: ase2ttf [OPTIONS] <PATH>

Arguments:
  <PATH>  

Options:
  -o, --output <OUTPUT>                            
      --copyright <COPYRIGHT>                      
      --family <FAMILY>                            
      --subfamily <SUBFAMILY>                      
      --font-version <FONT_VERSION>                
      --font-weight=<FONT_WEIGHT>                  
      --glyph-width=<GLYPH_WIDTH>                  [default: 16]
      --glyph-height=<GLYPH_HEIGHT>                [default: 16]
      --trim                                       
      --trim-pad=<TRIM_PAD>                        [default: 1]
      --line-gap=<LINE_GAP>                        [default: 0]
      --baseline=<BASELINE>                        [default: 2]
      --underline-position=<UNDERLINE_POSITION>    [default: 0]
      --underline-thickness=<UNDERLINE_THICKNESS>  [default: 1]
  -h, --help                                       Print help
  -V, --version                                    Print version

CLIツールの実装には、ほとんどデファクタスタンダードとなっているClapを利用しました。APIも使いやすく、機能も豊富でとても良い感じです。せっかくなのでこれも解説記事書こうかな...

https://github.com/clap-rs/clap

フロントエンド

フロントエンドはVite + Reactのオーソドックスな構成です。 また、状態管理ライブラリにはJotaiを利用しています。手軽に状態管理ができて便利です。

https://jotai.org/

さらに、Rustで書いたコア部分はwasmとしてビルドしたものを直接フロントエンドに持ち込んでいます。これによってバックエンドが不要になり、全ての処理がフロントエンドのみで完結しています。wasm最高。

また、サイトのフォントはase2ttfを用いて自作したものです。このフォントもBooth/GitHubで配布しているので、こちらも是非使ってみていただけると嬉しいです。

https://nuskey.booth.pm/items/7132083

https://github.com/nuskey8/ase2ttf-font

まとめ

もともとゲーム向けにドット絵をよく書いていたこともあって、フォント自作は一度やってみたいなーと思っていました。とはいえ専用のソフトを買って作るのは大変なので、このようにサクッと変換できるサイトがあれば、手軽にフォントを自作出来るのにな、と。それが今回ase2ttfという形で、かなり理想的なものを実現できたので満足です。

というわけでase2ttf、是非試してもらえると嬉しいです〜!フォント制作はいいぞ!

Discussion