Ruby で png 画像を自力生成できるらしいですよ

このところ脳みそが腐って全然プログラミングとかできないのだが、Ruby 愛はちっとも減じていないので、他人のブログをパクってエントリを書きます。「パクって」はいい言葉じゃないですね。とにかく唐突に「png ruby」でぐぐってみましたら、Ruby コミッタのまめさんのブログがヒットしました。
だいぶ昔の記事ですね。ほほう、Rubypng 画像を生成するかあ。これはおもしろそうですね。png の生成は結構むずかしいらしいのですが、Ruby だとそんなにむずかしくないと。とりあえず、まめさんのコードをコピペ、実行してみます。赤色のグラデーションをもった画像を生成します。もともとは Ruby 1.9 用らしいですが、Ruby 2.3.3 でまったく修正なしに走りました。(Linux Mint 18.2)

コード(コピペほぼそのまま)。
gradation.rb

require "zlib"

width, height = 100, 20
depth, color_type = 8, 2

# グラデーションのベタデータ
line = (0...width).map {|x| [x * 255 / width, 0, 0] }
raw_data = [line] * height

# チャンクのバイト列生成関数
def chunk(type, data)
  [data.bytesize, type, data, Zlib.crc32(type + data)].pack("NA4A*N")
end

# ファイルシグニチャ
print "\x89PNG\r\n\x1a\n"

# ヘッダ
print chunk("IHDR", [width, height, depth, color_type, 0, 0, 0].pack("NNCCCCC"))

# 画像データ
img_data = raw_data.map {|line| ([0] + line.flatten).pack("C*") }.join
print chunk("IDAT", Zlib::Deflate.deflate(img_data))

# 終端
print chunk("IEND", "")

"zlib" ライブラリは標準添付ライブラリなので、Gem としてインストールする必要はありません。実行は

$ ruby gradation.rb > gradation.png

みたいな感じ。こうして生成された png 画像はこれだ!
 

 
小さいけれど、ちゃんと赤色のグラデーションとして生成されていますね。うーん、いいなあこれ。


ふつうのブログならこれを解説するわけだが、面倒なのでしない。というか、まめさんの説明がすごくわかりやすいので、自分のは不要なのである。といって、じつは自分はまめさんの記事を読んで密かにぐぐったので、それだけ書いておこう。

まず、標準添付ライブラリの "zlib" が話題になっているが、これって何か? これはここを見るといいですね。つまり zlib とは、データを圧縮するアルゴリズムであるそうだ。Linux では gzip コマンドというので使われているらしいが、しかしよく Windows で使われている zip ってやつとは関係ないらしい。zlib は「可逆圧縮」、つまり解凍すると完全にもとのデータが復元できるということで、じつは png 画像ファイルの圧縮もこれであり、そう、png は劣化しないのですね。これは皆んな知っているだろう。ちなみに jpg は劣化するのですね。というわけで、png 画像を作るには、メインの画像データは zlib で圧縮すればよいというわけである。そのためのライブラリなのだった。

あとひとつ。まめさんの「臼NG はみんな見たことあるはず」がわからなかった。「臼NG」である。自分は見たことがない。これはわからないということでぐぐったら、ここがヒットした。ははあ、ファイルシグニチャ*1の「\x89P」が Shift_JIS で「臼」なわけね。納得。ただし、これを LinuxRuby で実感してみようと思ったが、

require 'kconv'

puts "\x89PNG".tosjis.encode("UTF-8")     #=>臼NG

しかやり方がわからなかった。"kconv" を使うのは Ruby 2.3 っぽくないのだけれど*2。どうやるといいのかな。(つまりは、バイナリ→Shift_JISUTF-8 の変換を文字列ですること。これ、すごく時間をかけて調べたのだけれどわからなかった。もちろんマジックコメントを使えば簡単なのだが。)


自分も上のコードを少しだけ変えて、完全にランダムな色で打点した png 画像を作ってみた。コードは変更部分のみ。
random_color_png.rb

width, height = 100, 100

# グラデーションのベタデータ
raw_data = []
height.times do
  raw_data << width.times.with_object([]) do |i, ob|
    ob << [rand(256), rand(256), rand(256)]
  end
end

画像。
 


こんな具合ですね。

*1:png ファイルの先頭部分で、これは png ファイルですよという宣言。

*2:どうも最近の Ruby だと、エンコーディングをわざとまちがえて(笑)文字化けさせるということが想定されていないらしい。文字列が自分のエンコーディングの情報をもっています。で、バイナリから例えば UTF-8 へという無意味な変換ができないらしい。そういうことをやると Encoding::InvalidByteSequenceError が出ます。