KOTET'S PERSONAL BLOG

#tech 紙とペンでビットマップ画像を生成してbase64としてツイート

Created: , Last modified:
#tech

これは1年以上前の記事です

ここに書かれている情報、見解は現在のものとは異なっている場合があります。

文名は容易に揚らず、生活リズムは日を逐うて崩れてゆく。 Kotetは漸く焦躁に駆られて来た。 この頃からその容貌も峭刻となり、肉落ち骨秀で、眼光のみ徒らに炯々として、 曾て進士に登第した頃の豊頬の美少年の俤は、何処に求めようもない。 数年の後、貧窮に堪えず、自らの衣食のために遂に節を屈して、再び東へ赴き、 一地方アルバイトを奉ずることになった。 数日の後、公用で旅に出ること叶わず初日にて欠勤した時、遂に発狂した。或夜半、 急に顔色を変えて寝床から起上ると、 base64何か訳の分らぬことを叫びつつそのまま下にとび下りて、 闇の中へ駈出した。彼は二度と戻って来なかった。附近の山野を捜索しても、何の手掛りもない。 その後Kotetがどうなったかを知る者は、誰もなかった。1

ビットマップ画像を手書きしてツイートしよう

かなり前に D で ビットマップ形式の画像を生成するプログラムを書いた記憶がある。 その時のソースコードは残っていないが、けっこう単純なフォーマットだった。 ひょっとしたら全くコンピューターに頼らずに同じことができるのではないかと思い立った。 というわけで画像を手書きしていく。

目標は次の3つである。

  1. ビットマップ画像を紙とペンを使って生成
  2. できたバイナリを紙とペンを使って base64 でエンコード
  3. Data URL としてスマートフォンのキーボードを使ってTwitterに投稿

特殊なものは使わない。 誰でも持っている道具だけで画像をツイートしてみる。

ツイートできる最大サイズ

この方式でツイートできる画像の大きさはどれくらいになるだろうか? まず、出てくる文字はすべて ASCII の範囲内なので文字数制限は280文字と考えていいだろう。 ここから画像データに使えない領域を引いていく。

まず、Data URL なので最初の data:image/bmp;base64, は必須である。 これでまず22文字引かれる。 残りは258文字となる。

base64 エンコードを行うため、文字数に対して表現できるバイト数は4分の3になる。 また、base64 文字数を4の倍数にするためのパディングが必要になる。 したがって残りの文字数で表現できるデータ量は192バイトとなる。

まず必要になるのが画像のヘッダ領域だが、これはフォーマットの選択によって減らすことができる。 しかし頑張って作ったのにビューアが対応してなくて表示できなかったりすると悲しいので、 全部で54バイト必要なWindows形式を採用した。 残りは138バイトである。

最後にビットマップ、画像データである。 これも形式を選択したり圧縮したりで画素あたりのデータ量をかなり減らすことができるのだが、 これもなんかめんどくさいので一般的な1画素あたり24ビット = 3バイトを選択しよう (ズボラ)。 138を3で割ると46となるので、この条件でツイートできる画像の最大画素数は46である。 ただし行を4バイト境界でアラインする必要があるので、横幅によっては大きなロスが発生する。 それも考慮して、現実的に画像として使える縦横比のものを探すと横4ピクセル、 縦11ピクセルの44画素くらいになるだろう。

画像の作成

