804 lines
27 KiB
Python
804 lines
27 KiB
Python
#
|
|
# The Python Imaging Library.
|
|
# $Id$
|
|
#
|
|
# JPEG (JFIF) file handling
|
|
#
|
|
# See "Digital Compression and Coding of Continuous-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)
|
|
# 2003-06-06 fl Added experimental EXIF GPSinfo decoder
|
|
# 2003-09-13 fl Extract COM markers
|
|
# 2009-09-06 fl Added icc_profile support (from Florian Hoech)
|
|
# 2009-03-06 fl Changed CMYK handling; always use Adobe polarity (0.6)
|
|
# 2009-03-08 fl Added subsampling support (from Justin Huff).
|
|
#
|
|
# 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.
|
|
#
|
|
|
|
from __future__ import print_function
|
|
|
|
import array
|
|
import struct
|
|
import io
|
|
import warnings
|
|
from . import Image, ImageFile, TiffImagePlugin
|
|
from ._binary import i8, o8, i16be as i16
|
|
from .JpegPresets import presets
|
|
from ._util import isStringType
|
|
|
|
__version__ = "0.6"
|
|
|
|
|
|
#
|
|
# Parser
|
|
|
|
def Skip(self, marker):
|
|
n = i16(self.fp.read(2))-2
|
|
ImageFile._safe_read(self.fp, n)
|
|
|
|
|
|
def APP(self, marker):
|
|
#
|
|
# Application marker. Store these in the APP dictionary.
|
|
# Also look for well-known application markers.
|
|
|
|
n = i16(self.fp.read(2))-2
|
|
s = ImageFile._safe_read(self.fp, n)
|
|
|
|
app = "APP%d" % (marker & 15)
|
|
|
|
self.app[app] = s # compatibility
|
|
self.applist.append((app, s))
|
|
|
|
if marker == 0xFFE0 and s[:4] == b"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 = i8(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] == b"Exif\0":
|
|
if "exif" not in self.info:
|
|
# extract Exif information (incomplete)
|
|
self.info["exif"] = s # FIXME: value will change
|
|
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
|
|
# extract FlashPix information (incomplete)
|
|
self.info["flashpix"] = s # FIXME: value will change
|
|
elif marker == 0xFFE2 and s[:12] == b"ICC_PROFILE\0":
|
|
# Since an ICC profile can be larger than the maximum size of
|
|
# a JPEG marker (64K), we need provisions to split it into
|
|
# multiple markers. The format defined by the ICC specifies
|
|
# one or more APP2 markers containing the following data:
|
|
# Identifying string ASCII "ICC_PROFILE\0" (12 bytes)
|
|
# Marker sequence number 1, 2, etc (1 byte)
|
|
# Number of markers Total of APP2's used (1 byte)
|
|
# Profile data (remainder of APP2 data)
|
|
# Decoders should use the marker sequence numbers to
|
|
# reassemble the profile, rather than assuming that the APP2
|
|
# markers appear in the correct sequence.
|
|
self.icclist.append(s)
|
|
elif marker == 0xFFEE and s[:5] == b"Adobe":
|
|
self.info["adobe"] = i16(s, 5)
|
|
# extract Adobe custom properties
|
|
try:
|
|
adobe_transform = i8(s[1])
|
|
except:
|
|
pass
|
|
else:
|
|
self.info["adobe_transform"] = adobe_transform
|
|
elif marker == 0xFFE2 and s[:4] == b"MPF\0":
|
|
# extract MPO information
|
|
self.info["mp"] = s[4:]
|
|
# offset is current location minus buffer size
|
|
# plus constant header size
|
|
self.info["mpoffset"] = self.fp.tell() - n + 4
|
|
|
|
# If DPI isn't in JPEG header, fetch from EXIF
|
|
if "dpi" not in self.info and "exif" in self.info:
|
|
try:
|
|
exif = self._getexif()
|
|
resolution_unit = exif[0x0128]
|
|
x_resolution = exif[0x011A]
|
|
try:
|
|
dpi = x_resolution[0] / x_resolution[1]
|
|
except TypeError:
|
|
dpi = x_resolution
|
|
if resolution_unit == 3: # cm
|
|
# 1 dpcm = 2.54 dpi
|
|
dpi *= 2.54
|
|
self.info["dpi"] = dpi, dpi
|
|
except (KeyError, SyntaxError, ZeroDivisionError):
|
|
# SyntaxError for invalid/unreadable exif
|
|
# KeyError for dpi not included
|
|
# ZeroDivisionError for invalid dpi rational value
|
|
self.info["dpi"] = 72, 72
|
|
|
|
|
|
def COM(self, marker):
|
|
#
|
|
# Comment marker. Store these in the APP dictionary.
|
|
n = i16(self.fp.read(2))-2
|
|
s = ImageFile._safe_read(self.fp, n)
|
|
|
|
self.app["COM"] = s # compatibility
|
|
self.applist.append(("COM", s))
|
|
|
|
|
|
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.
|
|
|
|
n = i16(self.fp.read(2))-2
|
|
s = ImageFile._safe_read(self.fp, n)
|
|
self.size = i16(s[3:]), i16(s[1:])
|
|
|
|
self.bits = i8(s[0])
|
|
if self.bits != 8:
|
|
raise SyntaxError("cannot handle %d-bit layers" % self.bits)
|
|
|
|
self.layers = i8(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["progressive"] = self.info["progression"] = 1
|
|
|
|
if self.icclist:
|
|
# fixup icc profile
|
|
self.icclist.sort() # sort by sequence number
|
|
if i8(self.icclist[0][13]) == len(self.icclist):
|
|
profile = []
|
|
for p in self.icclist:
|
|
profile.append(p[14:])
|
|
icc_profile = b"".join(profile)
|
|
else:
|
|
icc_profile = None # wrong number of fragments
|
|
self.info["icc_profile"] = icc_profile
|
|
self.icclist = None
|
|
|
|
for i in range(6, len(s), 3):
|
|
t = s[i:i+3]
|
|
# 4-tuples: id, vsamp, hsamp, qtable
|
|
self.layer.append((t[0], i8(t[1])//16, i8(t[1]) & 15, i8(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.
|
|
|
|
n = i16(self.fp.read(2))-2
|
|
s = ImageFile._safe_read(self.fp, n)
|
|
while len(s):
|
|
if len(s) < 65:
|
|
raise SyntaxError("bad quantization table marker")
|
|
v = i8(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", COM)
|
|
}
|
|
|
|
|
|
def _accept(prefix):
|
|
return prefix[0:1] == b"\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 i8(s) != 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 = []
|
|
self.icclist = []
|
|
|
|
while True:
|
|
|
|
i = i8(s)
|
|
if i == 0xFF:
|
|
s = s + self.fp.read(1)
|
|
i = i16(s)
|
|
else:
|
|
# Skip non-0xFF junk
|
|
s = self.fp.read(1)
|
|
continue
|
|
|
|
if i in MARKER:
|
|
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":
|
|
rawmode = "CMYK;I" # assume adobe conventions
|
|
self.tile = [("jpeg", (0, 0) + self.size, 0,
|
|
(rawmode, ""))]
|
|
# self.__offset = self.fp.tell()
|
|
break
|
|
s = self.fp.read(1)
|
|
elif i == 0 or i == 0xFFFF:
|
|
# padded marker or junk; move on
|
|
s = b"\xff"
|
|
elif i == 0xFF00: # Skip extraneous data (escaped 0xFF)
|
|
s = self.fp.read(1)
|
|
else:
|
|
raise SyntaxError("no marker found")
|
|
|
|
def load_read(self, read_bytes):
|
|
"""
|
|
internal: read more image data
|
|
For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker
|
|
so libjpeg can finish decoding
|
|
"""
|
|
s = self.fp.read(read_bytes)
|
|
|
|
if not s and ImageFile.LOAD_TRUNCATED_IMAGES:
|
|
# Premature EOF.
|
|
# Pretend file is finished adding EOI marker
|
|
return b"\xFF\xD9"
|
|
|
|
return s
|
|
|
|
def draft(self, mode, size):
|
|
|
|
if len(self.tile) != 1:
|
|
return
|
|
|
|
# Protect from second call
|
|
if self.decoderconfig:
|
|
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 = min(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, 0)
|
|
|
|
return self
|
|
|
|
def load_djpeg(self):
|
|
|
|
# ALTERNATIVE: handle JPEGs via the IJG command line utilities
|
|
|
|
import subprocess
|
|
import tempfile
|
|
import os
|
|
f, path = tempfile.mkstemp()
|
|
os.close(f)
|
|
if os.path.exists(self.filename):
|
|
subprocess.check_call(["djpeg", "-outfile", path, self.filename])
|
|
else:
|
|
raise ValueError("Invalid Filename")
|
|
|
|
try:
|
|
_im = Image.open(path)
|
|
_im.load()
|
|
self.im = _im.im
|
|
finally:
|
|
try:
|
|
os.unlink(path)
|
|
except OSError:
|
|
pass
|
|
|
|
self.mode = self.im.mode
|
|
self.size = self.im.size
|
|
|
|
self.tile = []
|
|
|
|
def _getexif(self):
|
|
return _getexif(self)
|
|
|
|
def _getmp(self):
|
|
return _getmp(self)
|
|
|
|
|
|
def _fixup_dict(src_dict):
|
|
# Helper function for _getexif()
|
|
# returns a dict with any single item tuples/lists as individual values
|
|
def _fixup(value):
|
|
try:
|
|
if len(value) == 1 and not isinstance(value, dict):
|
|
return value[0]
|
|
except:
|
|
pass
|
|
return value
|
|
|
|
return {k: _fixup(v) for k, v in src_dict.items()}
|
|
|
|
|
|
def _getexif(self):
|
|
# Extract EXIF information. This method is highly experimental,
|
|
# and is likely to be replaced with something better in a future
|
|
# version.
|
|
|
|
# The EXIF record consists of a TIFF file embedded in a JPEG
|
|
# application marker (!).
|
|
try:
|
|
data = self.info["exif"]
|
|
except KeyError:
|
|
return None
|
|
file = io.BytesIO(data[6:])
|
|
head = file.read(8)
|
|
# process dictionary
|
|
info = TiffImagePlugin.ImageFileDirectory_v1(head)
|
|
info.load(file)
|
|
exif = dict(_fixup_dict(info))
|
|
# get exif extension
|
|
try:
|
|
# exif field 0x8769 is an offset pointer to the location
|
|
# of the nested embedded exif ifd.
|
|
# It should be a long, but may be corrupted.
|
|
file.seek(exif[0x8769])
|
|
except (KeyError, TypeError):
|
|
pass
|
|
else:
|
|
info = TiffImagePlugin.ImageFileDirectory_v1(head)
|
|
info.load(file)
|
|
exif.update(_fixup_dict(info))
|
|
# get gpsinfo extension
|
|
try:
|
|
# exif field 0x8825 is an offset pointer to the location
|
|
# of the nested embedded gps exif ifd.
|
|
# It should be a long, but may be corrupted.
|
|
file.seek(exif[0x8825])
|
|
except (KeyError, TypeError):
|
|
pass
|
|
else:
|
|
info = TiffImagePlugin.ImageFileDirectory_v1(head)
|
|
info.load(file)
|
|
exif[0x8825] = _fixup_dict(info)
|
|
|
|
return exif
|
|
|
|
|
|
def _getmp(self):
|
|
# Extract MP information. This method was inspired by the "highly
|
|
# experimental" _getexif version that's been in use for years now,
|
|
# itself based on the ImageFileDirectory class in the TIFF plug-in.
|
|
|
|
# The MP record essentially consists of a TIFF file embedded in a JPEG
|
|
# application marker.
|
|
try:
|
|
data = self.info["mp"]
|
|
except KeyError:
|
|
return None
|
|
file_contents = io.BytesIO(data)
|
|
head = file_contents.read(8)
|
|
endianness = '>' if head[:4] == b'\x4d\x4d\x00\x2a' else '<'
|
|
# process dictionary
|
|
try:
|
|
info = TiffImagePlugin.ImageFileDirectory_v2(head)
|
|
info.load(file_contents)
|
|
mp = dict(info)
|
|
except:
|
|
raise SyntaxError("malformed MP Index (unreadable directory)")
|
|
# it's an error not to have a number of images
|
|
try:
|
|
quant = mp[0xB001]
|
|
except KeyError:
|
|
raise SyntaxError("malformed MP Index (no number of images)")
|
|
# get MP entries
|
|
mpentries = []
|
|
try:
|
|
rawmpentries = mp[0xB002]
|
|
for entrynum in range(0, quant):
|
|
unpackedentry = struct.unpack_from(
|
|
'{}LLLHH'.format(endianness), rawmpentries, entrynum * 16)
|
|
labels = ('Attribute', 'Size', 'DataOffset', 'EntryNo1',
|
|
'EntryNo2')
|
|
mpentry = dict(zip(labels, unpackedentry))
|
|
mpentryattr = {
|
|
'DependentParentImageFlag': bool(mpentry['Attribute'] &
|
|
(1 << 31)),
|
|
'DependentChildImageFlag': bool(mpentry['Attribute'] &
|
|
(1 << 30)),
|
|
'RepresentativeImageFlag': bool(mpentry['Attribute'] &
|
|
(1 << 29)),
|
|
'Reserved': (mpentry['Attribute'] & (3 << 27)) >> 27,
|
|
'ImageDataFormat': (mpentry['Attribute'] & (7 << 24)) >> 24,
|
|
'MPType': mpentry['Attribute'] & 0x00FFFFFF
|
|
}
|
|
if mpentryattr['ImageDataFormat'] == 0:
|
|
mpentryattr['ImageDataFormat'] = 'JPEG'
|
|
else:
|
|
raise SyntaxError("unsupported picture format in MPO")
|
|
mptypemap = {
|
|
0x000000: 'Undefined',
|
|
0x010001: 'Large Thumbnail (VGA Equivalent)',
|
|
0x010002: 'Large Thumbnail (Full HD Equivalent)',
|
|
0x020001: 'Multi-Frame Image (Panorama)',
|
|
0x020002: 'Multi-Frame Image: (Disparity)',
|
|
0x020003: 'Multi-Frame Image: (Multi-Angle)',
|
|
0x030000: 'Baseline MP Primary Image'
|
|
}
|
|
mpentryattr['MPType'] = mptypemap.get(mpentryattr['MPType'],
|
|
'Unknown')
|
|
mpentry['Attribute'] = mpentryattr
|
|
mpentries.append(mpentry)
|
|
mp[0xB002] = mpentries
|
|
except KeyError:
|
|
raise SyntaxError("malformed MP Index (bad MP Entry)")
|
|
# Next we should try and parse the individual image unique ID list;
|
|
# we don't because I've never seen this actually used in a real MPO
|
|
# file and so can't test it.
|
|
return mp
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
# stuff to save JPEG files
|
|
|
|
RAWMODE = {
|
|
"1": "L",
|
|
"L": "L",
|
|
"RGB": "RGB",
|
|
"RGBX": "RGB",
|
|
"CMYK": "CMYK;I", # assume adobe conventions
|
|
"YCbCr": "YCbCr",
|
|
}
|
|
|
|
zigzag_index = (0, 1, 5, 6, 14, 15, 27, 28,
|
|
2, 4, 7, 13, 16, 26, 29, 42,
|
|
3, 8, 12, 17, 25, 30, 41, 43,
|
|
9, 11, 18, 24, 31, 40, 44, 53,
|
|
10, 19, 23, 32, 39, 45, 52, 54,
|
|
20, 22, 33, 38, 46, 51, 55, 60,
|
|
21, 34, 37, 47, 50, 56, 59, 61,
|
|
35, 36, 48, 49, 57, 58, 62, 63)
|
|
|
|
samplings = {(1, 1, 1, 1, 1, 1): 0,
|
|
(2, 1, 1, 1, 1, 1): 1,
|
|
(2, 2, 1, 1, 1, 1): 2,
|
|
}
|
|
|
|
|
|
def convert_dict_qtables(qtables):
|
|
qtables = [qtables[key] for key in range(len(qtables)) if key in qtables]
|
|
for idx, table in enumerate(qtables):
|
|
qtables[idx] = [table[i] for i in zigzag_index]
|
|
return qtables
|
|
|
|
|
|
def get_sampling(im):
|
|
# There's no subsampling when image have only 1 layer
|
|
# (grayscale images) or when they are CMYK (4 layers),
|
|
# so set subsampling to default value.
|
|
#
|
|
# NOTE: currently Pillow can't encode JPEG to YCCK format.
|
|
# If YCCK support is added in the future, subsampling code will have
|
|
# to be updated (here and in JpegEncode.c) to deal with 4 layers.
|
|
if not hasattr(im, 'layers') or im.layers in (1, 4):
|
|
return -1
|
|
sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3]
|
|
return samplings.get(sampling, -1)
|
|
|
|
|
|
def _save(im, fp, filename):
|
|
|
|
try:
|
|
rawmode = RAWMODE[im.mode]
|
|
except KeyError:
|
|
raise IOError("cannot write mode %s as JPEG" % im.mode)
|
|
|
|
info = im.encoderinfo
|
|
|
|
dpi = [int(round(x)) for x in info.get("dpi", (0, 0))]
|
|
|
|
quality = info.get("quality", 0)
|
|
subsampling = info.get("subsampling", -1)
|
|
qtables = info.get("qtables")
|
|
|
|
if quality == "keep":
|
|
quality = 0
|
|
subsampling = "keep"
|
|
qtables = "keep"
|
|
elif quality in presets:
|
|
preset = presets[quality]
|
|
quality = 0
|
|
subsampling = preset.get('subsampling', -1)
|
|
qtables = preset.get('quantization')
|
|
elif not isinstance(quality, int):
|
|
raise ValueError("Invalid quality setting")
|
|
else:
|
|
if subsampling in presets:
|
|
subsampling = presets[subsampling].get('subsampling', -1)
|
|
if isStringType(qtables) and qtables in presets:
|
|
qtables = presets[qtables].get('quantization')
|
|
|
|
if subsampling == "4:4:4":
|
|
subsampling = 0
|
|
elif subsampling == "4:2:2":
|
|
subsampling = 1
|
|
elif subsampling == "4:2:0":
|
|
subsampling = 2
|
|
elif subsampling == "4:1:1":
|
|
# For compatibility. Before Pillow 4.3, 4:1:1 actually meant 4:2:0.
|
|
# Set 4:2:0 if someone is still using that value.
|
|
subsampling = 2
|
|
elif subsampling == "keep":
|
|
if im.format != "JPEG":
|
|
raise ValueError(
|
|
"Cannot use 'keep' when original image is not a JPEG")
|
|
subsampling = get_sampling(im)
|
|
|
|
def validate_qtables(qtables):
|
|
if qtables is None:
|
|
return qtables
|
|
if isStringType(qtables):
|
|
try:
|
|
lines = [int(num) for line in qtables.splitlines()
|
|
for num in line.split('#', 1)[0].split()]
|
|
except ValueError:
|
|
raise ValueError("Invalid quantization table")
|
|
else:
|
|
qtables = [lines[s:s+64] for s in range(0, len(lines), 64)]
|
|
if isinstance(qtables, (tuple, list, dict)):
|
|
if isinstance(qtables, dict):
|
|
qtables = convert_dict_qtables(qtables)
|
|
elif isinstance(qtables, tuple):
|
|
qtables = list(qtables)
|
|
if not (0 < len(qtables) < 5):
|
|
raise ValueError("None or too many quantization tables")
|
|
for idx, table in enumerate(qtables):
|
|
try:
|
|
if len(table) != 64:
|
|
raise TypeError
|
|
table = array.array('B', table)
|
|
except TypeError:
|
|
raise ValueError("Invalid quantization table")
|
|
else:
|
|
qtables[idx] = list(table)
|
|
return qtables
|
|
|
|
if qtables == "keep":
|
|
if im.format != "JPEG":
|
|
raise ValueError(
|
|
"Cannot use 'keep' when original image is not a JPEG")
|
|
qtables = getattr(im, "quantization", None)
|
|
qtables = validate_qtables(qtables)
|
|
|
|
extra = b""
|
|
|
|
icc_profile = info.get("icc_profile")
|
|
if icc_profile:
|
|
ICC_OVERHEAD_LEN = 14
|
|
MAX_BYTES_IN_MARKER = 65533
|
|
MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN
|
|
markers = []
|
|
while icc_profile:
|
|
markers.append(icc_profile[:MAX_DATA_BYTES_IN_MARKER])
|
|
icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:]
|
|
i = 1
|
|
for marker in markers:
|
|
size = struct.pack(">H", 2 + ICC_OVERHEAD_LEN + len(marker))
|
|
extra += (b"\xFF\xE2" + size + b"ICC_PROFILE\0" + o8(i) +
|
|
o8(len(markers)) + marker)
|
|
i += 1
|
|
|
|
# "progressive" is the official name, but older documentation
|
|
# says "progression"
|
|
# FIXME: issue a warning if the wrong form is used (post-1.1.7)
|
|
progressive = (info.get("progressive", False) or
|
|
info.get("progression", False))
|
|
|
|
optimize = info.get("optimize", False)
|
|
|
|
# get keyword arguments
|
|
im.encoderconfig = (
|
|
quality,
|
|
progressive,
|
|
info.get("smooth", 0),
|
|
optimize,
|
|
info.get("streamtype", 0),
|
|
dpi[0], dpi[1],
|
|
subsampling,
|
|
qtables,
|
|
extra,
|
|
info.get("exif", b"")
|
|
)
|
|
|
|
# if we optimize, libjpeg needs a buffer big enough to hold the whole image
|
|
# in a shot. Guessing on the size, at im.size bytes. (raw pixel size is
|
|
# channels*size, this is a value that's been used in a django patch.
|
|
# https://github.com/matthewwithanm/django-imagekit/issues/50
|
|
bufsize = 0
|
|
if optimize or progressive:
|
|
# CMYK can be bigger
|
|
if im.mode == 'CMYK':
|
|
bufsize = 4 * im.size[0] * im.size[1]
|
|
# keep sets quality to 0, but the actual value may be high.
|
|
elif quality >= 95 or quality == 0:
|
|
bufsize = 2 * im.size[0] * im.size[1]
|
|
else:
|
|
bufsize = im.size[0] * im.size[1]
|
|
|
|
# The exif info needs to be written as one block, + APP1, + one spare byte.
|
|
# Ensure that our buffer is big enough. Same with the icc_profile block.
|
|
bufsize = max(ImageFile.MAXBLOCK, bufsize, len(info.get("exif", b"")) + 5,
|
|
len(extra) + 1)
|
|
|
|
ImageFile._save(im, fp, [("jpeg", (0, 0)+im.size, 0, rawmode)], bufsize)
|
|
|
|
|
|
def _save_cjpeg(im, fp, filename):
|
|
# ALTERNATIVE: handle JPEGs via the IJG command line utilities.
|
|
import os
|
|
import subprocess
|
|
tempfile = im._dump()
|
|
subprocess.check_call(["cjpeg", "-outfile", filename, tempfile])
|
|
try:
|
|
os.unlink(tempfile)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
##
|
|
# Factory for making JPEG and MPO instances
|
|
def jpeg_factory(fp=None, filename=None):
|
|
im = JpegImageFile(fp, filename)
|
|
try:
|
|
mpheader = im._getmp()
|
|
if mpheader[45057] > 1:
|
|
# It's actually an MPO
|
|
from .MpoImagePlugin import MpoImageFile
|
|
im = MpoImageFile(fp, filename)
|
|
except (TypeError, IndexError):
|
|
# It is really a JPEG
|
|
pass
|
|
except SyntaxError:
|
|
warnings.warn("Image appears to be a malformed MPO file, it will be "
|
|
"interpreted as a base JPEG file")
|
|
return im
|
|
|
|
|
|
# -------------------------------------------------------------------q-
|
|
# Registry stuff
|
|
|
|
Image.register_open(JpegImageFile.format, jpeg_factory, _accept)
|
|
Image.register_save(JpegImageFile.format, _save)
|
|
|
|
Image.register_extensions(JpegImageFile.format, [".jfif", ".jpe", ".jpg", ".jpeg"])
|
|
|
|
Image.register_mime(JpegImageFile.format, "image/jpeg")
|