GIF 画像データを作る

# GIF データ生成用クラス

bytearray の派生として [GIF 形式](https://qiita.com/ikiuo/items/69de0087e358a2ec6949)のデータを生成します。

```python:mkgif.py
#!/usr/bin/env python3

class File(bytearray):
    def __init__(self, version=b'89a'):
        super().__init__()
        self += b'GIF' + version
        self.logical_screen_width = 0
        self.logical_screen_height = 0

    @staticmethod
    def fix_color(data):
        color = []
        for d in data:
            if type(d) in (bytes, bytearray, list, tuple):
                color += d
            else:
                color.append(d)

        mod3 = len(color) % 3
        if mod3:
            color += [0] * (3 - mod3)
        color_size = 3
        size = 0
        while color_size < len(color):
            color_size <<= 1
            size += 1
        color += [0] * (color_size - len(color))
        return (color, size - 1)

    def append_data(self, data): self += bytes(data)
    def append_bytes(self, *data): self += bytes(data)
    def append_word(self, word): self += bytes((word & 255, (word >> 8) & 255))
    def append_words(self, *data): [self.append_word(word) for word in data]

    def append_sub_blocks(self, data):
        pos = [i for i in range(0, len(data), 255)] + [len(data)]
        cnt = [pos[i + 1] - pos[i] for i in range(len(pos) - 1)]
        for i in range(len(cnt)):
            p, n = pos[i], cnt[i]
            self.append(n)
            self.append_data(data[p:p+n])
        self.append(0)

    # Descriptor

    def append_logical_screen_descriptor(
            self, width, height, color_resolution=7, sort_flag=False,
            background_color_index=0, pixel_aspect_ratio=49,
            global_color=None):
        self.logical_screen_width = width
        self.logical_screen_height = height
        packed_fields = (color_resolution & 7) << 4
        if sort_flag:
            packed_fields |= (1 << 3)
        if global_color:
            global_color, color_size = File.fix_color(global_color)
            packed_fields |= (1 << 7) | color_size
        self.append_words(width, height)
        self.append_bytes(packed_fields,
                          background_color_index,
                          pixel_aspect_ratio)
        if global_color:
            self.append_data(global_color)

    def append_image_descriptr(
            self, image_data, left=0, top=0, width=None, height=None,
            local_color=None, sort_flag=False, interlace_flag=False):
        image_data = bytes(image_data)
        if width is None:
            width = self.logical_screen_width
        if height is None:
            height = self.logical_screen_height
        packed_fields = 0
        if sort_flag:
            packed_fields |= (1 << 5)
        if interlace_flag:
            packed_fields |= (1 << 6)
        if local_color:
            local_color, color_size = File.fix_color(local_color)
            packed_fields |= (1 << 7) | color_size
        self.append(0x2c)
        self.append_words(left, top, width, height)
        self.append(packed_fields)
        if local_color:
            self.append_data(local_color)
        lzw_minimun_code_size = 0
        image_data_max = max(image_data)
        while (1 << lzw_minimun_code_size) <= image_data_max:
            lzw_minimun_code_size += 1
        if lzw_minimun_code_size < 2:
            lzw_minimun_code_size = 2
        self.append(lzw_minimun_code_size)
        self.append_sub_blocks(File.LZWEncode(lzw_minimun_code_size, image_data))

    def append_trailer(self):
        self.append(0x3B)

    # Extension

    def append_graphic_control_extension(
            self, delay_time=0, tranparent_color_index=None,
            disposal_method=1, user_input_flag=False):
        packed_fields = (disposal_method & 7) << 3
        if user_input_flag:
            packed_fields |= (1 << 6)
        if tranparent_color_index:
            packed_fields |= (1 << 7)
        else:
            tranparent_color_index = 0
        self.append_bytes(0x21, 0xF9, 4, packed_fields)
        self.append_word(delay_time)
        self.append_bytes(tranparent_color_index, 0)

    def append_comment_extension(self, comment):
        self.append_bytes(0x21, 0xFE)
        self.append_sub_blocks(comment)

    def append_application_extension(self, identifier, code, data):
        identifier = bytes(identifier)
        if len(identifier) < 8:
            identifier += b' ' * 8
        code = bytes(code)
        if len(code) < 3:
            code += b' ' * 3
        self.append_bytes(0x21, 0xFF, 11)
        self += identifier[:8]
        self += code[:3]
        self.append_sub_blocks(data)

    def append_animaition_loop(self, count=0):
        data = bytes([1, count & 0xff, (count >> 8) & 0xff])
        self.append_application_extension(b'NETSCAPE', b'2.0', data)

    class BitStream(bytearray):
        def __init__(self):
            super().__init__()
            self.bit_pos = 0

        def write(self, data, bits):
            bpos = self.bit_pos & 7
            self.bit_pos += bits
            if bpos != 0:
                data <<= bpos
                bits += bpos
                self[-1] |= data & 0xff
                data >>= 8
                bits -= 8
            for n in range((bits + 7) >> 3):
                self.append(data & 0xff)
                data >>= 8

    class LZWEncode(BitStream):
        def __init__(self, minbits, data):
            super().__init__()
            self.bits_init = minbits
            self.code_clear = 1 << self.bits_init
            self.code_end = self.code_clear + 1
            self.code_max = (1 << 12) - 1

            self.clear()
            self.write(self.code_clear)
            s = ''
            for b in data:
                c = chr(b)
                s += c
                if s in self.code_tree:
                    continue
                self.write(self.code_tree[s[:-1]])
                self.code_curr += 1
                if self.code_curr >= self.code_step:
                    self.bits_curr += 1
                    self.code_step <<= 1
                if self.code_curr < self.code_max:
                    self.code_tree[s] = self.code_curr
                else:
                    self.write(self.code_clear)
                    self.clear()
                s = c
            self.write(self.code_tree[s])
            self.write(self.code_end)

        def clear(self):
            self.bits_curr = self.bits_init + 1
            self.code_curr = self.code_end
            self.code_step = 1 << self.bits_curr
            self.code_tree = {chr(n): n for n in range(self.code_clear)}

        def write(self, data):
            super().write(data, self.bits_curr)
```


# 使用例

以下、静止画と動画の生成例です。

## 静止画

```python:静止画(sample-1.gif)の生成
import mkgif

width = 256   # 画像の幅
height = 64  # 画像の高さ

# 256色
color = tuple((n & 0xe0, (n << 3) & 0xe0, (n << 5) & 0xc0) for n in range(256))

# mkgif.File オブジェクトを用意
gif = mkgif.File()

# Logical Screen Descriptor は最初で必須
gif.append_logical_screen_descriptor(width, height, global_color=color)

# 画像データ (8ビットのデータを width * height 個)
gif.append_image_descriptr(n & 0xff for n in range(width * height))

# Trailer は最後で必須
gif.append_trailer()

# ファイルに出力
open('sample-1.gif', 'wb').write(gif)
```

![sample-1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226677/a6200919-0976-a781-9ed7-ae5bc088e06a.gif)

## 動画

```python:動画(sample-2.gif)の生成
import mkgif

width = 256   # 画像の幅
height = 64  # 画像の高さ

# 256色
color = tuple((n & 0xe0, (n << 3) & 0xe0, (n << 5) & 0xc0) for n in range(256))

# mkgif.File オブジェクトを用意
gif = mkgif.File()

# Logical Screen Descriptor は最初で必須
gif.append_logical_screen_descriptor(width, height, global_color=color)

# 無限ループのアニメーションを設定
gif.append_animaition_loop()  # 回数を引数として渡す。なし(0)は無限ループ。

# 1 ループ分の画像を追加
for m in range(32):
    # 更新画像の情報
    gif.append_graphic_control_extension(5)  # 画像の表示時間(1/100秒単位)

    # 画像データ (8ビットのデータを width * height 個)
    gif.append_image_descriptr((n + m * 8) & 0xff for n in range(width * height))

# Trailer は最後で必須
gif.append_trailer()

# ファイルに出力
open('sample-2.gif', 'wb').write(gif)
```

![sample-2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226677/b8bd9697-40f5-81bc-8259-189a06603818.gif)