From 23d18bf12df89dc84a8a49eeb271f425cc36c1f0 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Tue, 2 Jul 2019 16:42:21 +0200 Subject: [PATCH 1/2] Add support for GIF files --- adafruit_imageload/__init__.py | 3 + adafruit_imageload/gif.py | 160 +++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 adafruit_imageload/gif.py 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..b59deab --- /dev/null +++ b/adafruit_imageload/gif.py @@ -0,0 +1,160 @@ +# 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") + w, h, flags, background, aspect = struct.unpack('> 4) + 1 + bitmap_obj = bitmap(w, h, (1 << color_bits) - 1) + while True: + block_type = file.read(1)[0] + if block_type == 0x2c: # frame + dx, dy, frame_w, frame_h, flags = struct.unpack('= frame_w: + x = 0 + y += 1 + elif block_type == 0x21: # extension + extension_type = file.read(1)[0] + # 0x01 = label, 0xfe = comment + data = bytes(_read_blockstream(file)) + elif block_type == 0x3b: # terminator + break + else: + raise ValueError("Bad block type") + return bitmap_obj, palette_obj + + +def _read_blockstream(f): + while True: + size = f.read(1)[0] + if size == 0: + break + for i in range(size): + yield f.read(1)[0] + + +class EndOfData(Exception): + pass + + +class LZWDict: + 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.clear() + + def clear(self): + self.last = b'' + self.code_len = self.code_size + 1 + self.codes[:] = [] + + def decode(self, 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): + dictionary = LZWDict(code_size) + bit = 0 + byte = next(data) + 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) + yield dictionary.decode(code) + except EndOfData: + while True: + next(data) From 48cd7c865513d509a310073753a1f919c681b84d Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Tue, 2 Jul 2019 21:34:10 +0200 Subject: [PATCH 2/2] Make pylint happier --- adafruit_imageload/gif.py | 79 +++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/adafruit_imageload/gif.py b/adafruit_imageload/gif.py index b59deab..4657ee1 100644 --- a/adafruit_imageload/gif.py +++ b/adafruit_imageload/gif.py @@ -36,6 +36,7 @@ __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``. @@ -48,7 +49,7 @@ def load(file, *, bitmap=None, palette=None): header = file.read(6) if header not in {b'GIF87a', b'GIF89a'}: raise ValueError("Not a GIF file") - w, h, flags, background, aspect = struct.unpack('> 4) + 1 - bitmap_obj = bitmap(w, h, (1 << color_bits) - 1) + bitmap_obj = bitmap(width, height, (1 << color_bits) - 1) while True: block_type = file.read(1)[0] if block_type == 0x2c: # frame - dx, dy, frame_w, frame_h, flags = struct.unpack('= frame_w: - x = 0 - y += 1 + _read_frame(file, bitmap_obj) elif block_type == 0x21: # extension - extension_type = file.read(1)[0] + _ = file.read(1)[0] # 0x01 = label, 0xfe = comment - data = bytes(_read_blockstream(file)) + _ = bytes(_read_blockstream(file)) elif block_type == 0x3b: # terminator break else: @@ -94,33 +74,59 @@ def load(file, *, bitmap=None, palette=None): return bitmap_obj, palette_obj -def _read_blockstream(f): +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 = f.read(1)[0] + size = file.read(1)[0] if size == 0: break - for i in range(size): - yield f.read(1)[0] + for _ in range(size): + yield file.read(1)[0] class EndOfData(Exception): - pass + """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'' @@ -135,16 +141,17 @@ def decode(self, code): 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.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) + byte = next(data) # pylint: disable=stop-iteration-return try: while True: code = 0 @@ -153,8 +160,8 @@ def lzw_decode(data, code_size): bit += 1 if bit >= 8: bit = 0 - byte = next(data) + byte = next(data) # pylint: disable=stop-iteration-return yield dictionary.decode(code) except EndOfData: while True: - next(data) + next(data) # pylint: disable=stop-iteration-return