Skip to content

Commit 8d5ec07

Browse files
authored
Merge pull request #17 from deshipu/master
Add support for GIF files
2 parents 42db804 + 48cd7c8 commit 8d5ec07

File tree

2 files changed

+170
-0
lines changed

2 files changed

+170
-0
lines changed

adafruit_imageload/__init__.py

+3
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

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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+
40+
def load(file, *, bitmap=None, palette=None):
41+
"""Loads a GIF image from the open ``file``.
42+
43+
Returns tuple of bitmap object and palette object.
44+
45+
:param object bitmap: Type to store bitmap data. Must have API similar to `displayio.Bitmap`.
46+
Will be skipped if None
47+
:param object palette: Type to store the palette. Must have API similar to
48+
`displayio.Palette`. Will be skipped if None"""
49+
header = file.read(6)
50+
if header not in {b'GIF87a', b'GIF89a'}:
51+
raise ValueError("Not a GIF file")
52+
width, height, flags, _, _ = struct.unpack('<HHBBB', file.read(7))
53+
if (flags & 0x80) != 0:
54+
palette_size = 1 << ((flags & 0x07) + 1)
55+
palette_obj = palette(palette_size)
56+
for i in range(palette_size):
57+
palette_obj[i] = file.read(3)
58+
else:
59+
palette_obj = None
60+
color_bits = ((flags & 0x70) >> 4) + 1
61+
bitmap_obj = bitmap(width, height, (1 << color_bits) - 1)
62+
while True:
63+
block_type = file.read(1)[0]
64+
if block_type == 0x2c: # frame
65+
_read_frame(file, bitmap_obj)
66+
elif block_type == 0x21: # extension
67+
_ = file.read(1)[0]
68+
# 0x01 = label, 0xfe = comment
69+
_ = bytes(_read_blockstream(file))
70+
elif block_type == 0x3b: # terminator
71+
break
72+
else:
73+
raise ValueError("Bad block type")
74+
return bitmap_obj, palette_obj
75+
76+
77+
def _read_frame(file, bitmap):
78+
"""Read a signle frame and apply it to the bitmap."""
79+
ddx, ddy, width, _, flags = struct.unpack('<HHHHB', file.read(9))
80+
if (flags & 0x40) != 0:
81+
raise NotImplementedError("Interlacing not supported")
82+
if (flags & 0x80) != 0:
83+
palette_size = 1 << ((flags & 0x07) + 1)
84+
for _ in range(palette_size):
85+
_ = file.read(3)
86+
min_code_size = file.read(1)[0]
87+
x = 0
88+
y = 0
89+
for decoded in lzw_decode(_read_blockstream(file), min_code_size):
90+
for byte in decoded:
91+
bitmap[ddx + x, ddy + y] = byte
92+
x += 1
93+
if x >= width:
94+
x = 0
95+
y += 1
96+
97+
98+
def _read_blockstream(file):
99+
"""Read a block from a file."""
100+
while True:
101+
size = file.read(1)[0]
102+
if size == 0:
103+
break
104+
for _ in range(size):
105+
yield file.read(1)[0]
106+
107+
108+
class EndOfData(Exception):
109+
"""Signified end of compressed data."""
110+
111+
112+
class LZWDict:
113+
"""A dictionary of LZW codes."""
114+
def __init__(self, code_size):
115+
self.code_size = code_size
116+
self.clear_code = 1 << code_size
117+
self.end_code = self.clear_code + 1
118+
self.codes = []
119+
self.last = None
120+
self.clear()
121+
122+
def clear(self):
123+
"""Reset the dictionary to default codes."""
124+
self.last = b''
125+
self.code_len = self.code_size + 1
126+
self.codes[:] = []
127+
128+
def decode(self, code):
129+
"""Decode a code."""
130+
if code == self.clear_code:
131+
self.clear()
132+
return b''
133+
elif code == self.end_code:
134+
raise EndOfData()
135+
elif code < self.clear_code:
136+
value = bytes([code])
137+
elif code <= len(self.codes) + self.end_code:
138+
value = self.codes[code - self.end_code - 1]
139+
else:
140+
value = self.last + self.last[0:1]
141+
if self.last:
142+
self.codes.append(self.last + value[0:1])
143+
if (len(self.codes) + self.end_code + 1 >= 1 << self.code_len and
144+
self.code_len < 12):
145+
self.code_len += 1
146+
self.last = value
147+
return value
148+
149+
150+
def lzw_decode(data, code_size):
151+
"""Decode LZW-compressed data."""
152+
dictionary = LZWDict(code_size)
153+
bit = 0
154+
byte = next(data) # pylint: disable=stop-iteration-return
155+
try:
156+
while True:
157+
code = 0
158+
for i in range(dictionary.code_len):
159+
code |= ((byte >> bit) & 0x01) << i
160+
bit += 1
161+
if bit >= 8:
162+
bit = 0
163+
byte = next(data) # pylint: disable=stop-iteration-return
164+
yield dictionary.decode(code)
165+
except EndOfData:
166+
while True:
167+
next(data) # pylint: disable=stop-iteration-return

0 commit comments

Comments
 (0)