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

646 lines
25 KiB
Python

"""
JPEG picture parser.
Information:
- APP14 documents
http://partners.adobe.com/public/developer/en/ps/sdk/5116.DCT_Filter.pdf
http://java.sun.com/j2se/1.5.0/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#color
- APP12:
http://search.cpan.org/~exiftool/Image-ExifTool/lib/Image/ExifTool/TagNames.pod
- JPEG Data Format
http://www.w3.org/Graphics/JPEG/itu-t81.pdf
Author: Victor Stinner, Robert Xiao
"""
from hachoir.parser import Parser
from hachoir.field import (FieldSet, ParserError, FieldError,
UInt8, UInt16, Enum, Field,
Bit, Bits, NullBits, NullBytes, PaddingBits,
String, RawBytes)
from hachoir.parser.image.common import PaletteRGB
from hachoir.core.endian import BIG_ENDIAN
from hachoir.core.text_handler import textHandler, hexadecimal
from hachoir.parser.image.exif import Exif
from hachoir.parser.image.photoshop_metadata import PhotoshopMetadata
from hachoir.parser.archive.zlib import build_tree
from hachoir.core.tools import paddingSize, alignValue
MAX_FILESIZE = 100 * 1024 * 1024
# The four tables (hash/sum for color/grayscale JPEG) comes
# from ImageMagick project
QUALITY_HASH_COLOR = (
1020, 1015, 932, 848, 780, 735, 702, 679, 660, 645,
632, 623, 613, 607, 600, 594, 589, 585, 581, 571,
555, 542, 529, 514, 494, 474, 457, 439, 424, 410,
397, 386, 373, 364, 351, 341, 334, 324, 317, 309,
299, 294, 287, 279, 274, 267, 262, 257, 251, 247,
243, 237, 232, 227, 222, 217, 213, 207, 202, 198,
192, 188, 183, 177, 173, 168, 163, 157, 153, 148,
143, 139, 132, 128, 125, 119, 115, 108, 104, 99,
94, 90, 84, 79, 74, 70, 64, 59, 55, 49,
45, 40, 34, 30, 25, 20, 15, 11, 6, 4,
0)
QUALITY_SUM_COLOR = (
32640, 32635, 32266, 31495, 30665, 29804, 29146, 28599, 28104, 27670,
27225, 26725, 26210, 25716, 25240, 24789, 24373, 23946, 23572, 22846,
21801, 20842, 19949, 19121, 18386, 17651, 16998, 16349, 15800, 15247,
14783, 14321, 13859, 13535, 13081, 12702, 12423, 12056, 11779, 11513,
11135, 10955, 10676, 10392, 10208, 9928, 9747, 9564, 9369, 9193,
9017, 8822, 8639, 8458, 8270, 8084, 7896, 7710, 7527, 7347,
7156, 6977, 6788, 6607, 6422, 6236, 6054, 5867, 5684, 5495,
5305, 5128, 4945, 4751, 4638, 4442, 4248, 4065, 3888, 3698,
3509, 3326, 3139, 2957, 2775, 2586, 2405, 2216, 2037, 1846,
1666, 1483, 1297, 1109, 927, 735, 554, 375, 201, 128,
0)
QUALITY_HASH_GRAY = (
510, 505, 422, 380, 355, 338, 326, 318, 311, 305,
300, 297, 293, 291, 288, 286, 284, 283, 281, 280,
279, 278, 277, 273, 262, 251, 243, 233, 225, 218,
211, 205, 198, 193, 186, 181, 177, 172, 168, 164,
158, 156, 152, 148, 145, 142, 139, 136, 133, 131,
129, 126, 123, 120, 118, 115, 113, 110, 107, 105,
102, 100, 97, 94, 92, 89, 87, 83, 81, 79,
76, 74, 70, 68, 66, 63, 61, 57, 55, 52,
50, 48, 44, 42, 39, 37, 34, 31, 29, 26,
24, 21, 18, 16, 13, 11, 8, 6, 3, 2,
0)
QUALITY_SUM_GRAY = (
16320, 16315, 15946, 15277, 14655, 14073, 13623, 13230, 12859, 12560,
12240, 11861, 11456, 11081, 10714, 10360, 10027, 9679, 9368, 9056,
8680, 8331, 7995, 7668, 7376, 7084, 6823, 6562, 6345, 6125,
5939, 5756, 5571, 5421, 5240, 5086, 4976, 4829, 4719, 4616,
4463, 4393, 4280, 4166, 4092, 3980, 3909, 3835, 3755, 3688,
3621, 3541, 3467, 3396, 3323, 3247, 3170, 3096, 3021, 2952,
2874, 2804, 2727, 2657, 2583, 2509, 2437, 2362, 2290, 2211,
2136, 2068, 1996, 1915, 1858, 1773, 1692, 1620, 1552, 1477,
1398, 1326, 1251, 1179, 1109, 1031, 961, 884, 814, 736,
667, 592, 518, 441, 369, 292, 221, 151, 86, 64,
0)
JPEG_NATURAL_ORDER = (
0, 1, 8, 16, 9, 2, 3, 10,
17, 24, 32, 25, 18, 11, 4, 5,
12, 19, 26, 33, 40, 48, 41, 34,
27, 20, 13, 6, 7, 14, 21, 28,
35, 42, 49, 56, 57, 50, 43, 36,
29, 22, 15, 23, 30, 37, 44, 51,
58, 59, 52, 45, 38, 31, 39, 46,
53, 60, 61, 54, 47, 55, 62, 63)
class JpegChunkApp0(FieldSet):
UNIT_NAME = {
0: "pixels",
1: "dots per inch",
2: "dots per cm",
}
def createFields(self):
yield String(self, "jfif", 5, "JFIF string", charset="ASCII")
if self["jfif"].value != "JFIF\0":
raise ParserError(
"Stream doesn't look like JPEG chunk (wrong JFIF signature)")
yield UInt8(self, "ver_maj", "Major version")
yield UInt8(self, "ver_min", "Minor version")
yield Enum(UInt8(self, "units", "Units"), self.UNIT_NAME)
if self["units"].value == 0:
yield UInt16(self, "aspect_x", "Aspect ratio (X)")
yield UInt16(self, "aspect_y", "Aspect ratio (Y)")
else:
yield UInt16(self, "x_density", "X density")
yield UInt16(self, "y_density", "Y density")
yield UInt8(self, "thumb_w", "Thumbnail width")
yield UInt8(self, "thumb_h", "Thumbnail height")
thumb_size = self["thumb_w"].value * self["thumb_h"].value
if thumb_size != 0:
yield PaletteRGB(self, "thumb_palette", 256)
yield RawBytes(self, "thumb_data", thumb_size, "Thumbnail data")
class Ducky(FieldSet):
BLOCK_TYPE = {
0: "end",
1: "Quality",
2: "Comment",
3: "Copyright",
}
def createFields(self):
yield Enum(UInt16(self, "type"), self.BLOCK_TYPE)
if self["type"].value == 0:
return
yield UInt16(self, "size")
size = self["size"].value
if size:
yield RawBytes(self, "data", size)
class APP12(FieldSet):
"""
The JPEG APP12 "Picture Info" segment was used by some older cameras, and
contains ASCII-based meta information.
"""
def createFields(self):
yield String(self, "ducky", 5, '"Ducky" string', charset="ASCII")
while not self.eof:
yield Ducky(self, "item[]")
class SOFComponent(FieldSet):
def createFields(self):
yield UInt8(self, "component_id")
yield Bits(self, "horiz_sample", 4, "Horizontal sampling factor")
yield Bits(self, "vert_sample", 4, "Vertical sampling factor")
yield UInt8(self, "quant_table", "Quantization table destination selector")
class StartOfFrame(FieldSet):
def createFields(self):
yield UInt8(self, "precision")
yield UInt16(self, "height")
yield UInt16(self, "width")
yield UInt8(self, "nr_components")
for index in range(self["nr_components"].value):
yield SOFComponent(self, "component[]")
class Comment(FieldSet):
def createFields(self):
yield String(self, "comment", self.size // 8, strip="\0")
class AdobeChunk(FieldSet):
COLORSPACE_TRANSFORMATION = {
1: "YCbCr (converted from RGB)",
2: "YCCK (converted from CMYK)",
}
def createFields(self):
if self.stream.readBytes(self.absolute_address, 5) != b"Adobe":
yield RawBytes(self, "raw", self.size // 8, "Raw data")
return
yield String(self, "adobe", 5, "\"Adobe\" string", charset="ASCII")
yield UInt16(self, "version", "DCT encoder version")
yield Enum(Bit(self, "flag00"),
{False: "Chop down or subsampling", True: "Blend"})
yield NullBits(self, "flags0_reserved", 15)
yield NullBytes(self, "flags1", 2)
yield Enum(UInt8(self, "color_transform", "Colorspace transformation code"), self.COLORSPACE_TRANSFORMATION)
class SOSComponent(FieldSet):
def createFields(self):
comp_id = UInt8(self, "component_id")
yield comp_id
if not(1 <= comp_id.value <= self["../nr_components"].value):
raise ParserError("JPEG error: Invalid component-id")
yield Bits(self, "dc_coding_table", 4, "DC entropy coding table destination selector")
yield Bits(self, "ac_coding_table", 4, "AC entropy coding table destination selector")
class StartOfScan(FieldSet):
def createFields(self):
yield UInt8(self, "nr_components")
for index in range(self["nr_components"].value):
yield SOSComponent(self, "component[]")
yield UInt8(self, "spectral_start", "Start of spectral or predictor selection")
yield UInt8(self, "spectral_end", "End of spectral selection")
yield Bits(self, "bit_pos_high", 4, "Successive approximation bit position high")
yield Bits(self, "bit_pos_low", 4, "Successive approximation bit position low or point transform")
class RestartInterval(FieldSet):
def createFields(self):
yield UInt16(self, "interval", "Restart interval")
class QuantizationTable(FieldSet):
def createFields(self):
# Code based on function get_dqt() (jdmarker.c from libjpeg62)
yield Bits(self, "is_16bit", 4)
yield Bits(self, "index", 4)
if self["index"].value >= 4:
raise ParserError("Invalid quantification index (%s)" %
self["index"].value)
if self["is_16bit"].value:
coeff_type = UInt16
else:
coeff_type = UInt8
for index in range(64):
natural = JPEG_NATURAL_ORDER[index]
yield coeff_type(self, "coeff[%u]" % natural)
def createDescription(self):
return "Quantification table #%u" % self["index"].value
class DefineQuantizationTable(FieldSet):
def createFields(self):
while self.current_size < self.size:
yield QuantizationTable(self, "qt[]")
class HuffmanTable(FieldSet):
def createFields(self):
# http://www.w3.org/Graphics/JPEG/itu-t81.pdf, page 40-41
yield Enum(Bits(self, "table_class", 4, "Table class"), {
0: "DC or Lossless Table",
1: "AC Table"})
yield Bits(self, "index", 4, "Huffman table destination identifier")
for i in range(1, 17):
yield UInt8(self, "count[%i]" % i, "Number of codes of length %i" % i)
lengths = []
remap = {}
for i in range(1, 17):
for j in range(self["count[%i]" % i].value):
field = UInt8(self, "value[%i][%i]" % (
i, j), "Value of code #%i of length %i" % (j, i))
yield field
remap[len(lengths)] = field.value
lengths.append(i)
self.tree = {}
for i, j in build_tree(lengths).items():
self.tree[i] = remap[j]
class DefineHuffmanTable(FieldSet):
def createFields(self):
while self.current_size < self.size:
yield HuffmanTable(self, "huffman_table[]")
class HuffmanCode(Field):
"""Huffman code. Uses tree parameter as the Huffman tree."""
def __init__(self, parent, name, tree, description=""):
Field.__init__(self, parent, name, 0, description)
endian = self.parent.endian
stream = self.parent.stream
addr = self.absolute_address
value = 0
met_ff = False
while (self.size, value) not in tree:
if addr % 8 == 0:
last_byte = stream.readBytes(addr - 8, 1)
if last_byte == b'\xFF':
next_byte = stream.readBytes(addr, 1)
if next_byte != b'\x00':
raise FieldError(
"Unexpected byte sequence %r!" % (last_byte + next_byte))
addr += 8 # hack hack hack
met_ff = True
self._description = "[skipped 8 bits after 0xFF] "
bit = stream.readBits(addr, 1, endian)
value <<= 1
value += bit
self._size += 1
addr += 1
self.createValue = lambda: value
self.realvalue = tree[(self.size, value)]
if met_ff:
self._size += 8
class JpegHuffmanImageUnit(FieldSet):
"""8x8 block of sample/coefficient values"""
def __init__(self, parent, name, dc_tree, ac_tree, *args, **kwargs):
FieldSet.__init__(self, parent, name, *args, **kwargs)
self.dc_tree = dc_tree
self.ac_tree = ac_tree
def createFields(self):
field = HuffmanCode(self, "dc_data", self.dc_tree)
field._description = "DC Code %i (Huffman Code %i)" % (
field.realvalue, field.value) + field._description
yield field
if field.realvalue != 0:
extra = Bits(self, "dc_data_extra", field.realvalue)
if extra.value < 2**(field.realvalue - 1):
corrected_value = extra.value + (-1 << field.realvalue) + 1
else:
corrected_value = extra.value
extra._description = "Extra Bits: Corrected DC Value %i" % corrected_value
yield extra
data = []
while len(data) < 63:
field = HuffmanCode(self, "ac_data[]", self.ac_tree)
value_r = field.realvalue >> 4
if value_r:
data += [0] * value_r
value_s = field.realvalue & 0x0F
if value_r == value_s == 0:
field._description = "AC Code Block Terminator (0, 0) (Huffman Code %i)" % field.value + \
field._description
yield field
return
field._description = "AC Code %i, %i (Huffman Code %i)" % (
value_r, value_s, field.value) + field._description
yield field
if value_s != 0:
extra = Bits(self, "ac_data_extra[%s" %
field.name.split('[')[1], value_s)
if extra.value < 2**(value_s - 1):
corrected_value = extra.value + (-1 << value_s) + 1
else:
corrected_value = extra.value
extra._description = "Extra Bits: Corrected AC Value %i" % corrected_value
data.append(corrected_value)
yield extra
else:
data.append(0)
class JpegImageData(FieldSet):
def __init__(self, parent, name, frame, scan, restart_interval, restart_offset=0, *args, **kwargs):
FieldSet.__init__(self, parent, name, *args, **kwargs)
self.frame = frame
self.scan = scan
self.restart_interval = restart_interval
self.restart_offset = restart_offset
# try to figure out where this field ends
start = self.absolute_address
while True:
end = self.stream.searchBytes(b"\xff", start, MAX_FILESIZE * 8)
if end is None:
# this is a bad sign, since it means there is no terminator
# we ignore this; it likely means a truncated image
break
if self.stream.readBytes(end, 2) == b'\xff\x00':
# padding: false alarm
start = end + 16
continue
else:
self._size = end - self.absolute_address
break
def createFields(self):
if self.frame["../type"].value in [0xC0, 0xC1]:
# yay, huffman coding!
if not hasattr(self, "huffman_tables"):
self.huffman_tables = {}
for huffman in self.parent.array("huffman"):
for table in huffman["content"].array("huffman_table"):
for _dummy_ in table:
# exhaust table, so the huffman tree is built
pass
self.huffman_tables[table["table_class"].value, table[
"index"].value] = table.tree
components = [] # sos_comp, samples
max_vert = 0
max_horiz = 0
for component in self.scan.array("component"):
for sof_comp in self.frame.array("component"):
if sof_comp["component_id"].value == component["component_id"].value:
vert = sof_comp["vert_sample"].value
horiz = sof_comp["horiz_sample"].value
components.append((component, vert * horiz))
max_vert = max(max_vert, vert)
max_horiz = max(max_horiz, horiz)
mcu_height = alignValue(
self.frame["height"].value, 8 * max_vert) // (8 * max_vert)
mcu_width = alignValue(
self.frame["width"].value, 8 * max_horiz) // (8 * max_horiz)
if self.restart_interval and self.restart_offset > 0:
mcu_number = self.restart_interval * self.restart_offset
else:
mcu_number = 0
initial_mcu = mcu_number
while True:
if (self.restart_interval and mcu_number != initial_mcu and mcu_number % self.restart_interval == 0) or\
mcu_number == mcu_height * mcu_width:
padding = paddingSize(self.current_size, 8)
if padding:
yield PaddingBits(self, "padding[]", padding) # all 1s
last_byte = self.stream.readBytes(
self.absolute_address + self.current_size - 8, 1)
if last_byte == b'\xFF':
next_byte = self.stream.readBytes(
self.absolute_address + self.current_size, 1)
if next_byte != b'\x00':
raise FieldError(
"Unexpected byte sequence %r!" % (last_byte + next_byte))
yield NullBytes(self, "stuffed_byte[]", 1)
break
for sos_comp, num_units in components:
for interleave_count in range(num_units):
yield JpegHuffmanImageUnit(self, "block[%i]component[%i][]" % (mcu_number, sos_comp["component_id"].value),
self.huffman_tables[
0, sos_comp["dc_coding_table"].value],
self.huffman_tables[1, sos_comp["ac_coding_table"].value])
mcu_number += 1
else:
self.warning(
"Sorry, only supporting Baseline & Extended Sequential JPEG images so far!")
return
class JpegChunk(FieldSet):
TAG_SOI = 0xD8
TAG_EOI = 0xD9
TAG_SOS = 0xDA
TAG_DQT = 0xDB
TAG_DRI = 0xDD
TAG_INFO = {
0xC4: ("huffman[]", "Define Huffman Table (DHT)", DefineHuffmanTable),
0xD8: ("start_image", "Start of image (SOI)", None),
0xD9: ("end_image", "End of image (EOI)", None),
0xD0: ("restart_marker_0[]", "Restart Marker (RST0)", None),
0xD1: ("restart_marker_1[]", "Restart Marker (RST1)", None),
0xD2: ("restart_marker_2[]", "Restart Marker (RST2)", None),
0xD3: ("restart_marker_3[]", "Restart Marker (RST3)", None),
0xD4: ("restart_marker_4[]", "Restart Marker (RST4)", None),
0xD5: ("restart_marker_5[]", "Restart Marker (RST5)", None),
0xD6: ("restart_marker_6[]", "Restart Marker (RST6)", None),
0xD7: ("restart_marker_7[]", "Restart Marker (RST7)", None),
0xDA: ("start_scan[]", "Start Of Scan (SOS)", StartOfScan),
0xDB: ("quantization[]", "Define Quantization Table (DQT)", DefineQuantizationTable),
0xDC: ("nb_line", "Define number of Lines (DNL)", None),
0xDD: ("restart_interval", "Define Restart Interval (DRI)", RestartInterval),
0xE0: ("app0", "APP0", JpegChunkApp0),
0xE1: ("exif", "Exif metadata", Exif),
0xE2: ("icc", "ICC profile", None),
0xEC: ("app12", "APP12", APP12),
0xED: ("photoshop", "Photoshop", PhotoshopMetadata),
0xEE: ("adobe", "Image encoding information for DCT filters (Adobe)", AdobeChunk),
0xFE: ("comment[]", "Comment", Comment),
}
START_OF_FRAME = {
0xC0: "Baseline",
0xC1: "Extended sequential",
0xC2: "Progressive",
0xC3: "Lossless",
0xC5: "Differential sequential",
0xC6: "Differential progressive",
0xC7: "Differential lossless",
0xC9: "Extended sequential, arithmetic coding",
0xCA: "Progressive, arithmetic coding",
0xCB: "Lossless, arithmetic coding",
0xCD: "Differential sequential, arithmetic coding",
0xCE: "Differential progressive, arithmetic coding",
0xCF: "Differential lossless, arithmetic coding",
}
for key, text in START_OF_FRAME.items():
TAG_INFO[key] = ("start_frame", "Start of frame (%s)" %
text.lower(), StartOfFrame)
def __init__(self, parent, name, description=None):
FieldSet.__init__(self, parent, name, description)
tag = self["type"].value
if tag == 0xE1:
# Hack for Adobe extension: XAP metadata (as XML)
bytes = self.stream.readBytes(self.absolute_address + 32, 6)
if bytes == b"Exif\0\0":
self._name = "exif"
self._description = "EXIF"
self._parser = Exif
else:
self._parser = None
elif tag in self.TAG_INFO:
self._name, self._description, self._parser = self.TAG_INFO[tag]
else:
self._parser = None
def createFields(self):
yield textHandler(UInt8(self, "header", "Header"), hexadecimal)
if self["header"].value != 0xFF:
raise ParserError("JPEG: Invalid chunk header!")
yield textHandler(UInt8(self, "type", "Type"), hexadecimal)
tag = self["type"].value
# D0 - D7 inclusive are the restart markers
if tag in [self.TAG_SOI, self.TAG_EOI] + list(range(0xD0, 0xD8)):
return
yield UInt16(self, "size", "Size")
size = (self["size"].value - 2)
if 0 < size:
if self._parser:
yield self._parser(self, "content", "Chunk content", size=size * 8)
else:
yield RawBytes(self, "data", size, "Data")
def createDescription(self):
return "Chunk: %s" % self["type"].display
class JpegFile(Parser):
endian = BIG_ENDIAN
PARSER_TAGS = {
"id": "jpeg",
"category": "image",
"file_ext": ("jpg", "jpeg"),
"mime": ("image/jpeg",),
"magic": (
(b"\xFF\xD8\xFF\xE0", 0), # (Start Of Image, APP0)
(b"\xFF\xD8\xFF\xE1", 0), # (Start Of Image, EXIF)
(b"\xFF\xD8\xFF\xEE", 0), # (Start Of Image, Adobe)
),
"min_size": 22 * 8,
"description": "JPEG picture",
"subfile": "skip",
}
def validate(self):
if self.stream.readBytes(0, 2) != b"\xFF\xD8":
return "Invalid file signature"
try:
for index, field in enumerate(self):
chunk_type = field["type"].value
if chunk_type not in JpegChunk.TAG_INFO:
return "Unknown chunk type: 0x%02X (chunk #%s)" % (chunk_type, index)
if index == 2:
# Only check 3 fields
break
except Exception:
return "Unable to parse at least three chunks"
return True
def createFields(self):
frame = None
scan = None
restart_interval = None
restart_offset = 0
while not self.eof:
chunk = JpegChunk(self, "chunk[]")
yield chunk
if chunk["type"].value in JpegChunk.START_OF_FRAME:
# SOF0 [Baseline], SOF1 [Extended Sequential]
if chunk["type"].value not in [0xC0, 0xC1]:
self.warning(
"Only supporting Baseline & Extended Sequential JPEG images so far!")
frame = chunk["content"]
if chunk["type"].value == JpegChunk.TAG_SOS:
if not frame:
self.warning("Missing or invalid SOF marker before SOS!")
continue
scan = chunk["content"]
# hack: scan only the fields seen so far (in _fields): don't
# use the generator
if "restart_interval" in self._fields:
restart_interval = self[
"restart_interval/content/interval"].value
else:
restart_interval = None
yield JpegImageData(self, "image_data[]", frame, scan, restart_interval)
elif chunk["type"].value in range(0xD0, 0xD8):
restart_offset += 1
yield JpegImageData(self, "image_data[]", frame, scan, restart_interval, restart_offset)
# TODO: is it possible to handle piped input?
if self._size is None:
raise NotImplementedError
has_end = False
size = (self._size - self.current_size) // 8
if size:
if 2 < size \
and self.stream.readBytes(self._size - 16, 2) == b"\xff\xd9":
has_end = True
size -= 2
yield RawBytes(self, "data", size, "JPEG data")
if has_end:
yield JpegChunk(self, "chunk[]")
def createDescription(self):
desc = "JPEG picture"
if "start_frame/content" in self:
header = self["start_frame/content"]
desc += ": %ux%u pixels" % (header["width"].value,
header["height"].value)
return desc
def createContentSize(self):
if "end" in self:
return self["end"].absolute_address + self["end"].size
if "data" in self:
start = self["data"].absolute_address
elif "image_data[0]" in self:
start = self["image_data[0]"].absolute_address
else:
return None
end = self.stream.searchBytes(b"\xff\xd9", start, MAX_FILESIZE * 8)
if end is not None:
return end + 16
return None