#
# The Python Imaging Library.
# $Id: //modules/pil/PIL/JpegImagePlugin.py#6 $
#
# JPEG (JFIF) file handling
#
# See "Digital Compression and Coding of Continous-Tone Still Images,
# Part 1, Requirements and Guidelines" (CCITT T.81 / ISO 10918-1)
#
# History:
# 1995-09-09 fl   Created
# 1995-09-13 fl   Added full parser
# 1996-03-25 fl   Added hack to use the IJG command line utilities
# 1996-05-05 fl   Workaround Photoshop 2.5 CMYK polarity bug
# 1996-05-28 fl   Added draft support, JFIF version (0.1)
# 1996-12-30 fl   Added encoder options, added progression property (0.2)
# 1997-08-27 fl   Save mode 1 images as BW (0.3)
# 1998-07-12 fl   Added YCbCr to draft and save methods (0.4)
# 1998-10-19 fl   Don't hang on files using 16-bit DQT's (0.4.1)
# 2001-04-16 fl   Extract DPI settings from JFIF files (0.4.2)
# 2002-07-01 fl   Skip pad bytes before markers; identify Exif files (0.4.3)
# 2003-04-25 fl   Added experimental EXIF decoder (0.5)
#
# Copyright (c) 1997-2003 by Secret Labs AB.
# Copyright (c) 1995-1996 by Fredrik Lundh.
#
# See the README file for information on usage and redistribution.
#

__version__ = "0.5"

import array, string
import Image, ImageFile

def i16(c,o=0):
    return ord(c[o+1]) + (ord(c[o])<<8)

def i32(c,o=0):
    return ord(c[o+3]) + (ord(c[o+2])<<8) + (ord(c[o+1])<<16) + (ord(c[o])<<24)

#
# Parser

def Skip(self, marker):
    self.fp.read(i16(self.fp.read(2))-2)

def APP(self, marker):
    #
    # Application marker.  Store these in the APP dictionary.
    # Also look for well-known application markers.

    s = self.fp.read(i16(self.fp.read(2))-2)

    app = "APP%d" % (marker&15)

    self.app[app] = s # compatibility
    self.applist.append((app, s))

    if marker == 0xFFE0 and s[:4] == "JFIF":
        # extract JFIF information
        self.info["jfif"] = version = i16(s, 5) # version
        self.info["jfif_version"] = divmod(version, 256)
        # extract JFIF properties
        try:
            jfif_unit = ord(s[7])
            jfif_density = i16(s, 8), i16(s, 10)
        except:
            pass
        else:
            if jfif_unit == 1:
                self.info["dpi"] = jfif_density
            self.info["jfif_unit"] = jfif_unit
            self.info["jfif_density"] = jfif_density
    elif marker == 0xFFE1 and s[:5] == "Exif\0":
        # extract Exif information (incomplete)
        self.info["exif"] = s # FIXME: value will change
    elif marker == 0xFFE2 and s[:5] == "FPXR\0":
        # extract FlashPix information (incomplete)
        self.info["flashpix"] = s # FIXME: value will change
    elif marker == 0xFFEE and s[:5] == "Adobe":
        self.info["adobe"] = i16(s, 5)
        # extract Adobe custom properties
        try:
            adobe_transform = ord(s[1])
        except:
            pass
        else:
            self.info["adobe_transform"] = adobe_transform

def SOF(self, marker):
    #
    # Start of frame marker.  Defines the size and mode of the
    # image.  JPEG is colour blind, so we use some simple
    # heuristics to map the number of layers to an appropriate
    # mode.  Note that this could be made a bit brighter, by
    # looking for JFIF and Adobe APP markers.

    s = self.fp.read(i16(self.fp.read(2))-2)
    self.size = i16(s[3:]), i16(s[1:])

    self.bits = ord(s[0])
    if self.bits != 8:
        raise SyntaxError("cannot handle %d-bit layers" % self.bits)

    self.layers = ord(s[5])
    if self.layers == 1:
        self.mode = "L"
    elif self.layers == 3:
        self.mode = "RGB"
    elif self.layers == 4:
        self.mode = "CMYK"
    else:
        raise SyntaxError("cannot handle %d-layer images" % self.layers)

    if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]:
        self.info["progression"] = 1

    for i in range(6, len(s), 3):
        t = s[i:i+3]
        # 4-tuples: id, vsamp, hsamp, qtable
        self.layer.append((t[0], ord(t[1])/16, ord(t[1])&15, ord(t[2])))

def DQT(self, marker):
    #
    # Define quantization table.  Support baseline 8-bit tables
    # only.  Note that there might be more than one table in
    # each marker.

    # FIXME: The quantization tables can be used to estimate the
    # compression quality.

    s = self.fp.read(i16(self.fp.read(2))-2)
    while len(s):
        if len(s) < 65:
            raise SyntaxError("bad quantization table marker")
        v = ord(s[0])
        if v/16 == 0:
            self.quantization[v&15] = array.array("b", s[1:65])
            s = s[65:]
        else:
            return # FIXME: add code to read 16-bit tables!
            # raise SyntaxError, "bad quantization table element size"


