full-on-chain pixel art NFT

こんにちは。double jump.tokyo CEO 上野です。
仕事が多忙な時期に何も考えずアドベントカレンダーへ参加表明してしまったため、夜中に記事（というかオープンソース）を書くハメになってしまいました。
先ほど公開したオープンソースは「SolidityでBitmapファイルフォーマットの画像を扱う」ライブラリです。
https://github.com/doublejumptokyo/solidity-bitmap
# 使い方
誰か使ってくれる人がいたらgithubに使い方を書いてくれるだろうというOptimisticな気持ちで最低限書きます。
<dl>
  <dt>contracts/Bitmap.sol</dt>
  <dd>Bitmapを扱うライブラリで、struct Bitmapが本体です。initしてsetPixelしていけば、dataにBMPバイナリデータができあがっています。</dd>
  <dt>contracts/BitmapNFT.sol</dt>
  <dd>Bitmap.solの利用例ですが、凝りすぎてしまって複雑かも。</dd>
</dl>
# モチベーション
今年、2021年はまさに（世間の注目を浴びたという意味で）NFT元年と言って良い年でした。アートNFTからゲームで使えるユーティリティNFTまで様々なNFTに価値が認められる中、とりわけfull-on-chain NFTという外部ストレージに依存しないNFTが通の間で嗜好品として重宝されています。
とはいえ、大きな画像データをそのままon-chainに乗せてしまうのは、gas feeも高い上になにより世界共有DBのストレージを食ってしまうのでできれば避けたい。そのため、プログラム生成するGenerativeな画像やベクターデータ（SVG）でコンパクトになった画像がよく使われます。
画像サイズとしてはコンパクトな32*32くらいのドット絵（Pixel Art）もon-chainに乗せるのに適していますが、意外とBitmapフォーマットをそのまま扱うライブラリがないのに気づきました。（SVGでドットをrectで扱うようなものはありますが）Bitmapフォーマットは比較的素直なバイナリフォーマットなので、ちょっと手を加えればSolidityでも扱えるんじゃね？という軽い気持ちで作りはじめました。
# Bitmapファイルフォーマット
Bitmapファイルフォーマットについては以下の記事を参考にしました。
https://qiita.com/ledsun/items/9ca815739201ea395ed2
32bit BMP ではなく 24bit BMP なところがお気に入りです。（RGB 8bit*3色=24bitなら32bitは余計）
この記事内にあるようにBitmapは54バイトのヘッダー情報を作るところが面倒ですが、ボディ情報はRGBを埋めていくだけなので単純です。リトルエンディアン方式なところだけ注意が必要です。

```solidity:contracts/Bitmap.sol
pragma solidity ^0.8.0;

struct XY {
    int256 x;
    int256 y;
}

struct RGB {
    bytes1 red;
    bytes1 green;
    bytes1 blue;
}

struct Bitmap {
    bytes data;
    uint256 linePadding;
    uint256 lineSize;
}

library BitmapLib {
    uint256 constant headerSize = 54;
    uint256 constant pixelSize = 3;

    function init(Bitmap memory bitmap, XY memory size) internal pure {
        bitmap.linePadding = (uint256(size.y) * pixelSize) % 4 == 0
            ? 0
            : 4 - ((uint256(size.x) * pixelSize) % 4);
        bitmap.lineSize = uint256(size.x) * pixelSize + bitmap.linePadding;
        uint256 bodySize = bitmap.lineSize * uint256(size.y);
        uint256 fileSize = headerSize + bodySize;
        bitmap.data = new bytes(fileSize);

        // これがBitmapヘッダ領域
        bitmap.data[0] = 0x42;
        bitmap.data[1] = 0x4d;
        setUint32(bitmap, 2, uint32(fileSize));
        setUint32(bitmap, 10, uint32(headerSize));
        setUint32(bitmap, 14, 40);
        setUint32(bitmap, 18, uint32(int32(size.x)));
        setUint32(bitmap, 22, uint32(int32(size.y)));
        setUint16(bitmap, 26, 1);
        setUint16(bitmap, 28, uint16(pixelSize * 8));
        setUint32(bitmap, 34, uint32(bodySize));
    }

    // Bitmapボディ領域は素直に詰めるだけ
    function setPixel(
        Bitmap memory bitmap,
        XY memory position,
        RGB memory pixel
    ) internal pure {
        uint256 index = headerSize +
            uint256(position.y) *
            bitmap.lineSize +
            uint256(position.x) *
            pixelSize;
        bitmap.data[index] = pixel.blue;
        bitmap.data[index + 1] = pixel.green;
        bitmap.data[index + 2] = pixel.red;
    }

    // Bitmapボディ領域をそのまま詰めても良い
    function setBody(Bitmap memory bitmap, bytes memory body) internal pure {
        uint256 bodyLength = body.length;
        require(
            bitmap.data.length == headerSize + bodyLength,
            "invalid body size"
        );
        for (uint256 i = 0; i < bodyLength; i++) {
            bitmap.data[headerSize + i] = body[i];
        }
    }

    // リトルエンディアンでuint32を詰める
    function setUint32(
        Bitmap memory bitmap,
        uint256 offset,
        uint32 value
    ) private pure {
        for (uint256 i = 0; i < 4; i++) {
            bitmap.data[offset + i] = bytes1(uint8(value / (2**(8 * i))));
        }
    }

    // リトルエンディアンでuint16を詰める
    function setUint16(
        Bitmap memory bitmap,
        uint256 offset,
        uint16 value
    ) private pure {
        for (uint256 i = 0; i < 2; i++) {
            bitmap.data[offset + i] = bytes1(uint8(value / (2**(8 * i))));
        }
    }
}
```

# 今後
Bitmapファイルフォーマットの保存を考えた場合、もう少し改善の余地がありそう。
いずれにしても、32*32や64*64くらいのBitmapファイルならある程度コンパクトに保存も可能なので、そのあたりのユースケースが増えてくれば改善していけるかなと思っているが、ひとまずこれくらいコンパクトな仕様で良いだろう。
というか、まずはマイクリくらいの64*64Bitmapなら使えるんじゃないかな、と思う。