GIF データを確認する

# デバッグ用のツール

GIF ファイルの[データ形式](https://qiita.com/ikiuo/items/69de0087e358a2ec6949)は難しいものではないので、ライブラリ等を用いなくても作る事が比較的容易です。すると、作った GIF ファイルの状態を確認したくなります。そのためのツールです。

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

INDENT = ' ' * 4


def read8(fp):
    return fp.read(1)[0]


def read16(fp):
    l, h = fp.read(2)
    return (h << 8) | l


def print_header(fp):
    SIGNATURE = b'GIF'
    VERSION = (b'87a', b'89a')

    signature = fp.read(3)
    if signature != SIGNATURE:
        raise Exception('%s: signature(%s) != %s' % (path, SIGNATURE, signature))
    print('Signature:', signature)
    version = fp.read(3)
    if version not in VERSION:
        raise Exception('%s: unknown version: %s' % (path, version))
    print('Version  :', version)


def print_logical_screen_descriptor(fp):
    indent = INDENT
    print('Logical Screen Descriptor:')
    print(indent + 'Logical Screen Width :', read16(fp))
    print(indent + 'Logical Screen Height:', read16(fp))
    packed_fields = read8(fp)
    size_of_global_color_table = packed_fields & 7
    color_resolution = (packed_fields >> 4) & 7
    global_color_table_flag = bool((packed_fields >> 7) & 1)
    print(indent + 'Packed Fields: 0x%02x' % packed_fields)
    print(indent * 2 + 'Size of Global Color Table: %d (%d)' %
          (size_of_global_color_table, 2 << size_of_global_color_table))
    print(indent * 2 + 'Sort Flag                 : %s' % bool((packed_fields >> 3) & 1))
    print(indent * 2 + 'Color Resolution          : %d (%d)' % (color_resolution, (2 << color_resolution)))
    print(indent * 2 + 'Global Color Table Flag   : %s' % global_color_table_flag)
    print(indent + 'Background Color Index: %d' % read8(fp))
    print(indent + 'Pixel Aspect Ratio    : %d' % read8(fp))
    if global_color_table_flag:
        print_color('Global Color Table', size_of_global_color_table, fp, indent)


def print_descriptor(fp):
    desc = read8(fp)
    if desc == 0x2c:
        return print_image_descriptor(fp)
    if desc == 0x21:
        return print_extension(fp)
    if desc == 0x3b:
        return print_trailer(fp)
    raise Exception('unknown descriptor: 0x%02x' % desc)


def print_image_descriptor(fp):
    indent = INDENT
    print('Image Descriptor: descriptor=0x2c')
    print(indent + 'Image Left Position:', read16(fp))
    print(indent + 'Image Top  Position:', read16(fp))
    print(indent + 'Image Width        :', read16(fp))
    print(indent + 'Image Height       :', read16(fp))
    packed_fields = read8(fp)
    size_of_local_color_table = packed_fields & 7
    local_color_table_flag = bool((packed_fields >> 7) & 1)
    print(indent + 'Packed Fields      : 0x%02x' % packed_fields)
    print(indent * 2 + 'Size of Local Color Table: %d (%d)' %
          (size_of_local_color_table, 2 << size_of_local_color_table))
    print(indent * 2 + 'Reserved                 : %s' % ((packed_fields >> 3) & 3))
    print(indent * 2 + 'Sort Flag                : %s' % bool((packed_fields >> 5) & 1))
    print(indent * 2 + 'Interlace Flag           : %s' % bool((packed_fields >> 6) & 1))
    print(indent * 2 + 'Local Color Table Flag   : %s' % local_color_table_flag)
    if local_color_table_flag:
        print_color('Local Color Table', size_of_local_color_table, fp, indent)
    print('Table Based Image Data:')
    print(indent + 'LZW minimum code size: %d' % read8(fp))
    print_data_sub_blocks(fp, indent)
    return True


def print_extension(fp):
    label = read8(fp)
    if label == 0xf9:
        return print_graphic_control_extension(fp)
    if label == 0xfe:
        return print_commnet_extension(fp)
    if label == 0x01:
        return print_plain_text_extension(fp)
    if label == 0xff:
        return print_application_extension(fp)
    print('Unknown Extension: descriptor=0x21, label=0x%02x' % label)
    print_data_sub_blocks(fp, INDENT)
    return True


def print_graphic_control_extension(fp):
    indent = INDENT
    indent2 = indent * 2
    indent3 = indent * 3
    print('Graphic Control Extension: descriptor=0x21, label=0xf9')
    if read8(fp) != 4:
        raise Exception('Invalid Graphic Control Extension')
    print(indent + 'Data Sub-block: size=4')
    packed_fields = read8(fp)
    print(indent2 + 'Packed Fields           : 0x%02x' % packed_fields)
    print(indent3 + 'Reserved              : %d' % (packed_fields & 7))
    print(indent3 + 'Disposal Method       : %d' % ((packed_fields >> 3) & 7))
    print(indent3 + 'User Input Flag       : %s' % bool((packed_fields >> 6) & 1))
    print(indent3 + 'Transparent Color flag: %s' % bool((packed_fields >> 7) & 1))
    print(indent2 + 'Delay Time              : %d' % read16(fp))
    print(indent2 + 'Transparent Color Indent: %d' % read8(fp))
    if read8(fp) != 0:
        raise Exception('Invalid Graphic Control Extension')
    return True


def print_commnet_extension(fp):
    indent = INDENT
    print('Comment Extension: descriptor=0x21, label=0xfe')
    print_data_sub_blocks(fp, indent)


def print_plain_text_extension(fp):
    indent = INDENT
    indent2 = indent * 2
    print('Plain Text Extension: descriptor=0x21, label=0x01')
    if read8(fp) != 12:
        raise Exception('Invalid Plain Text Extension')
    print(indent + 'Data Sub-block: size=12')
    print(indent2 + 'Text Grid Left Position    : %d' % read16(fp))
    print(indent2 + 'Text Grid Top  Position    : %d' % read16(fp))
    print(indent2 + 'Text Grid Width            : %d' % read16(fp))
    print(indent2 + 'Text Grid Height           : %d' % read16(fp))
    print(indent2 + 'Character Cell Width       : %d' % read8(fp))
    print(indent2 + 'Character Cell Height      : %d' % read8(fp))
    print(indent2 + 'Text Foreground Color Index: %d' % read8(fp))
    print(indent2 + 'Text Background Color Index: %d' % read8(fp))
    print_data_sub_blocks(fp, indent)
    return True


def print_application_extension(fp):
    indent = INDENT
    indent2 = indent * 2
    print('Application Extension: descriptor=0x21, label=0xff')
    if read8(fp) != 11:
        raise Exception('Invalid Application Extension')
    print(indent + 'Data Sub-block: size=11')
    identifier = fp.read(8)
    print(indent2 + 'Application Identifier         :', identifier)
    authentication_code = fp.read(3)
    print(indent2 + 'Application Authentication Code:', authentication_code)
    print_data_sub_blocks(fp, indent)
    return True


def print_trailer(fp):
    print('Trailer: descriptor=0x3b')
    return False


def print_color(prefix, ctsize, fp, indent):
    print(indent + prefix + ':')
    count = 2 << ctsize
    for y in range(0, count, 4):
        msg = []
        for x in range(4):
            n = y + x
            if n >= count:
                break
            msg.append('(%3d,%3d,%3d)' % tuple(fp.read(3)))
        print(indent * 2 + ('%02x: ' % y) + '  '.join(msg))


def print_data_sub_blocks(fp, indent):
    while True:
        count = read8(fp)
        print(indent + 'Data Sub-block: size=%d' % count)
        if count == 0:
            break
        for y in range(0, count, 16):
            msg = []
            for x in range(16):
                n = y + x
                if n >= count:
                    break
                msg.append('%02x' % read8(fp))
            print(indent * 2 + ('%02x: ' % y) + ' '.join(msg))


if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('GIF')

    args = parser.parse_args()
    path = args.GIF

    print('GIF:', path)
    fp = open(path, 'rb')

    print_header(fp)
    print_logical_screen_descriptor(fp)
    while print_descriptor(fp):
        pass
```

# テスト

こんな感じで出力します。

```console:
$ python3 gifdump.py sample.gif
GIF: sample.gif
Signature: b'GIF'
Version  : b'89a'
Logical Screen Descriptor:
    Logical Screen Width : 640
    Logical Screen Height: 480
    Packed Fields: 0xf7
        Size of Global Color Table: 7 (256)
        Sort Flag                 : False
        Color Resolution          : 7 (256)
        Global Color Table Flag   : True
    Background Color Index: 0
    Pixel Aspect Ratio    : 49
    Global Color Table:
        00: (  0,  0,  0)  (  1,  1,  1)  (  2,  2,  2)  (  3,  3,  3)
        04: (  4,  4,  4)  (  5,  5,  5)  (  6,  6,  6)  (  7,  7,  7)
        08: (  8,  8,  8)  (  9,  9,  9)  ( 10, 10, 10)  ( 11, 11, 11)
        0c: ( 12, 12, 12)  ( 13, 13, 13)  ( 14, 14, 14)  ( 15, 15, 15)
```

〜〜〜（略）〜〜〜

```console:
        f0: (240,240,240)  (241,241,241)  (242,242,242)  (243,243,243)
        f4: (244,244,244)  (245,245,245)  (246,246,246)  (247,247,247)
        f8: (248,248,248)  (249,249,249)  (250,250,250)  (251,251,251)
        fc: (252,252,252)  (253,253,253)  (254,254,254)  (255,255,255)
Image Descriptor: descriptor=0x2c
    Image Left Position: 0
    Image Top  Position: 0
    Image Width        : 640
    Image Height       : 480
    Packed Fields      : 0x00
        Size of Local Color Table: 0 (2)
        Reserved                 : 0
        Sort Flag                : False
        Interlace Flag           : False
        Local Color Table Flag   : False
Table Based Image Data:
    LZW minimum code size: 8
    Data Sub-block: size=255
        00: 00 01 08 1c 48 b0 a0 c1 83 08 13 2a 5c c8 b0 a1
        10: c3 87 10 23 4a 9c 48 b1 a2 c5 8b 18 33 6a dc c8
        20: b1 a3 c7 8f 20 43 8a 1c b9 30 80 c9 93 28 53 aa
        30: 5c c9 b2 a5 cb 97 30 63 ca 9c 49 b3 a6 cd 9b 38
```

〜〜〜（略）〜〜〜

```console:
        c0: 2b d8 c1 12 b6 b0 86 3d 2c 62 13 ab d8 c5 32 b6
        d0: b1 8e 7d 2c 64 23 2b d9 c9 de 75 25 96 bd 2c 66
        e0: 33 ab d9 cd 72 b6 b3 9e fd 2c 68 43 2b da d1 92
        f0: b6 b4 a6 3d 2d 6a 53 ab da d5 b2 b6 b5 ae 7d
    Data Sub-block: size=14
        00: 2d 6c 63 2b db d9 d2 b6 b6 b6 fd 6c 40 00
    Data Sub-block: size=0
Trailer: descriptor=0x3b
```