GIF データ生成用クラス
bytearray の派生として GIF 形式のデータを生成します。
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)
使用例
以下、静止画と動画の生成例です。
静止画
静止画(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-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)