というわけで画像 (のバイト列) を作っていく。 今回作るのはこの縦横2ピクセルの画像である。 左上から順に赤 (#FF0000)、緑 (#00FF00)、青 (#0000FF)、白 (#FFFFFF) で塗ってある。 最初はがんばって絵を書こうとしたが、画像が大きくなるに従って計算ミスも増えてくるのでしかたない。

お題

ファイルヘッダー

ファイルタイプ

必ず先頭にBMの2文字が入る。 Aが0x41であるということさえ覚えておけば計算できるだろう。

42 4D
ファイルサイズ

ファイル全体のバイト数。 32ビット整数。 変な数値を入れても入れなくてもまともなビューアーはよしなに表示してくれるらしい。 ノートに余白を設けておいて最後に計算するといいだろう。

今回は70バイトだったので00 00 00 46(00000000 00000000 00000000 01000110)である。 リトルエンディアンにしなければならないので実際のバイト列はこれを反転したものになる。 今回255を超える数値は登場しないので、何も考えずに数値の後ろにゼロを連ねるだけで良くて非常に楽。

46 00 00 00
予約領域

2バイトの予約領域が2つある。 ここはゼロ埋めしなければならない。

00 00 00 00
画像データまでのオフセット

画像データが何バイトめから始まるかを書く。 これも32ビット整数。 ヘッダのバイト数である54 = 0x36を書いたが、これでよかったのだろうか?

36 00 00 00

情報ヘッダー

情報ヘッダーのサイズ

Windows 式なので40バイト。 これも32ビット整数。

28 00 00 00
画像サイズ

ピクセル数。 幅、高さの順番にどちらも符号付き32ビット整数。 高さは通常正の数にするが、負にすると上下が反転して 後述する 画像データの並びがわかりやすくなるらしい。 わざわざ負数にして計算ミスを起こしたくないし、変なことして表示されなかったら悲しいので、 ここはお行儀よくしておく。

02 00 00 00
02 00 00 00
プレーン数

これは16ビット整数。 「ターゲット・デバイスのプレーンの枚数」らしいが、1で固定である。 何を想定しているんだろうか。

01 00
色ビット数

1画素を表現するのに使うビット数。 これも16ビット整数。 0 (プリンター等で使う)、1、4、8、16 (「正式に対応していない」らしい)、24、32から選ぶことができる。 今回は一般的な8ビット×3の24ビット。

18 00
圧縮形式

条件によってはちょっとした圧縮をかけることができるらしい。 32ビット整数。

今回小さな画像を作っているのでバイト列にゼロが多い。 しかし大きい画像の場合はファイルサイズなど32ビット整数でないと不十分なときもあるだろう。 それを考慮してもこの項目は今までで一番ムダにスペースをとっているように思える。 4億通りも圧縮形式の選択肢ができるものだろうか? 余ったビットに圧縮に必要な情報でも入れるのだろうか。

今回は色ビット数に24を指定してしまったので0 (無圧縮) 1択である。 ビットマップのくせに圧縮なんて生意気なんだよ (暴言)

00 00 00 00
その他

この後にも画像データサイズ、解像度、パレット数、重要色数と項目が続いているが、今回の場合はすべてゼロでいい。 5項目すべて32ビット整数。

00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00

画像データ

いよいよ絵を書いていく。 いくつか注意点がある。 まず画像は下から上に向かって描画されていく。 したがって行は上下反転した順番に並ぶ。 次に、行は4バイトの境界でアラインされている必要がある。 今回は1行が24ビット×2=6バイトになっているので、2バイトのパディングを入れてやらなければならない。 最後に、色の並び順は RGB ではなく BGR である。

FF 00 00 FF FF FF 00 00
00 00 FF 00 FF 00 00 00

base64でエンコード

以上でバイナリデータが完成したので、base64にしていく。 参考までにここまでで作ってきたビットマップ画像の全体を載せておく。

42 4D
46 00 00 00
00 00 00 00
36 00 00 00
28 00 00 00
02 00 00 00
02 00 00 00
01 00
18 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
FF 00 00 FF FF FF 00 00
00 00 FF 00 FF 00 00 00

base64はASCIIの文字だけを使ってバイナリデータを表現するフォーマットである。 6ビットを1文字で表現するため、データ量としては3分の4に増えることになる。

3バイトごとに区切る

上のバイト列から毎回6ビットごと切り出して変換していくのは大変に辛そうなので、 lcm(6,8) = 24ビット = 3バイトごとのブロックに分けて変換していく。 上のバイト列を3バイトごとに改行するようにしたのがこちらである。

42 4D 46
00 00 00
00 00 00
00 36 00
00 00 28
00 00 00
02 00 00
00 02 00
00 00 01
00 18 00
00 00 00
00 00 00
00 00 00
00 00 00
00 00 00
00 00 00
00 00 00
00 00 00
FF 00 00
FF FF FF
00 00 00
00 FF 00
FF 00 00
00

00 00 00が多くて楽そうだ。 というわけで変換していく。

ブロックの変換

最初の42 4D 46を変換してみる。 まずはバイト列を2進数に直してみる。 元が16進数なので1文字づつ置き換えていけばいい。

0100 0010 0100 1101 0100 0110

次にこれを6ビットごとに区切る。

010000 100100 110101 000110

ひとつづつ変換していく。 ビット列と文字の対応は大文字、小文字、数値、+、/ という並びになっており規則性があるが、 毎回計算すると (少なくとも自分は) 必ずミスをするので Wikipediaの対応表などをしっかり見ながらやろう。 すると3バイトのブロックが以下のように4文字にエンコードされる。

Qk1G

最後の処理

一番最後の1バイトが仲間を作れないでいる。 ここでは上に加えて特別な処理が必要になる。

まず2進数にして6ビットごとに区切ってみる。

000000 00

余った2ビットの後ろに0を続けて、6ビットに揃える。

000000 000000

そして同じように変換する。

AA

これで終わりではない。 文字列の長さが4の倍数になるように=でパディングをしてやる必要がある。 ここまでブロックを4文字に変換してきたので、ここも4文字になるようにしてやればいい。

AA==

というわけでできた文字列がこちらになる。

Qk1GAAAAAAAAADYAAAAoAAAAAgAAAAIAAAABABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAA////AAAAAP8A/wAAAA==

ちゃんと画像になっているかテストしてみよう。 Arch Linuxの場合base64というコマンドが coreutils の中に入っている。

$ echo "Qk1GAAAAAAAAADYAAAAoAAAAAgAAAAIAAAABABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAA////AAAAAP8A/wAAAA==" | base64 -d > test.bmp

完成、そしてツイート

ノート

ノート1ページ (と複数の失敗ページ) の計算の結果、base64にエンコードされたビットマップ画像が完成した。 後はdata:image/bmp;base64,をくっつけてツイートするだけだ。

完成した画像を下に貼っておく。 たった4ドットだが、たしかにそこにある。

画像

参考