""" GIF picture parser. Author: Victor Stinner, Robert Xiao - GIF format http://local.wasp.uwa.edu.au/~pbourke/dataformats/gif/ - LZW compression http://en.wikipedia.org/wiki/LZW """ from hachoir.parser import Parser from hachoir.field import (FieldSet, ParserError, Enum, UInt8, UInt16, Bit, Bits, NullBytes, String, PascalString8, Character, NullBits, RawBytes, CustomFragment) from hachoir.parser.image.common import PaletteRGB from hachoir.core.endian import LITTLE_ENDIAN from hachoir.core.tools import humanDuration, paddingSize from hachoir.core.text_handler import textHandler, displayHandler, hexadecimal # Maximum image dimension (in pixel) MAX_WIDTH = 6000 MAX_HEIGHT = MAX_WIDTH MAX_FILE_SIZE = 100 * 1024 * 1024 def rle_repr(chain): """Run-length encode a list into an "eval"-able form Example: >>> rle_repr([20, 16, 16, 16, 16, 16, 18, 18, 65]) '[20] + [16]*5 + [18]*2 + [65]' Adapted from http://twistedmatrix.com/trac/browser/trunk/twisted/python/dxprofile.py """ def add_rle(previous, runlen, result): if isinstance(previous, (list, tuple)): previous = rle_repr(previous) if runlen > 1: result.append('[%s]*%i' % (previous, runlen)) else: if result and '*' not in result[-1]: result[-1] = '[%s, %s]' % (result[-1][1:-1], previous) else: result.append('[%s]' % previous) iterable = iter(chain) runlen = 1 result = [] try: previous = next(iterable) except StopIteration: return "[]" for element in iterable: if element == previous: runlen = runlen + 1 continue else: add_rle(previous, runlen, result) previous = element runlen = 1 add_rle(previous, runlen, result) return ' + '.join(result) class GifImageBlock(Parser): endian = LITTLE_ENDIAN def createFields(self): dictionary = {} self.nbits = self.startbits CLEAR_CODE = 2**self.nbits END_CODE = CLEAR_CODE + 1 compress_code = CLEAR_CODE + 2 obuf = [] output = [] while True: if compress_code >= 2**self.nbits: self.nbits += 1 code = Bits(self, "code[]", self.nbits) if code.value == CLEAR_CODE: if compress_code == 2**(self.nbits - 1): # this fixes a bizarre edge case where the reset code could # appear just after the bits incremented. Apparently, the # correct behaviour is to express the reset code with the # old number of bits, not the new... code = Bits(self, "code[]", self.nbits - 1) self.nbits = self.startbits + 1 dictionary = {} compress_code = CLEAR_CODE + 2 obuf = [] code._description = "Reset Code (LZW code %i)" % code.value yield code continue elif code.value == END_CODE: code._description = "End of Information Code (LZW code %i)" % code.value yield code break if code.value < CLEAR_CODE: # literal if obuf: chain = obuf + [code.value] dictionary[compress_code] = chain compress_code += 1 obuf = [code.value] output.append(code.value) code._description = "Literal Code %i" % code.value elif code.value >= CLEAR_CODE + 2: if code.value in dictionary: chain = dictionary[code.value] code._description = "Compression Code %i (found in dictionary as %s)" % ( code.value, rle_repr(chain)) else: chain = obuf + [obuf[0]] code._description = "Compression Code %i (not found in dictionary; guessed to be %s)" % ( code.value, rle_repr(chain)) dictionary[compress_code] = obuf + [chain[0]] compress_code += 1 obuf = chain output += chain code._description += "; Current Decoded Length %i" % len(output) yield code padding = paddingSize(self.current_size, 8) if padding: yield NullBits(self, "padding[]", padding) class Image(FieldSet): def createFields(self): yield UInt16(self, "left", "Left") yield UInt16(self, "top", "Top") yield UInt16(self, "width", "Width") yield UInt16(self, "height", "Height") yield Bits(self, "size_local_map", 3, "log2(size of local map) minus one") yield NullBits(self, "reserved", 2) yield Bit(self, "sort_flag", "Is the local map sorted by decreasing importance?") yield Bit(self, "interlaced", "Interlaced?") yield Bit(self, "has_local_map", "Use local color map?") if self["has_local_map"].value: nb_color = 1 << (1 + self["size_local_map"].value) yield PaletteRGB(self, "local_map", nb_color, "Local color map") yield UInt8(self, "lzw_min_code_size", "LZW Minimum Code Size") group = None while True: size = UInt8(self, "image_block_size[]") if size.value == 0: break yield size block = CustomFragment( self, "image_block[]", size.value * 8, GifImageBlock, "GIF Image Block", group) if group is None: block.group.args["startbits"] = self["lzw_min_code_size"].value group = block.group yield block yield NullBytes(self, "terminator", 1, "Terminator (0)") def createDescription(self): return "Image: %ux%u pixels at (%u,%u)" % ( self["width"].value, self["height"].value, self["left"].value, self["top"].value) DISPOSAL_METHOD = { 0: "No disposal specified", 1: "Do not dispose", 2: "Restore to background color", 3: "Restore to previous", } NETSCAPE_CODE = { 1: "Loop count", } def parseApplicationExtension(parent): yield PascalString8(parent, "app_name", "Application name") while True: size = UInt8(parent, "size[]") if size.value == 0: break yield size if parent["app_name"].value == "NETSCAPE2.0" and size.value == 3: yield Enum(UInt8(parent, "netscape_code"), NETSCAPE_CODE) if parent["netscape_code"].value == 1: yield UInt16(parent, "loop_count") else: yield RawBytes(parent, "raw[]", 2) else: yield RawBytes(parent, "raw[]", size.value) yield NullBytes(parent, "terminator", 1, "Terminator (0)") def parseGraphicControl(parent): yield UInt8(parent, "size", "Block size (4)") yield Bit(parent, "has_transp", "Has transparency") yield Bit(parent, "user_input", "User input") yield Enum(Bits(parent, "disposal_method", 3), DISPOSAL_METHOD) yield NullBits(parent, "reserved[]", 3) if parent["size"].value != 4: raise ParserError("Invalid graphic control size") yield displayHandler(UInt16(parent, "delay", "Delay time in millisecond"), humanDuration) yield UInt8(parent, "transp", "Transparent color index") yield NullBytes(parent, "terminator", 1, "Terminator (0)") def parseComments(parent): while True: field = PascalString8(parent, "comment[]", strip=" \0\r\n\t") yield field if field.length == 0: break def parseTextExtension(parent): yield UInt8(parent, "block_size", "Block Size") yield UInt16(parent, "left", "Text Grid Left") yield UInt16(parent, "top", "Text Grid Top") yield UInt16(parent, "width", "Text Grid Width") yield UInt16(parent, "height", "Text Grid Height") yield UInt8(parent, "cell_width", "Character Cell Width") yield UInt8(parent, "cell_height", "Character Cell Height") yield UInt8(parent, "fg_color", "Foreground Color Index") yield UInt8(parent, "bg_color", "Background Color Index") while True: field = PascalString8(parent, "comment[]", strip=" \0\r\n\t") yield field if field.length == 0: break def defaultExtensionParser(parent): while True: size = UInt8(parent, "size[]", "Size (in bytes)") yield size if 0 < size.value: yield RawBytes(parent, "content[]", size.value) else: break class Extension(FieldSet): ext_code = { 0xf9: ("graphic_ctl[]", parseGraphicControl, "Graphic control"), 0xfe: ("comments[]", parseComments, "Comments"), 0xff: ("app_ext[]", parseApplicationExtension, "Application extension"), 0x01: ("text_ext[]", parseTextExtension, "Plain text extension") } def __init__(self, *args): FieldSet.__init__(self, *args) code = self["code"].value if code in self.ext_code: self._name, self.parser, self._description = self.ext_code[code] else: self.parser = defaultExtensionParser def createFields(self): yield textHandler(UInt8(self, "code", "Extension code"), hexadecimal) yield from self.parser(self) def createDescription(self): return "Extension: function %s" % self["func"].display class ScreenDescriptor(FieldSet): def createFields(self): yield UInt16(self, "width", "Width") yield UInt16(self, "height", "Height") yield Bits(self, "size_global_map", 3, "log2(size of global map) minus one") yield Bit(self, "sort_flag", "Is the global map sorted by decreasing importance?") yield Bits(self, "color_res", 3, "Color resolution minus one") yield Bit(self, "global_map", "Has global map?") yield UInt8(self, "background", "Background color") field = UInt8(self, "pixel_aspect_ratio") if field.value: field._description = "Pixel aspect ratio: %f (stored as %i)" % ( (field.value + 15) / 64., field.value) else: field._description = "Pixel aspect ratio: not specified" yield field def createDescription(self): colors = 1 << (self["size_global_map"].value + 1) return "Screen descriptor: %ux%u pixels %u colors" \ % (self["width"].value, self["height"].value, colors) class GifFile(Parser): endian = LITTLE_ENDIAN separator_name = { "!": "Extension", ",": "Image", ";": "Terminator" } PARSER_TAGS = { "id": "gif", "category": "image", "file_ext": ("gif",), "mime": ("image/gif",), # signature + screen + separator + image "min_size": (6 + 7 + 1 + 9) * 8, "magic": ((b"GIF87a", 0), (b"GIF89a", 0)), "description": "GIF picture" } def validate(self): if self.stream.readBytes(0, 6) not in (b"GIF87a", b"GIF89a"): return "Wrong header" if self["screen/width"].value == 0 or self["screen/height"].value == 0: return "Invalid image size" if MAX_WIDTH < self["screen/width"].value: return "Image width too big (%u)" % self["screen/width"].value if MAX_HEIGHT < self["screen/height"].value: return "Image height too big (%u)" % self["screen/height"].value return True def createFields(self): # Header yield String(self, "magic", 3, "File magic code", charset="ASCII") yield String(self, "version", 3, "GIF version", charset="ASCII") yield ScreenDescriptor(self, "screen") if self["screen/global_map"].value: bpp = (self["screen/size_global_map"].value + 1) yield PaletteRGB(self, "color_map", 1 << bpp, "Color map") self.color_map = self["color_map"] else: self.color_map = None self.images = [] while True: code = Enum(Character(self, "separator[]", "Separator code"), self.separator_name) yield code code = code.value if code == "!": yield Extension(self, "extensions[]") elif code == ",": yield Image(self, "image[]") elif code == ";": # GIF Terminator break else: raise ParserError( "Wrong GIF image separator: 0x%02X" % ord(code)) def createContentSize(self): field = self["image[0]"] start = field.absolute_address + field.size end = start + MAX_FILE_SIZE * 8 pos = self.stream.searchBytes(b"\0;", start, end) if pos: return pos + 16 return None