この前ドット絵のアイコン書いたんですよ、こんなの。
結構うまくできたかなって満足してます。
それで、このアイコン、16x16なので、そのくらいならTwitterに140文字にして投稿できないかなって思ったんです。
そういうお話。
バイナリデータである画像を文字列に圧縮するにあたって、次のルールを決めました。
- 140文字に納めること(バイト数は問わず = マルチバイト文字も1文字)
- 一般に使われてる処理系(Rubyなど)やエンコード方式(Base64など)は前提にしてもよい
- それ以外のエンコード方式などは、デコード処理も含めて140文字以内に納める
これをふまえて、方針を立てます。 スタート時点で、画像のサイズは16x16のpngで180バイト程度です。
単純にBase64方式でエンコードする方法を最初に考えました。 でも、Base64はデータ量が133%になる = 105バイト以下のデータの必要がある のでダメです。
じゃあやっぱりマルチバイト文字が使えるってことを利用するしかないねってことで、次の方針を立てました。
- 元データの画像をできるだけ圧縮する
- マルチバイト文字を使って1文字にできるだけ多くの文字を詰め込む。
- デコード処理をできるだけ短く書く
それでは、実際に進めていきます。
はじめに、画像をできるだけ小さく圧縮します。
画像形式はPNGとGIFの両方について、画像サイズ圧縮のwebサービスを使いました。 その結果、最もサイズが小さくなったのはGIFファイルを
http://tools.dynamicdrive.com/imageoptimizer/index.php
で圧縮した場合でした。
これで画像サイズが142バイトになりました。
文字列への変換において必要な条件は以下の2点です。
- (Twitterにポストできるような)有効な文字である
- 任意のデータを表すことができる
まずは一般的なエンコード方式ってことで UTF-8 について調べます。
- http://ja.wikipedia.org/wiki/UTF-8#エンコード体系
- http://www.unicode.org/Public/UNIDATA/Blocks.txt
- http://ja.wikipedia.org/wiki/Unicode文字のマッピング#非文字
わかったことを以下に示します。
- 規格として表現できる範囲は0x000000〜0x10FFFF
- 割り当てられている領域は連続していない(抜けがある)
- ほとんどの領域(0x01FFFF〜0x10FFFF)は "Supplementary Private Use Area" (何が入ってるんだ・・・)
- 0xFEFFなど非文字が存在する(0x0000(NUL)などの制御文字もありますね・・・)
これらをふまえると、単純に例えば0x0000〜0xFFFF(2バイト)の領域をそのまま使ってエンコードするのは難しく、 一部の領域に限定して使用する必要がありそうです。
さきほどの、文字の割当領域リストから、一番広いものを探してみると、
4E00..9FFF; CJK Unified Ideographs
というものがあります。漢字エリアです。この中に無効な文字が含まれている可能性はありましたが、 領域を一つにしぼってとりあえず圧縮してみます。
この範囲は、末尾から先頭を引いて
0x9FFFF - 0x4E00 = 0x51FF
なので、
0x51FF > 0x3FFF = 0b 0011 1111 1111 1111 (14bit)
ということで14bitは確保できそうです。
それでは実際に圧縮のコードを書いていきます。 私はRubyを使いました。
コードの流れとしては、
- データを読み込む
- 14bitごとにチャンクに分割する
- 14bitのチャンクを 0x4E00 から14bitの範囲に変換(単純に0x4E00を足せばよい)
- その値をUTF-8にエンコードする(14bitの値が1文字になる)
- すべての14bitチャンクに対して3-4の処理を繰り返す
- 圧縮結果を出力する
となります。
data = File.open(ARGV[0]){|f| f.read } # ファイルの読み込み
bit_str = data.unpack("B*")[0] # 1bitずつ0/1で表された文字列に変換
encoded_data =
bit_str.split(//)
.each_slice(14) # 14bitずつ処理
.map{|arr| arr.join.to_i(2) + 0x4e00 } # 0/1の文字列を数値に変換し0x4E00を足す
.pack("U*") # UTF-8エンコード
.join
puts encoded_data
これで、
忒扣滥漐丄丏嘀一一呵歃捚松尘跷哅跿跰一一一借爐伀一丂縀一丄丁一七拊墭凸渄嚆故徙翛
劖稅茑坼嚬兆柮堦埱味甝乱痮疾曵沰坊嵈礚潍皌脧洭亲縕嘪彐硶殄绊椛綑峄噼筴掠褋址浱唂与七
という文字列に画像データを変換できました。(適宜改行しています) これで82文字です。
次にデコードの処理を書いてみましょう。
デコードの処理はエンコードの処理を逆にたどるだけですので、以下の通りです。
- 文字列を1文字ずつUTF-8としてデコードする(数値が得られる)
- 得られた数値から0x4E00を引いて14bitのチャンクを得る(数値は14bitの範囲に収まる)
- それぞれの文字について同様に14bitのチャンクを取り出し、結合する
- データを出力する
str = "忒扣滥漐丄丏嘀一一呵歃捚松尘跷哅跿跰一一一借爐伀一丂縀一丄丁一七拊墭凸渄嚆故徙翛劖稅茑坼嚬兆柮堦埱味甝乱痮疾曵沰坊嵈礚潍皌脧洭亲縕嘪彐硶殄绊椛綑峄噼筴掠褋址浱唂与七"
data01 =
str
.unpack("U*") # 文字ごとにUTF-8としてデコード
.map{|n| n-0x4e00 } # 得られた値から0x4E00を引く
.map{|n| sprintf("%14b", n } # 14bit長の0/1文字列に変換
.join # 14bit長の0/1データを結合
data = [data01].pack("B*") # 0/1文字列をバイナリに変換
print data
これで元のデータを取り出すことができました。やったー!
$ ruby icon.rb > icon.gif
とすることでアイコンのGIF画像が得られます。
あとは、このコードをできるだけ短くしていきます。
- 変数代入をなくして、2つのmapは統合
sprintf
の代わりにString#%
0x4e00
は10進表記の19968
に
実際に適用した結果がこちら。
print ["忒扣滥漐丄丏嘀一一呵歃捚松尘跷哅跿跰一一一借爐伀一丂縀一丄丁一七拊墭凸渄嚆故徙翛劖稅茑坼嚬兆柮堦埱味甝乱痮疾曵沰坊嵈礚潍皌脧洭亲縕嘪彐硶殄绊椛綑峄噼筴掠褋址浱唂与七".unpack("U*").map{|n|"%14b"%[n-19968]}.join].pack('B*')
これで146文字です。 目標の140文字にはまだ届きませんが、"ファイルをデコード処理を含んだ文字列に変換する"ということは実現できました!わーい!
140字に納めるにはもっと短くする必要がありますが、デコード処理の部分はもうムリ。。。 データ部分をどうにか短くできないか考えます。
エンコード後の文字列は80文字程度。無作為にUnicodeの範囲から選んだとしても、有効な文字が存在する確率ってとても高そうですね。
さっきまでのコードでは、 元のデータを14bitごとに 0x4E00 シフトして、UTF-8としてエンコードしました。 ここで、元のデータを15bitごとに分割しても、"ある適当な量" シフトさせれば運良く全部のチャンクが有効な文字に当たるのでは??と考えました。
そこで、チャンクの長さを15bitに変え、試行錯誤でいろいろシフト量を変えてみました。
その結果、シフト量 0x5000 で、すべての文字が有効な文字列になり、ちょうど140文字に納まりました!!!目標達成!!
print ['玤ꆎ圬愀傀又倀倀倳ꗔ檫癟霌迷嶋쿿쾀倀倀倂忈怄倀倀昀倀倂倁倀倍癑竐켐儈錮镆莎뱉薀굄璾墬嚌랸ꄱ漑鹳靀裏뺞쳇ꗫ儩珒斍煍ꄙ鲝쥨嬬劤媑硔왶妆禱귲掱吾絴筁밬騃윑끀倻'.unpack('U*').map{|n|'%15b'%[n-20480]}.join].pack('B*')
※もちろん、元のデータが変われば適したシフト量は変わってしまいます。 この後、すべての文字が有効な文字になるまでシフト量を変えて試すスクリプトを書きました。
もっと短くするには、チャンク長を16bitにするしかありません。
シフト量を変えて試すスクリプトを使って、16bitチャンクに適したシフト量を探しました。 その結果、適したシフト量の一つとして 0x0025 が挙げられました。 エンコード後の文字列が以下です。
䝮䙝㦆ဥဥ%%杼僺媊句逢웪𐀤E%%F朗ĥ%Q%%ဥဥ(卍ꫵ﹅чᦙ咋爀ቿ삂䑮
籇끙暠詋⟩杞읥燄몠廕╍웆䷇㍗矰䃗쁹苉呏皛ጱꛫ㬵蟰崺ꄑⲹࠁ䜧`
71文字です・・・!!
しかも、チャンクが16bitということで、デコードの処理も簡単になります。
print '䝮䙝㦆ဥဥ%%杼僺媊句逢웪𐀤E%%F朗ĥ%Q%%ဥဥ(卍ꫵ﹅чᦙ咋爀ቿ삂䑮籇끙暠詋⟩杞읥燄몠廕╍웆䷇㍗矰䃗쁹苉呏皛ጱꛫ㬵蟰崺ꄑⲹࠁ䜧`'.unpack('U*').map{|n|n-37}.pack('n*')
116文字まで短くなりました!!!!!!!
http://twitter.com/kyubing/status/358186039858307073
2015/7/24 動作確認 https://twitter.com/kyubing/status/624462346581180417
これを読んだ方は、自分のアイコンも140文字化したくなったことでしょう? まずは小さいアイコンを作るのです・・・!
最後に、これをやっているときに助言をくれた各位ありがとうございました。
実際のスクリプトなどはこちらにあります: https://gist.github.com/clicube/6031642