Skip to content

Commit 23d18bf

Browse files
committed
Add support for GIF files
1 parent 42db804 commit 23d18bf

File tree

2 files changed

+163
-0
lines changed

2 files changed

+163
-0
lines changed

adafruit_imageload/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,7 @@ def load(filename, *, bitmap=None, palette=None):
5050
if header.startswith(b"P"):
5151
from . import pnm
5252
return pnm.load(file, header, bitmap=bitmap, palette=palette)
53+
if header.startswith(b"GIF"):
54+
from . import gif
55+
return gif.load(file, bitmap=bitmap, palette=palette)
5356
raise RuntimeError("Unsupported image format")

adafruit_imageload/gif.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# The MIT License (MIT)
2+
#
3+
# Copyright (c) 2019 Radomir Dopieralski
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in
13+
# all copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
# THE SOFTWARE.
22+
"""
23+
`adafruit_imageload.gif`
24+
====================================================
25+
26+
Load pixel values (indices or colors) into a bitmap and colors into a palette
27+
from a GIF file.
28+
29+
* Author(s): Radomir Dopieralski
30+
31+
"""
32+
33+
import struct
34+
35+
36+
__version__ = "0.0.0-auto.0"
37+
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ImageLoad.git"
38+
39+
def load(file, *, bitmap=None, palette=None):
40+
"""Loads a GIF image from the open ``file``.
41+
42+
Returns tuple of bitmap object and palette object.
43+
44+
:param object bitmap: Type to store bitmap data. Must have API similar to `displayio.Bitmap`.
45+
Will be skipped if None
46+
:param object palette: Type to store the palette. Must have API similar to
47+
`displayio.Palette`. Will be skipped if None"""
48+
header = file.read(6)
49+
if header not in {b'GIF87a', b'GIF89a'}:
50+
raise ValueError("Not a GIF file")
51+
w, h, flags, background, aspect = struct.unpack('<HHBBB', file.read(7))
52+
if (flags & 0x80) != 0:
53+
palette_size = 1 << ((flags & 0x07) + 1)
54+
palette_obj = palette(palette_size)
55+
for i in range(palette_size):
56+
palette_obj[i] = file.read(3)
57+
else:
58+
palette_obj = None
59+
color_bits = ((flags & 0x70) >> 4) + 1
60+
bitmap_obj = bitmap(w, h, (1 << color_bits) - 1)
61+
while True:
62+
block_type = file.read(1)[0]
63+
if block_type == 0x2c: # frame
64+
dx, dy, frame_w, frame_h, flags = struct.unpack('<HHHHB',
65+
file.read(9))
66+
if (flags & 0x40) != 0:
67+
raise NotImplementedError("Interlacing not supported")
68+
if (flags & 0x80) != 0:
69+
palette_size = 1 << ((flags & 0x07) + 1)
70+
frame_palette = palette(palette_size)
71+
for i in range(palette_size):
72+
frame_palette[i] = file.read(3)
73+
if palette_obj is None:
74+
# if there is no global palette, use local one
75+
palette_obj = frame_palette
76+
min_code_size = file.read(1)[0]
77+
x = 0
78+
y = 0
79+
for decoded in lzw_decode(_read_blockstream(file), min_code_size):
80+
for byte in decoded:
81+
bitmap_obj[dx+x, dy+y] = byte
82+
x += 1
83+
if x >= frame_w:
84+
x = 0
85+
y += 1
86+
elif block_type == 0x21: # extension
87+
extension_type = file.read(1)[0]
88+
# 0x01 = label, 0xfe = comment
89+
data = bytes(_read_blockstream(file))
90+
elif block_type == 0x3b: # terminator
91+
break
92+
else:
93+
raise ValueError("Bad block type")
94+
return bitmap_obj, palette_obj
95+
96+
97+
def _read_blockstream(f):
98+
while True:
99+
size = f.read(1)[0]
100+
if size == 0:
101+
break
102+
for i in range(size):
103+
yield f.read(1)[0]
104+
105+
106+
class EndOfData(Exception):
107+
pass
108+
109+
110+
class LZWDict:
111+
def __init__(self, code_size):
112+
self.code_size = code_size
113+
self.clear_code = 1 << code_size
114+
self.end_code = self.clear_code + 1
115+
self.codes = []
116+
self.clear()
117+
118+
def clear(self):
119+
self.last = b''
120+
self.code_len = self.code_size + 1
121+
self.codes[:] = []
122+
123+
def decode(self, code):
124+
if code == self.clear_code:
125+
self.clear()
126+
return b''
127+
elif code == self.end_code:
128+
raise EndOfData()
129+
elif code < self.clear_code:
130+
value = bytes([code])
131+
elif code <= len(self.codes) + self.end_code:
132+
value = self.codes[code - self.end_code - 1]
133+
else:
134+
value = self.last + self.last[0:1]
135+
if self.last:
136+
self.codes.append(self.last + value[0:1])
137+
if (len(self.codes) + self.end_code + 1 >= 1 << self.code_len and
138+
self.code_len < 12):
139+
self.code_len += 1
140+
self.last = value
141+
return value
142+
143+
144+
def lzw_decode(data, code_size):
145+
dictionary = LZWDict(code_size)
146+
bit = 0
147+
byte = next(data)
148+
try:
149+
while True:
150+
code = 0
151+
for i in range(dictionary.code_len):
152+
code |= ((byte >> bit) & 0x01) << i
153+
bit += 1
154+
if bit >= 8:
155+
bit = 0
156+
byte = next(data)
157+
yield dictionary.decode(code)
158+
except EndOfData:
159+
while True:
160+
next(data)

0 commit comments

Comments
 (0)