次ログ

次ログ

ゆるりと働いているSREの技術ブログのような何か。趣味の話も書く

NimでPNM画像を扱うライブラリを書いた

NimでPNM画像を扱うライブラリを書きました。

リポジトリは下記の通りです。

github.com

Qiita - Nimのパッケージリポジトリに自前のライブラリを追加する流れの手順を実施して すでにnimble install可能な状態です。

Qiitaにも書いたのですが 自作のライブラリなのでブログに載っけといたほうがいいようなのでブログに転記しました。

そもそもNimって?

Qiita - 至高の言語、Nimを始めるエンジニアへにNimとは何かが書かれています。 僕の感触としてはPythonの皮をかぶったRubyっぽいC言語です。

開発環境

  • Ubuntu18.10
  • Nim 0.19.4

PNMとは

以下のWikiに書かれている内容がわかりやすいです。

PNM (画像フォーマット) - Wikipedia

一応説明すると、2次元の数値の並びがそのまま画像として表示される画像フォーマットになります。

たとえば以下のテキストはPNMの1つです。

P1
5 5
0 0 1 0 0
0 1 1 0 0
0 0 1 0 0
0 0 1 0 0
0 1 1 1 0

これを画像ビューワで開くと、以下のようにレンダリングされます。

t.png

前述のテキストファイルは以下のような書式になっています。

  • 1行目に画像フォーマット名
  • 2行目に列数、行数
  • 3行目以降に画像のデータ

データ部分については、0が白色、1が黒色としてレンダリングされます。 PNMはテキストファイルだけ渡されても自力で脳内レンダリングしやすいフォーマットです。

前述の P1 というディスクリプタの画像は PBM という画像フォーマットになります。 PNM(Portable Anymap)は1つの画像フォーマットではなく、PBM、PGM, PPMの総称です。

PNMの画像フォーマットはWikiにも記載あるとおり6種類あります。

ディスクリプタ データフォーマット
P1 PBM (テキスト)
P2 PGM (テキスト)
P3 PPM (テキスト)
P4 PBM (バイナリ)
P5 PGM (バイナリ)
P6 PPM (バイナリ)

今回作成したpnmというライブラリでは、この6種類すべて扱えるように実装しました。

PBMの実装

PBM、PGM、PPMと書式があって、どれも簡単なフォーマットだったので実装には苦労しませんでした。 そのうち、一番めんどくさかったのは、PBM(P4)です。

バイナリ形式なのですが、バイナリデータのビットがそれぞれ画像のドットに対応するので 0, 1の数値データを8個ずつ切り出して1byteのデータに変換する必要がありました。

せっかくなので、この記事では一番面倒だったPBMの書き込みについてを取り扱います。

ライブラリの使い方

pnmライブラリを使ってPBM P4として画像出力するコード例をいかに示します。

import pnm

let col = 5
let row = 5
let data = @[
  0'u8, 0, 1, 0, 0,
  0,    1, 1, 0, 0,
  0,    0, 1, 0, 0,
  0,    0, 1, 0, 0,
  0,    1, 1, 1, 0,
]
let pbm = newPBM(pbmFileDiscriptorP4, col, row, data.toBin(5))
writePBMFile("1.pbm", pbm)

このコードを実行すると、前述のPNM画像ファイルが生成されます。

2進数のデータをbyteデータに変換する

dataは5行5列のデータです。 25個のデータですが、これをbyteデータに変換します。 期待値としては、以下のようなbyteデータにします。

0b0010_0000
0b0110_0000
0b0010_0000
0b0010_0000
0b0111_0000

newPBM(pbmFileDiscriptorP4, col, row, data.toBin(5)) でのtoBinはbyteデータへの変換をやっています。

5データずつ切り出して5bitのデータにする必要がありますが byteデータは8bitです。 byte型にするには3bit分たりないのですが、足りない分は左シフトします。

toBinは今回作成したライブラリのサブモジュール内のプロシージャの1つです。 toBinプロシージャの実装をいかに示します。

proc toBin*(arr: openArray[uint8], col: int =  8): seq[uint8] =
  ## Returns sequences that binary sequence is converted to uint8 every 8 bits.
  runnableExamples:
    doAssert @[1'u8, 1, 1, 1, 0, 0, 0, 0].toBin == @[0b1111_0000'u8]
    doAssert @[1'u8, 1, 1, 1, 1, 1].toBin == @[0b1111_1100'u8]
    var s: seq[uint8]
    doAssert s.toBin == s
  var data: uint8
  var i = 0
  for u in arr:
    data = data shl 1
    data += u
    i.inc
    if i mod 8 == 0:
      result.add data
      data = 0'u8
      continue
    if i mod col == 0:
      data = data shl (8 - (i mod 8))
      result.add data
      data = 0'u8
      i = 0
  if data != 0:
    result.add data shl (8 - (i mod 8))