#
# JPEG marker table

MARKER = {
    0xFFC0: ("SOF0", "Baseline DCT", SOF),
    0xFFC1: ("SOF1", "Extended Sequential DCT", SOF),
    0xFFC2: ("SOF2", "Progressive DCT", SOF),
    0xFFC3: ("SOF3", "Spatial lossless", SOF),
    0xFFC4: ("DHT", "Define Huffman table", Skip),
    0xFFC5: ("SOF5", "Differential sequential DCT", SOF),
    0xFFC6: ("SOF6", "Differential progressive DCT", SOF),
    0xFFC7: ("SOF7", "Differential spatial", SOF),
    0xFFC8: ("JPG", "Extension", None),
    0xFFC9: ("SOF9", "Extended sequential DCT (AC)", SOF),
    0xFFCA: ("SOF10", "Progressive DCT (AC)", SOF),
    0xFFCB: ("SOF11", "Spatial lossless DCT (AC)", SOF),
    0xFFCC: ("DAC", "Define arithmetic coding conditioning", Skip),
    0xFFCD: ("SOF13", "Differential sequential DCT (AC)", SOF),
    0xFFCE: ("SOF14", "Differential progressive DCT (AC)", SOF),
    0xFFCF: ("SOF15", "Differential spatial (AC)", SOF),
    0xFFD0: ("RST0", "Restart 0", None),
    0xFFD1: ("RST1", "Restart 1", None),
    0xFFD2: ("RST2", "Restart 2", None),
    0xFFD3: ("RST3", "Restart 3", None),
    0xFFD4: ("RST4", "Restart 4", None),
    0xFFD5: ("RST5", "Restart 5", None),
    0xFFD6: ("RST6", "Restart 6", None),
    0xFFD7: ("RST7", "Restart 7", None),
    0xFFD8: ("SOI", "Start of image", None),
    0xFFD9: ("EOI", "End of image", None),
    0xFFDA: ("SOS", "Start of scan", Skip),
    0xFFDB: ("DQT", "Define quantization table", DQT),
    0xFFDC: ("DNL", "Define number of lines", Skip),
    0xFFDD: ("DRI", "Define restart interval", Skip),
    0xFFDE: ("DHP", "Define hierarchical progression", SOF),
    0xFFDF: ("EXP", "Expand reference component", Skip),
    0xFFE0: ("APP0", "Application segment 0", APP),
    0xFFE1: ("APP1", "Application segment 1", APP),
    0xFFE2: ("APP2", "Application segment 2", APP),
    0xFFE3: ("APP3", "Application segment 3", APP),
    0xFFE4: ("APP4", "Application segment 4", APP),
    0xFFE5: ("APP5", "Application segment 5", APP),
    0xFFE6: ("APP6", "Application segment 6", APP),
    0xFFE7: ("APP7", "Application segment 7", APP),
    0xFFE8: ("APP8", "Application segment 8", APP),
    0xFFE9: ("APP9", "Application segment 9", APP),
    0xFFEA: ("APP10", "Application segment 10", APP),
    0xFFEB: ("APP11", "Application segment 11", APP),
    0xFFEC: ("APP12", "Application segment 12", APP),
    0xFFED: ("APP13", "Application segment 13", APP),
    0xFFEE: ("APP14", "Application segment 14", APP),
    0xFFEF: ("APP15", "Application segment 15", APP),
    0xFFF0: ("JPG0", "Extension 0", None),
    0xFFF1: ("JPG1", "Extension 1", None),
    0xFFF2: ("JPG2", "Extension 2", None),
    0xFFF3: ("JPG3", "Extension 3", None),
    0xFFF4: ("JPG4", "Extension 4", None),
    0xFFF5: ("JPG5", "Extension 5", None),
    0xFFF6: ("JPG6", "Extension 6", None),
    0xFFF7: ("JPG7", "Extension 7", None),
    0xFFF8: ("JPG8", "Extension 8", None),
    0xFFF9: ("JPG9", "Extension 9", None),
    0xFFFA: ("JPG10", "Extension 10", None),
    0xFFFB: ("JPG11", "Extension 11", None),
    0xFFFC: ("JPG12", "Extension 12", None),
    0xFFFD: ("JPG13", "Extension 13", None),
    0xFFFE: ("COM", "Comment", Skip)
}


def _accept(prefix):
    return prefix[0] == "\377"

##
# Image plugin for JPEG and JFIF images.

