diff --git a/adafruit_imageload/__init__.py b/adafruit_imageload/__init__.py index 3a3fcdd..cc744bf 100644 --- a/adafruit_imageload/__init__.py +++ b/adafruit_imageload/__init__.py @@ -50,4 +50,7 @@ def load(filename, *, bitmap=None, palette=None): if header.startswith(b"P"): from . import pnm return pnm.load(file, header, bitmap=bitmap, palette=palette) + if header.startswith(b"GIF"): + from . import gif + return gif.load(file, bitmap=bitmap, palette=palette) raise RuntimeError("Unsupported image format") diff --git a/adafruit_imageload/gif.py b/adafruit_imageload/gif.py new file mode 100644 index 0000000..4657ee1 --- /dev/null +++ b/adafruit_imageload/gif.py @@ -0,0 +1,167 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Radomir Dopieralski +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_imageload.gif` +==================================================== + +Load pixel values (indices or colors) into a bitmap and colors into a palette +from a GIF file. + +* Author(s): Radomir Dopieralski + +""" + +import struct + + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ImageLoad.git" + + +def load(file, *, bitmap=None, palette=None): + """Loads a GIF image from the open ``file``. + + Returns tuple of bitmap object and palette object. + + :param object bitmap: Type to store bitmap data. Must have API similar to `displayio.Bitmap`. + Will be skipped if None + :param object palette: Type to store the palette. Must have API similar to + `displayio.Palette`. Will be skipped if None""" + header = file.read(6) + if header not in {b'GIF87a', b'GIF89a'}: + raise ValueError("Not a GIF file") + width, height, flags, _, _ = struct.unpack('> 4) + 1 + bitmap_obj = bitmap(width, height, (1 << color_bits) - 1) + while True: + block_type = file.read(1)[0] + if block_type == 0x2c: # frame + _read_frame(file, bitmap_obj) + elif block_type == 0x21: # extension + _ = file.read(1)[0] + # 0x01 = label, 0xfe = comment + _ = bytes(_read_blockstream(file)) + elif block_type == 0x3b: # terminator + break + else: + raise ValueError("Bad block type") + return bitmap_obj, palette_obj + + +def _read_frame(file, bitmap): + """Read a signle frame and apply it to the bitmap.""" + ddx, ddy, width, _, flags = struct.unpack('= width: + x = 0 + y += 1 + + +def _read_blockstream(file): + """Read a block from a file.""" + while True: + size = file.read(1)[0] + if size == 0: + break + for _ in range(size): + yield file.read(1)[0] + + +class EndOfData(Exception): + """Signified end of compressed data.""" + + +class LZWDict: + """A dictionary of LZW codes.""" + def __init__(self, code_size): + self.code_size = code_size + self.clear_code = 1 << code_size + self.end_code = self.clear_code + 1 + self.codes = [] + self.last = None + self.clear() + + def clear(self): + """Reset the dictionary to default codes.""" + self.last = b'' + self.code_len = self.code_size + 1 + self.codes[:] = [] + + def decode(self, code): + """Decode a code.""" + if code == self.clear_code: + self.clear() + return b'' + elif code == self.end_code: + raise EndOfData() + elif code < self.clear_code: + value = bytes([code]) + elif code <= len(self.codes) + self.end_code: + value = self.codes[code - self.end_code - 1] + else: + value = self.last + self.last[0:1] + if self.last: + self.codes.append(self.last + value[0:1]) + if (len(self.codes) + self.end_code + 1 >= 1 << self.code_len and + self.code_len < 12): + self.code_len += 1 + self.last = value + return value + + +def lzw_decode(data, code_size): + """Decode LZW-compressed data.""" + dictionary = LZWDict(code_size) + bit = 0 + byte = next(data) # pylint: disable=stop-iteration-return + try: + while True: + code = 0 + for i in range(dictionary.code_len): + code |= ((byte >> bit) & 0x01) << i + bit += 1 + if bit >= 8: + bit = 0 + byte = next(data) # pylint: disable=stop-iteration-return + yield dictionary.decode(code) + except EndOfData: + while True: + next(data) # pylint: disable=stop-iteration-return