"""
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
                # this likely means a truncated image:
                # set the size to the remaining length of the stream
                # to avoid being forced to parse subfields to calculate size
                self._size = self.stream._size - self.absolute_address
                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