class JpegImageFile(ImageFile.ImageFile):

    format = "JPEG"
    format_description = "JPEG (ISO 10918)"

    def _open(self):

        s = self.fp.read(1)

        if ord(s[0]) != 255:
            raise SyntaxError("not a JPEG file")

        # Create attributes
        self.bits = self.layers = 0

        # JPEG specifics (internal)
        self.layer = []
        self.huffman_dc = {}
        self.huffman_ac = {}
        self.quantization = {}
        self.app = {} # compatibility
        self.applist = []

        while 1:

            s = s + self.fp.read(1)

            i = i16(s)

            if MARKER.has_key(i):
                name, description, handler = MARKER[i]
                # print hex(i), name, description
                if handler is not None:
                    handler(self, i)
                if i == 0xFFDA: # start of scan
                    rawmode = self.mode
                    if self.mode == "CMYK" and self.info.has_key("adobe"):
                        rawmode = "CMYK;I" # Photoshop 2.5 is broken!
                    self.tile = [("jpeg", (0,0) + self.size, 0, (rawmode, ""))]
                    # self.__offset = self.fp.tell()
                    break
                s = self.fp.read(1)
            elif i == 65535:
                # padded marker; move on
                s = "\xff"
            else:
                raise SyntaxError("no marker found")

    def draft(self, mode, size):

        if len(self.tile) != 1:
            return

        d, e, o, a = self.tile[0]
        scale = 0

        if a[0] == "RGB" and mode in ["L", "YCbCr"]:
            self.mode = mode
            a = mode, ""

        if size:
            scale = max(self.size[0] / size[0], self.size[1] / size[1])
            for s in [8, 4, 2, 1]:
                if scale >= s:
                    break
            e = e[0], e[1], (e[2]-e[0]+s-1)/s+e[0], (e[3]-e[1]+s-1)/s+e[1]
            self.size = ((self.size[0]+s-1)/s, (self.size[1]+s-1)/s)
            scale = s

        self.tile = [(d, e, o, a)]
        self.decoderconfig = (scale, 1)

        return self

    def load_djpeg(self):

        # ALTERNATIVE: handle JPEGs via the IJG command line utilities

        import tempfile, os
        file = tempfile.mktemp()
        os.system("djpeg %s >%s" % (self.filename, file))

        try:
            self.im = Image.core.open_ppm(file)
        finally:
            try: os.unlink(file)
            except: pass

        self.mode = self.im.mode
        self.size = self.im.size

        self.tile = []

    def _getexif(self):
        # Extract EXIF information.  This method is highly experimental,
        # and is likely to be replaced with something better in a future
        # version.
        import TiffImagePlugin, StringIO
        def fixup(value):
            if len(value) == 1:
                return value[0]
            return value
        # The EXIF record consists of a TIFF file embedded in a JPEG
        # application marker (!).
        try:
            data = self.info["exif"]
        except KeyError:
            return None
        file = StringIO.StringIO(data[6:])
        head = file.read(8)
        exif = {}
        # process dictionary
        info = TiffImagePlugin.ImageFileDirectory(head)
        info.load(file)
        for key, value in info.items():
            exif[key] = fixup(value)
        # get exif extension
        file.seek(info[0x8769][0])
        info = TiffImagePlugin.ImageFileDirectory(head)
        info.load(file)
        for key, value in info.items():
            exif[key] = fixup(value)
        return exif

# --------------------------------------------------------------------
# stuff to save JPEG files

RAWMODE = {
    "1": "L",
    "L": "L",
    "RGB": "RGB",
    "RGBA": "RGB",
    "RGBX": "RGB",
    "CMYK": "CMYK",
    "YCbCr": "YCbCr",
}

def _save(im, fp, filename):

    try:
        rawmode = RAWMODE[im.mode]
    except KeyError:
        raise IOError("cannot write mode %s as JPEG" % im.mode)

    dpi = im.encoderinfo.get("dpi", (0, 0))

    # get keyword arguments
    im.encoderconfig = (
        im.encoderinfo.get("quality", 0),
        im.encoderinfo.has_key("progressive"),
        im.encoderinfo.get("smooth", 0),
        im.encoderinfo.has_key("optimize"),
        im.encoderinfo.get("streamtype", 0),
        dpi[0], dpi[1]
        )

    ImageFile._save(im, fp, [("jpeg", (0,0)+im.size, 0, rawmode)])

def _save_cjpeg(im, fp, filename):
    # ALTERNATIVE: handle JPEGs via the IJG command line utilities.
    import os
    file = im._dump()
    os.system("cjpeg %s >%s" % (file, filename))
    try: os.unlink(file)
    except: pass

# -------------------------------------------------------------------q-
# Registry stuff

Image.register_open("JPEG", JpegImageFile, _accept)
Image.register_save("JPEG", _save)

Image.register_extension("JPEG", ".jfif")
Image.register_extension("JPEG", ".jpe")
Image.register_extension("JPEG", ".jpg")
Image.register_extension("JPEG", ".jpeg")

Image.register_mime("JPEG", "image/jpeg")
