SickGear/lib/hachoir/parser/image/gif.py
2023-02-09 13:41:15 +00:00

362 lines
13 KiB
Python

"""
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