arrから1つずつデータを取り出して加算して左シフトを繰り返し、 1byte分データが加算されたらresultに追加を繰り返すような実装です。

Nimでは##をプロシージャ内に書くとドキュメンテーションコメントとして扱われます。 ライブラリとして公開するためにドキュメントも必要と思ったので書いています。

runnableExamplesドキュメンテーションコメントの1つです。 nim docでドキュメントを生成する際に、runnableExamplesのブロックのコードを実際にコンパイルして実行して コードが実行可能であることを検証してくれます。また、このブロックにかかれているコードもドキュメントに含まれます。

このソースコードから以下のドキュメントが生成されます。 https://jiro4989.github.io/pnm/util.html#toBin%2CopenArray%5Buint8%5D%2Cint

ファイル出力

writePBMFileではnewPBM()で生成した構造体をbyteデータに変換してファイル出力します。 byteデータへの変換は以下のような実装になっています。 データ構造は単純で、それぞれのデータをuint8型に変換しているだけです。

proc formatP4*(self: PBM): seq[uint8] =
  ## Return formatted byte data for PBM P4.
  runnableExamples:
    let p4 = newPBM(pbmFileDiscriptorP4, 1, 1, @[0b1000_0000'u8])
    doAssert p4.formatP4 == @[
      'P'.uint8, '4'.uint8, '\n'.uint8,
      '1'.uint8, ' '.uint8, '1'.uint8, '\n'.uint8,
      0b10000000'u8,
    ]
  # header part
  # -----------
  # file discriptor
  result.add self.fileDiscriptor.mapIt(it.uint8)
  result.add '\n'.uint8
  # col and row
  result.add self.col.`$`.mapIt(it.uint8)
  result.add ' '.uint8
  result.add self.row.`$`.mapIt(it.uint8)
  result.add '\n'.uint8
  # data part
  # ---------
  result.add self.data

書き込みをしている箇所はこれだけ。特に凝ったことはしていません。

let bin = data.formatP4
discard f.writeBytes(bin, 0, bin.len)

まとめ

PNMを扱うためのライブラリの使い方と、その実装の一部について説明しました。

PNMはPNGなどの一般的な画像フォーマットと比べると、非常に簡単な書式なので、実装の練習としては有用です。 僕の場合は、ビット演算を今までほとんどやったことなかったのですが、PBM P4の実装を通してビット演算の理解が深まりました。

あと実装についてですが、メソッドチェーンで処理をどんどんつないで コードをかけるのが面白いな、と感じました。

特にsequtilsstrutilsのモジュールには強力なものがたくさんあるので 少ないコード量でスイスイ実装を進められたと感じています。 また、スライスの値比較とかも普通に==でできますし、構造体のポインタ型のデータの値比較も 変数名の後に[]と書くだけで値型として扱えるのでテストコードも書きやすいです。

コード量はテストコード込で以下のようになりました。

-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Nim                              4            105            402           1075
-------------------------------------------------------------------------------
TOTAL                            4            105            402           1075
-------------------------------------------------------------------------------

しかしながら、後からコードを見直したときに、1行の情報量が増えやすく 気を抜くとすぐに可読性が低下しそうな危うさも感じました。 普段良く使ってるGoと比べると可読性は低くなりやすいと感じます。

以上です。

Terminal上の色のついたテキストを画像に再現するコマンド(textimg)をGoで作った

Terminal上の色のついたテキストを画像に再現するtextimgというコマンドをGoで作りました。 なぜ作ったのか、と何ができるのか、について記載します。

リポジトリは下記。

github.com

なぜ作ったのか

シェル芸bot環境で色付きのテキストを画像に再現したかったからです。 シェル芸bot環境にはImageMagickがインストールされています。 また、テキストを画像に出力する処理を簡易にするためのimgoutというコマンドもあります。

ImageMagickで色のついたテキストを画像に変換するには、非常に複雑な オプションを組み合わせる必要があって、Twitterの文字数内では辛いものがあります。

Linux環境でテキストに色をつけるにはエスケープシーケンスを使用します。 色のついたテキストを出力するコマンドもいくつかあって それをそのまま画像に起こせたら楽しいかな、と考えたからです。

使い方

非常に単純な使用例は下記のとおりです。

textimg $'\x1b[31mRED\x1b[0m' -o out.png

red

より複雑な使用例は下記のとおりです。

seq 0 255 | while read -r i; do
  echo -ne "\x1b[48;5;${i}m$(printf %03d $i)"
  if [ $(((i+1) % 16)) -eq 0 ]; then
    echo
  fi
done | textimg -o 256_bg.png

bgall
アニメーションGIFにも対応しています。 シェル芸bot環境での使用例は以下です。

使用しているコマンドのオプションの説明は以下のとおりです。

textimg -sal8 -d 6 -F 4

  • -s シェル芸botの画像出力先ディレクトリに画像を保存する(/images/t.gif)
  • -a アニメーションGIFとして画像を生成する
  • -l 8 1フレームの画像に何行テキストを使用するか
  • -d 6 アニメーションのフレームの待ちフレーム(delay)
  • -F 4 フォントサイズ

実装

エスケープシーケンスを含む文字列は以下のように分解・分類できる。

\x1b[1;31;42mRed foreground Green background\x[0mNormal

  • 分解後
    • \x1b[1;31;42m
      • 1 太字にする (文字装飾)
      • 31 文字色を赤にする (色変更)
      • 42 背景色を緑にする(色変更)
    • Red foreground Green background (文字列)
    • \x1b[0m 文字の装飾や色変更をもとに戻す (色変更)
    • Normal (文字列)

このように文字列をエスケープシーケンスか、テキストかに分解し、 エスケープシーケンスの中から着色に関係のある文字列か否かに分類している。

エスケープシーケンスとのマッチはそれぞれ以下の正規表現で取り出している。

  • 色系:^\x1b\[[\d;]*m
  • 色系以外:^\x1b\[\d*[A-HfSTJK]

画像描画では、文字列が来たときだけ画像変数にテキストを書き込み、それ以外の場合 は書き込むテキストに指定する色や装飾の変更だけを行うのをひたすら繰り返すように している。

前述の文字列の場合は以下のような順序で処理をしています。

  1. なにもしない(太字装飾は無視)
  2. foregroundColor = red
  3. backgroundColor = green
  4. drawText("Red foreground Green background", foregroundColor, backgroundColor)
  5. foregroundColor = defaultColor
  6. backgroundColor = defaultColor
  7. drawText("Normal", foregroundColor, backgroundColor)

※実装のイメージなので実際のコードの通りではないです(変数名とか)。

エスケープシーケンスなどの情報はBash tips: Colors and formatting (ANSI/VT100 Control sequences)を参考にした。

まとめ

Terminal上の色のついたテキストを画像に再現するtextimgというコマンドについて 説明しました。

シェル芸bot環境に色のついたテキストを画像として再現できるようになって とても満足しています。

シェル芸botを使ってる方からもそれなりに気に入っていただけたようで、 作って公開して少ししか経っていないのにスター数が一番多いリポジトリになりました。 やはりそれなりに需要のあるものだったんですね。

ひとまずPNG生成できるようになって、アニメーションGIFも作れるようになって ひとまず自分のほしかった機能はだいたい実装し終わりました。 アップデートは何かバグが見つかったときとか、PRが来た時だけを考えています。

以上です。

第41回 シェル芸勉強会振り返り

第41回シェル芸勉強会に参加した。初参加です。

まえがき

2019/4/27 (土)に「jus共催 第41回{ウン,ガク,}{チ,ト,}{,ン}{,コイン}{ブ,}{ラブラ, ハ,}{,イブ}{無,有}罪シェル芸勉強会」に参加してきたのでその振り返り。 この名称自体もシェル芸で[ブレース展開]という記法。これをechoしてみると以下のよう になる(※長すぎたので一部抜粋)。

echo jus共催 第41回{ウン,ガク,}{チ,ト,}{,ン}{,コイン}{ブ,}{ラブラ,ハ,}{,イブ}{無,有}罪シェル芸勉強会"\n" | wc -l

865

echo jus共催 第41回{ウン,ガク,}{チ,ト,}{,ン}{,コイン}{ブ,}{ラブラ,ハ,}{,イブ}{無,有}罪シェル芸勉強会"\n" | head

jus共催 第41回ウンチブラブラ無罪シェル芸勉強会
第41回ウンチブラブラ有罪シェル芸勉強会
第41回ウンチブラブライブ無罪シェル芸勉強会
第41回ウンチブラブライブ有罪シェル芸勉強会 +第41回ウンチブハ無罪シェル芸勉強会
第41回ウンチブハ有罪シェル芸勉強会
第41回ウンチブハイブ無罪シェル芸勉強会
第41回ウンチブハイブ有罪シェル芸勉強会
第41回ウンチブ無罪シェル芸勉強会
第41回ウンチブ有罪シェル芸勉強会

シェル芸勉強会の様子は Togetterにまとまっています。

午前の部

  • 文字コードの話
  • 文字列の結合、絵文字の結合、絵文字の色の変更ができる
  • UnicodeData.txtというファイルにユニコード文字のすべてが載っている

午後の部

8問のシェルの問題を片っ端からとくというもの。 問題に必要なファイルはこちらにある。 シェル芸のコマンドを実際に実行するなら、 あらかじめ以下のコマンドを実行して準備を整えておく。

git clone https://github.com/ryuichiueda/ShellGeiData
cd ShellGeiData/vol.41

問題の一覧はこちら。 以降は問題と自分の回答。

Q1

Q.次のファイルについて、2列目をキーにしてエクセルの横列の記号(A, B, ..., Z, AA, AB, ...のやつ)順に並べ替えてください。

$ cat excel
114514 B
1192296 AA
593195 CEZ
4120 TZ
999 QQQ

回答としては、以下のように出力されてほしい。テストの意図としては、文字列の長さも 考慮してソートしてほしい、というもの。

114514 B
1192296 AA
4120 TZ
593195 CEZ
999 QQQ

普通に sort コマンドで2カラム目を指定すると以下のようにソートされてしまう。

% sort -k2 excel 
593195 AA
114514 B
1192296 CEZ
999 QQQ
4120 TZ

文字列の長さのカラムを追加してソートするのが正攻法だろう。 僕の回答としては以下の通り。

% awk '{print $0, length($2)}' excel | sort -k3 -k2 | cut -d ' ' -f 1,2 
114514 B
593195 AA
4120 TZ
1192296 CEZ
999 QQQ

Q2

Q.次のファイルのレコードを干支順にソートしてください。

$ cat eto_yomi
申 さる
子 ね
寅 とら
卯 う
巳 み
辰 たつ
丑 うし
酉 とり
戌 いぬ
亥 い
午 うま
未 ひつじ

ただし、次のファイルを補助に使って良いこととします。

$ cat eto
子丑寅卯辰巳午未申酉戌亥

etoファイルの並びを利用してeto_yomiをソートするもの。まずは正攻法から。

sortなので素直にsortコマンドと別のファイルの中身を結合すると考えてjoinコマンドを 使用する。僕の回答としては以下の通り。

% grep -o . eto | nl | sort -k2 | join -1 2 -2 1 - <(sort eto_yomi) | sort -nk2 | cut -d ' ' -f 1,3
子 ね
丑 うし
寅 とら
卯 う
辰 たつ
巳 み
午 うま
未 ひつじ
申 さる
酉 とり
戌 いぬ

流れとしては下記の通り。

  1. grep -o で1行のデータを縦に並べて nl で番号の列を追加
  2. 後に控えている join は結合するフィールドが辞書順にソートされている必要がある ため、joinに指定するフィールドを辞書順にソート
  3. joinで結合。 -1 2-2 1 で1個めのファイルの2フィールド目と2個めのファ イルの1フィールド目を結合する
    1. eto_yomiも辞書順でソート
  4. 最初にnlで付与した番号を使い干支順にソートし直す
  5. cutで不要なフィールドの削除

シェル芸の非常に良いお題だったと思う。

次にgrepを使用した非常にコンパクトな例。初め僕は思いつかなかった。

% grep -o . eto | xargs -I@ grep @ eto_yomi
子 ね
丑 うし
寅 とら
卯 う
辰 たつ
巳 み
午 うま
未 ひつじ
申 さる
酉 とり
戌 いぬ
亥 い

すでに干支順でソートされているのでそのままgrepしてしまう、というもの。 こういうアプローチもあるのか、とまたシェル芸の知見が深まった。

Q3

Q.次のファイルのレコードを数字(第一フィールドの計算結果)が小さい順に並べてく ださい。

$ cat kim_calc
1+2+4 金正日
4*3 金正男
3-1-5 金日成
495/3 金正恩
0x1F 金正哲

これは割とすぐに思いついた。はじめは bc コマンドを使用していたが0x1fで失敗する のでbashの算術式を使用するようにしたら通った。 僕の回答としては下記の通り。

% cat kim_calc | while read -r e n; do echo $(($e)) $e $n; done | sort -nk1 | cut -d ' ' -f 2,3
3-1-5 金日成
1+2+4 金正日
4*3 金正男
0x1F 金正哲

Q4

Q.次のファイルはシフトJISのテキストですが、これを1) 辞書順、2) 数字の小さい順 、にソートしてください。出力もシフトJISとします。

$ cat sjis | nkf -g
Shift_JIS
$ cat sjis | nkf -wLux
123 ずんごるももう
31 こきたてひーひー
9 ほじぱんふんじこみ
2242 たまもとやろう

最初は試験の意図がわからなかったが、2つ結果を出力するだけでよかったらしい。 1の辞書順については下記の通り。

% cat sjis | nkf -wLux | sort
123 ずんごるももう
20 ほじぱんふんじこみ
2242 うえってきたかるとらまん
31 こきたてひーひー

2の数字の小さい順は手間が必要。このファイルの数値は全角数字なので数値ソートがで きない。なので修正してソートしてから復元する必要がある。僕の回答としては下記の通 り。

% cat sjis | nkf -wLux | sed 'y/1234567890/1234567890/' | sort -nk1 | sed 'y/1234567890/1234567890/'
20 ほじぱんふんじこみ
31 こきたてひーひー
123 ずんごるももう
2242 うえってきたかるとらまん

sedのyコマンドを使用して対応する数値を変換しているだけ。これも簡単な方だと思う。

Q5

Q.サイズの小さい順にソートしてください。

$ cat size 
2GB
1.2GB
40000MB
1000000000kB
0.4GB
410MB

はじめはsortのヒューマンリーダブルオプションでいけるか、と思ったがダメだった。

% cat size | sort --sort h
1000000000kB
410MB
40000MB
0.4GB
1.2GB
2GB

なんとキロバイト順、メガバイト順、ギガバイト順にソートされている。 仕方なくnumfmtで変換して元に戻した。

% cat size | tr -d B | tr k K | numfmt --from iec --to=none | paste - size | sort -nk1 | cut -f2
0.4GB
410MB
1.2GB
2GB
40000MB

Q6

Q.sleepと内部コマンドだけを使って次の数を小さい順にソートしてください。

$ cat nums
5.4
0.34
2.3
0.9
6

これも割とすぐ思いついた。 内部コマンドってなんだろうと思ったが help というコマンドを使えば分かる。 whileやforが使えたのと、非同期実行を使えば行ける。

% while read i; do (sleep $i; echo $i) & done < nums
0.34
0.9
2.3
5.4
6

Q7

Q.次のローマ数字をソートしてください。

$ cat roman
IV
XI
LXXXIX
IX
XLIII
XX
VIII

これは割と力づくだったが一応いけた。

% cat ShellGeiData/vol.41/roman \
| sed 's/IV/4 /g;s/XL/40 /g;s/IX/9 /g;s/I/1 /g;s/L/50 /g;s/X/10 /g;s/V/5 /g;s/ /+/g;s/+$//g;s/.*/echo $((&))/e' \
| paste - ShellGeiData/vol.41/roman \
| sort -nk1 \
| cut -f 2
IV
VIII
IX
XI
XX
XLIII
LXXXIX

numconvというコマンドの存在を他の方のシェルから知った。 それを使えば以下のように修正できる。

% cat roman | numconv | paste - roman | sort -nk1 | cut -f2
IV
VIII
IX
XI
XX
XLIII
LXXXIX

Q8

Q.次のファイルを辞書順にソートしてください。ただし、濁点がついているものが先に 来るようにしてください。できる人はワンライナー中で「かきくけこがぎぐげご」の文 字を使わないでください。

$ cat gagigugego 
かき氷
ぎ・おなら吸い込み隊
きつねうどん
ぐりこもりなが事件
きききりん
がきの使い
くその役にも立たない
げんしりょく発電
ごりらいも
こじんてきにはクソ
例
がきの使い
かき氷
ぎ・おなら吸い込み隊
きききりん
きつねうどん
ぐりこもりなが事件
くその役にも立たない
げんしりょく発電
ごりらいも
こじんてきにはクソ

これははじめどうしたら良いかわからなかったが、午前の部で話していた内容を使えばす ぐに解決できることがわかった。濁点文字を濁点なしの文字と濁点の結合文字に分割する ことでソート順序を変更するというもの。

% cat ShellGeiData/vol.41/gagigugego | uconv -x NFD | sort | uconv -x NFC
がきの使い
かき氷
ぎ・おなら吸い込み隊
きききりん
きつねうどん
ぐりこもりなが事件
くその役にも立たない
げんしりょく発電
ごりらいも
こじんてきにはクソ

LT会

まとめ

はじめてシェル芸勉強会に参加したけれど、新しい知見がいっぱいだった。 シェルはガリガリ普段から書いていたので、問題自体は割となんとかなった。 8問のうち7問は自力で解けた。解けなかったのはがぎぐげごのやつ。

何はともあれシェル芸勉強会、面白かった。 シェルはいろんな環境にあるし、ちょっとした作業をぱぱっと自動化できてやはり好きだし 今後も必要で有り続ける技術だと思うので、引き続き勉強は続けようと思う。

ただし100行を超えるような処理をシェルで書くのはやめよう。 素直に他の言語を使うべし(経験者は語る)。

以上。

テキストを左右中央寄せするalignコマンドをGoで作った

テキストを左右中央寄せするalignコマンドをGoで作りました。 なぜ作ったのか、と何ができるのか、について記載します。

作ったもの:

github.com

なぜ作ったのか

シェル芸bot環境で位置揃えを簡単にできるようにしたかったからです。 シェル芸bot環境では基本的にTwitterの140文字が文字数の限界で (引用リツイートを使うことで140文字以上入力することも可能ですが) 文字数を少しでも削りたい、という思いで作りました。

位置揃えの例

例えば以下のテキストがあります。

12345
abc
zzzzzzzzzzzzzzz

このテキストを右揃えするシェルは下記のようになります。

#!/bin/bash

align_right() {
  local p="$1"
  local max_line_width=0
  local lines=()
  while read -r line; do
    lines+=("$line")
    width=$(echo "$line" | wc -c)
    if [ "$max_line_width" -lt "$width" ]; then
      max_line_width=$width
    fi
  done

  for line in ${lines[@]}; do
    width=$(echo "$line" | wc -c)
    diff=$((max_line_width - width))
    pad=$(seq $diff | xargs -I@ echo -n "$p")
    echo "$pad$line"
  done
}

cat << EOS | align_right " "
12345
abc
zzzzzzzzzzzzzzz
EOS

無事実装できました。 中央寄せも同じように実装できます。

ただ毎回こんなのを実装するのも面倒ですし、 これだど日本語が混在するテキストの場合にきちんと位置を揃えられません。 位置揃えに使える文字も半角文字に限定されます。

入力のテキストに全角文字が混在していてもいい感じに処理できて、 位置揃えに全角文字も指定できるようにしたのが 今回作成したalignコマンドです。

alignコマンドの使い方

前述の例をalignを使うように書きかえると以下のようになります。

cat << EOS | align right -p " "
12345
abc
zzzzzzzzzzzzzzz

実行結果はこちら。

          12345
            abc
zzzzzzzzzzzzzzz

サブコマンドにはleft, center, rightが指定できます。 全角文字が混在する場合の例は下記。

cat << EOS | align right -p " "
あいうえお
abc
zzzzzzzzzzzzzzz
EOS

実行結果はこちら。

     あいうえお
            abc
zzzzzzzzzzzzzzz

無事、きちんと位置を揃えられています。 ※はてなブログ上ではフォントの関係でずれて表示されてますが・・・。

実装

位置を揃えるロジックについては前述のbashのコードと 同じようなことをやっています。

  1. 一番長い文字幅を取得する
  2. 差分を文字で埋める

重要なのは「文字幅をどう取得するか」です。

alignではgo-runewidthという外部ライブラリを使用することで 文字列の見た目上の文字幅を取得しています。

go-runewidthでは「全角文字なら文字幅2」「半角文字なら文字幅1」という具合に 見た目上のテキストの幅を返してくれます。 これを利用し、位置を揃えるようにしています。

Goで実装した箇所を抜粋します。

func MaxStringWidth(lines []string) (max int) {
    for _, v := range lines {
        l := runewidth.StringWidth(v)
        if max < l {
            max = l
        }
    }
    return max
}

// AlignRight は文字列を右寄せする。
// 右寄せは見た目上の文字列の長さで揃える。
// length = -1のときは、引数文字列の最長の長さに合わせる。
// padは埋める文字列を指定する。埋める文字が見た目上でマルチバイトの場合は
// たとえlengthが奇数でも+1して偶数になるように調整する。
func AlignRight(lines []string, length int, pad string) []string {
    if length == 0 || len(lines) < 1 {
        return lines
    }

    // 空白埋めする文字列がマルチバイト文字かどうか
    padWidtn := runewidth.StringWidth(pad)
    padIsMultiByteString := padWidtn == 2

    // -1のときは文字列の長さをalignの長さにする
    // パッディングの長さと、処理対象の文字列のより長い方を揃える数値に指定
    maxWidth := MaxStringWidth(lines)
    if length == -1 {
        length = maxWidth
    } else if length < maxWidth {
        length = maxWidth
    }

    // マルチバイト文字を使うときは長さを偶数に揃える
    if padIsMultiByteString && length%2 != 0 {
        length++
    }

    ret := []string{}
    for _, line := range lines {
        l := runewidth.StringWidth(line)
        diff := length - l
        if diff%2 != 0 {
            line = " " + line
            diff--
        }
        // Repeatするときにマルチバイト文字を使うときは2分の1にする
        if padIsMultiByteString {
            diff /= 2
        }
        s := strings.Repeat(pad, diff) + line
        ret = append(ret, s)
    }
    return ret
}

まとめ

自作のコマンドalignについてとその実装について一部紹介しました。

この程度ならシェルスクリプトだけでも実現できるようにも思いましたが Goの勉強もしたかったのでGoで実装しました。

余談

こういう自作のコマンドをはてなブログに書くかScrapboxに書くかQiitaに書くか悩みます。 前まではQiitaに書いてたけれど、自作のコマンドの紹介とかははてなブログに 書いたほうが良いみたい。

まぁQiitaは一般的な技術的TIPS、事実を述べる場所で 自作のコマンドは「作った人個人」に紐づくと考えると ブログに書くのが適当なのも納得がいきました。

Scrapboxに書くことも検討したんですが、Scrapboxは意図的に 外部に広く知ってもらう機能を実装していないそうです。 それだとせっかく作ったツールを知ってもらいたくても知ってもらえないように 感じたので書かないことにしました。

Scrapbox自体はすごく良いサービスなので、 社内WikiとかプロジェクトのWikiとしてはぜひ使ってみたいです。

物語におけるキャラ同士のやり取りをシーケンス図で整理する

はじめに

FF6みたいに複数のチームを切り替えながら操作するシーンであったり 世界崩壊後の仲間が散り散りになったシーンで、 キャラ同士のやり取りの整合性をキチンと取りながらストーリーを考えるにはどうすればいいだろう。

って考えた時に、シーケンス図を書くと割といい感じに整理できることに気づいた。 Twitterに数カ月前にちょろっとだけ載せたけれど、 せっかくだったのでキチンと整理して公開しておこうと思った次第。

目次

わかりにくいストーリーの例

あるところ日、青年田中は精霊祭の日に神のお告げを聞いた。 その日、田中は町をでた。100年に一度復活を遂げる魔王を倒すために。 田中は最初の町で魔王の手先を倒した。
田中は2つめの町で魔王の手先と対峙した、しかし敵は強かった。
その時、偶然居合わせた山田と鈴木が助けてくれて、協力して敵を倒した。
山田と鈴木の話を聞くに、彼らの故郷は魔王の手下に滅ぼされたそうだ。

なんやかんやあって、いい感じにストーリーが進んで海の神殿についた。 そこには封印された勇者が眠っていた。
田中はそのとき、なんかすごいパワーが覚醒して封印を解き放った。 おまけに敵も復活したけれど、いい感じに倒せた。
山田と鈴木は勇者が居眠りぶっこいていたせいで故郷を滅ぼされたことに激昂して問い詰めた。 勇者は魔王に単身挑んだものの返りうちにされたこととか諸々説明して無罪放免。

あとはなんかいい感じに魔王を倒してハッピーエンド。


適当に書いたからすごくわかりにくいけれど、わかりにくいことが伝わったなら十分。 ストーリーを思いついた順番に説明してもいい感じに伝わるわけがない。 そんな時こそシーケンス図。

たぶんわかりやすくまとまったストーリー図の例

シーケンス図

こうしてみると、キャラが裏でどういうことしてたのかがよくわかる。 プレイヤーが操作可能になるまえから、物語は始まっていた。 これがシーケンス図のパワー。

シーケンス図ってなんぞや

Wikipediaを読めばわかる

シーケンス図 - Wikipedia

UML図の一種なのでUML図のリンクも貼る。

統一モデリング言語 - Wikipedia

もともとはシステム設計をする時のデータのやり取りや オブジェクトの振る舞いを定義するためのモデリング言語であって ゲームのシナリオを整理するためのものじゃない。

こんな複雑な図を書けというのか?

PlantUMLを使うとすごく簡単に書ける。

Open-source tool that uses simple textual descriptions to draw beautiful UML diagrams.

手軽にWebブラウザ上で試したいなら以下のサイトがすごく便利。

sujoyu.github.io

テキストファイルとして管理できるのでGitHubとの相性も良い。 ちなみに前述の図は以下のテキストから生成されている。

@startuml img/chara_seq.png

title 仲間が全員合流するまでの行動

actor 田中 as tanaka
actor 山田 as yamada
actor 鈴木 as suzuki
actor 勇者山勇者太郎 as hogeyama
actor ラスボス as last_boss

activate last_boss

last_boss -> last_boss : 復活した
last_boss -> last_boss : 世界侵略を開始
activate hogeyama
hogeyama -> hogeyama : 100年の眠りから目覚める
note right : ほげ山は過去の勇者とかそのへん
activate yamada
hogeyama -> last_boss : 一人で戦いに挑む
yamada -> suzuki : 一緒に遠方の街に\n買い出しにでかける
deactivate yamada
activate suzuki

last_boss -> yamada : 手下だけ向かわせて故郷を焼き討ちにかける

suzuki -> yamada : 故郷に戻ってきた
activate yamada

yamada -> yamada : 壊滅した故郷を目の当たりにする
yamada -> yamada : last_bossの手先を撃退する
yamada -> yamada : 魔王討伐のたびにでる
last_boss -> hogeyama : 返り討ちにして、\n海底神殿に封印する
deactivate hogeyama
suzuki -> yamada : 同行する
deactivate suzuki

=== ゲーム開始 ===

activate tanaka
tanaka -> tanaka : 年に一度の精霊祭に参加する
hogeyama -> tanaka : 封印されながらも根性と神通力で語りかける
tanaka -> tanaka : 始まりの街をでる
tanaka -> tanaka : 最初のボスを倒す
tanaka -> tanaka : 第二の街に到着する
tanaka -> tanaka : 第二の街を襲っている魔物と対峙する
yamada -> tanaka : 戦いに参加する

deactivate yamada
deactivate suzuki
tanaka -> team : チーム結成
deactivate tanaka
activate team
team -> team : なんやかんやある
team -> team : 海底神殿につく

team -> hogeyama : 田中のなんかすごいフォースで\n封印を解く
activate hogeyama
team -> team : 一緒にボスも復活
hogeyama -> team : 共闘でボスを倒す
hogeyama -> team : チーム加入
deactivate hogeyama
team -> team : なんやかんや
team -> team : ラスボス城に到達
team -> last_boss : 決戦に挑む
team -> last_boss : 勝利
deactivate last_boss


deactivate team

@enduml

すごく簡単というわけではないけれど、 パワーポイントよりは簡単なはず。

以上

Scrapboxから戻ってきました

はじめに

お久しぶりです。次郎です。 はてなブログに復帰したという記事です。

GitHubPagesに移行したあと、また移行してScrapboxを使っていたのですが、結局戻ってきました。 戻ってきた理由と、これからについて書きます。

戻ってきた理由

主に集客的な側面からです。

ScrapboxWikiであってブログではなく、 集客の側面については意図的に排除しているとの記事を見かけまして戻ってきました。

別に集客には特にこだわりもなかったですし、今も余り興味ないのですが 絵の配布、ついては別だったので、それのためだけに戻ってきました。

使われるための絵

今まで、たまに絵を書いて、気に入ったものがあったら公開して配布できる形にしてきました。

Scrapboxでも同様のことを行っていたので すが、Scrapboxよりははてなブログのほうがより効果的に人の目につくのでは、と考え ました。

もともと僕は自分がゲームを作る時に自分が使うように絵を書いて、 ゲームが完成するまで潜ませとくのがもったいなくて公開するようになったのですが 最近はゲームを作る熱が冷めて、何かの実装か絵だけを書くような感じになりました。

書いた絵をより使ってもらえるように、よりひと目につきやすいとこに置いとこう、という感じです。

まぁ、Wikiサービスをブログとかの代わりに使っていたのが、 本来の用途とアンマッチしていた感じ...でしょうか。

とはいえ、Scrapboxを使っているから人が集まらない、ということは絶対になくて 実際Scrapboxのサービス製作者のプロジェクトは色んな人に閲覧されています。 単純に、僕の発信力、影響力が弱い、というだけです。

戻ってくる == Scrapboxを使わない というわけではない

Scrapboxは引き続き使います。 が、メインで告知する環境にはしないというだけです。

依然として思考するためのサービスとしてScrapboxは非常に素晴らしいので 引き続きScrapboxに技術的なメモは残していきます。

これから

じゃあこっちでは何を書くのか、というとイラストの配布やツール配布とかだけにしようと思ってます。

このブログも、以前は技術の記事とイラストの記事が混在していて それぞれカテゴリが全然違うものだったので、このブログの立ち位置がわからなく感じてました。

Scrapboxに移行してから技術用とイラスト用でプロジェクトを分けていて、 イラスト配布用のプロジェクトの機能だけをこのブログが引き継ぐ形になります。 よって、ゲームとイラストに関連した情報のみになる予定です。

引き続き技術系の情報はScrapboxに書きます。あちらのほうが居心地が良いので。

しっかりとした情報として公開したいものは、多分Qiitaに書くのかなぁと思います。 Qiitaだと情報が間違ってる場合に指摘してもらえるので。

GitHubPagesにブログ移転します。

GitHubPagesにブログを移転します。 移転先は下記。

jiro4989.github.io

配布イラスト一覧は下記 https://jiro4989.github.io/dist-illust/

はてなブログはエンジニアの利用率が結構高い印象です。 一番のメリットはMarkdown記法に対応していることだと思ってます。 僕もMarkdownが使えるから、ということでFC2から移ってきました。

他にもメール送信で記事の投稿ができるので スクリプトと連携して自動化したり Vimで記事を書いたりできる。 SEO対策もしっかりされてますし、 はてなブックマーク数やスター数が表示されるのも結構便利だったんですが 投稿後に記事を修正するための編集ページが重たかったり もろもろ不満もありました。

github.ioのドメインと柔軟性の高さ、 ブログ記事の更新履歴を残せる、 管理がやりやすい、 エディタが選べる、コマンドラインと相性が良い、自動化が簡単など いろんなメリットから移転を決意しました。

移行こちらのブログはおそらく更新しません。 イラストの配布についても新しいブログの方に告知していきます。

以上です。