funde-rectangleのブログ

ウルトラごった煮

Aseprite:オリジナルのフォント画像から欲しい文字のみを抜粋

最近、ドット絵のフォントを作ってみるというハッシュタグでの企画に参加してみました。

これが中々面白くて自分も作ってみたのですが、
せっかく作ったんだしAsepriteで活かしたいね...と思ったものの、
特定の文字を毎回コピペするのは億劫でした。

文字入力したら対象の文字だけ抜粋して出力してくれるような
スクリプトがないかな...と思っていましたが、見つからず
もう作っちゃったほうが早そうだな~と思い、スクリプトをしたためる年末。

スクリプト保管先(GitHub)
github.com

1.使い方

Asepriteにおけるスクリプトの保管場所については割愛します。


▲例によってこんな画像があったとさ。内訳は以下の通り。
 ・5x5の大きさの文字が1pxの枠線を区切りにして保管されている
 ・1段目は左から0から始まり9で終わる数字、
 ・2段目は左からAから始まりZで終わる文字
 ・画像形式はPNGファイル

1.スクリプトを実行


▲ファイルを開いた状態で「SelectFromFile_FontText」スクリプトを実行します。

2.取り込み文字情報、出力文字を入力


スクリプトを実行するとダイアログが表示されるので、以下の情報を入力します。
 ・出力する文字を~(略)     :出力したい文字。小文字のみ受け付け。
 ・文字入力の下にあるボタン   :クリックするとファイル選択ダイアログが出てくるので、取り込み文字の画像(PNGファイル)を指定。
 ・1文字当たりのX軸/Y軸ピクセル数:取り込んだ画像の文字の大きさを入力。出力サイズではないので注意。

3.出力ボタンを押す


▲処理に成功すると新規レイヤーが作成されて、1px X 1pxの位置に文字が出力されます。


アルファ値も取得しているので、背景を透明にしていれば文字以外は透過されて出力されます。
このスクリプト最大の魅力はフォントそのものに色があれば、それを継承してくれる
ところですね。 元絵の緑色の部分もちゃんと引っ張ってきてくれてます。

これがAsepriteのテキスト機能だと白黒になってしまうという...
これは5x5で作ってますが、20x20にし、ドラえもんフォントみたいに文字にモチーフついてるもので
使ってみるのもいいかもしれません。


2.解説。

1.処理概要

ダイアログで出力ボタンを押したときの処理は以下の通りです。
 1.画像ファイルを取得し、Imageオブジェクトを作成
 2.新規レイヤー作成
 <出力文字の特定>
 3.入力文字から1文字抜粋し、UTF-8コードを取得(入力された文字数分ループ)
 4.3で取得した文字コードが97以上であるか確認。(「a」の文字コードが「97」)
  97以下の場合は数字とみなす。
 5.文字コードの数字が「a」または「0」の文字コードからの絶対値を使って出力文字の抜粋位置を決める。
 <描画>
 6.5.で指定した絶対位置の値と1文字当たりのピクセル数を用いてImageオブジェクトから文字を
  ピクセル情報として抜粋
 7.新規作成したレイヤーに1ピクセルを描画(ピクセル情報分ループ。出力位置はピクセル情報が持ってます)


2.ダイアログのパーツから値を取得する

まずダイアログの使い方で結構躓きましたね...
ドキュメントが英語だから見落としやすいのと、AsepriteのAPIが記載されたGithubには具体例がないので、
改めて説明します。

ボタンやテキストボックスなどの入力値はDialogのオブジェクトに格納されています。

  local dlg1 = Dialog{ title="ドット絵文字入力ツール" }

▲今回のスクリプトでは dlg1がそう。

ボタンやテキストボックスなどの入力値は id=... で指定された箇所に格納されます。
そしてこの値を取り出して変数に出力するためには、
Dialogのオブジェクトからdataメンバ → idで指定したメンバ の順に指定して
取得するのです。  
※ファイル選択に限らず、テキストボックスも同様の仕組みです。

順番に例を挙げて説明します。

  dlg1:file{ id="filepath",
  filetypes={"png"},
  open=true,
  onchange=
    function()
      filepath = dlg1.data.filepath
    end
  }

▲ファイル選択のボタンの全体像。 filepathというidを指定しています。

      dlg1.data.filepath

▲その場合、この形式で取得すれば、ファイルのフルパスを取得できます。

  onchange=
    function()
      filepath = dlg1.data.filepath
    end

▲onchangeオプションは選択内容が変わったときに実行する処理です。
オプション内でfunctionを定義し、ファイルのフルパスを別で定義している、filepath変数に格納しています。
 (紛らわしくて済みませんが、この変数名はfilepathじゃなくてもOKです...)


しかし、これがわかっているだけでもAsepriteのスクリプトの使いやすさがうんと上がるんじゃないかと。


3.文字位置の判別(入力文字→画像位置)

これはAseprite特有というよりかはLua言語のお話になります。

今回のスクリプトは入力された文字のUTF-8文字コードを使って処理しています。

for pos, text in utf8.codes(inputText) do

▲utf8.codesが文字コードを取得する処理。textのなかに文字コードが入ります。

また、文字コードの数字が以下である前提で組まれています。

                -- 参考:UTF-8の文字コード
                --   -- 0:48   1:49   9:57
                --   -- a:97
                --   -- l:108
                --   -- w:119

▲数字のほうが文字コードが若いです。 故にテキスト画像は1段目を数字、2段目を文字として、
 見た目的にも処理的にも判別しやすいようにしています。

0が48、1が49、2が50.....
aが97、bが98、cが99....

というように、97という数字を境にして文字コードが決まっているので、
この「97」という数字を使って文字か数字を判別→そこから絶対値を計算して、文字位置を判定
という手段を取っているわけです。

            -- UTF-8の文字コードの文字の開始位置
            UNICODE_CHAR_INITIAL = 97

▲「97」という数字は定数で設定。文字コードUTF-8じゃないものに変わったら変えなきゃですね

                if text < UNICODE_CHAR_INITIAL then
                -- 数字の場合(画像の1段目)
                textY = 1
                -- 文字単位+枠線分 2px
                -- 数字の場合(画像の1段目)
                textX = 1 + UNIT_TEXT_X_PX * (text - UNICODE_NUMBER_INITIAL) + 1 * (text - UNICODE_NUMBER_INITIAL)
                else
                -- 文字の場合(画像の2段目)
                -- 文字単位+枠線分 2px
                textY = UNIT_TEXT_Y_PX + 2
                textX = 1 + UNIT_TEXT_X_PX * (text - UNICODE_CHAR_INITIAL) + 1 * (text - UNICODE_CHAR_INITIAL)
                end

▲そして一番上のif文で97以上であるかを判別し、1段目(数字)か、2段目(文字)であるかを分岐させて処理しています。
 別のケースに対応できるようになんかもっといい方法がありそうな気がしますが、
 個人で使うレベルなので、省力化させています。


4.Imageオブジェクトから抜粋して貼り付け

Imageを張り付けるという手段がうまくいかなかったので、
Imageオブジェクトから範囲指定して抜粋するのですが、
出力後の形式がpixelとなります。 このpixelの扱いについて補足します。

ループ内容として、Image:pixels にてピクセル情報群を取得して、1ピクセルずつ処理しています。
Image:pixelsの引数としてRectangleを与えてあげると、ImageからRectangleの範囲のピクセル情報を抜粋します。

x...始点のX軸位置
x...始点のY軸位置
width...横方向の大きさ
height...縦方向の大きさ

                -- 文字の描画(Imageから文字に相当する範囲を抜粋したピクセル群に対して処理する)
                for it in fontImage:pixels(Rectangle{x=textX, y=textY, width=UNIT_TEXT_X_PX, height=UNIT_TEXT_Y_PX}) do

ピクセル情報群に対してループ


取得した範囲のなかでの絶対位置はピクセル情報の中に持っているので、
ピクセル取得したはいいけれど、Y軸方向に指定できなくない?なんてことはありません。
yメンバをとってこればOKです。

                  -- 出力先セルの該当位置に1ピクセル出力する
                  destinationCel.image:drawPixel(Xposition + it.x - textX, it.y - textY, pixelValue)

▲1ピクセルを出力する処理。 pixelValueは色情報(integer)。


つまりは、1ピクセルずつ描画をしている、というわけですね。 大変だ~