First steps toward MPO support.

Allows Pillow to distinguish between JPEGs and MPOs, and provides some
MPO metadata handling. Does not yet handle multiple frames.
This commit is contained in:
Eric W. Brown 2014-07-16 11:36:56 -04:00
parent 2609a24221
commit 53b7f6294b
7 changed files with 222 additions and 10 deletions

View File

@ -2233,6 +2233,19 @@ def open(fp, mode="r"):
fp.seek(0) fp.seek(0)
im = factory(fp, filename) im = factory(fp, filename)
_decompression_bomb_check(im.size) _decompression_bomb_check(im.size)
if i == 'JPEG':
# Things are more complicated for JPEGs; we need to parse
# more deeply than 16 characters and check the contents of
# a potential MP header block to be sure.
mpheader = im._getmp()
try:
if mpheader[45057] > 1:
# It's actually an MPO
factory, accept = OPEN['MPO']
im = factory(fp, filename)
except (TypeError, IndexError):
# It is really a JPEG
pass
return im return im
except (SyntaxError, IndexError, TypeError): except (SyntaxError, IndexError, TypeError):
# import traceback # import traceback

View File

@ -36,7 +36,8 @@ __version__ = "0.6"
import array import array
import struct import struct
from PIL import Image, ImageFile, _binary import io
from PIL import Image, ImageFile, TiffImagePlugin, _binary
from PIL.JpegPresets import presets from PIL.JpegPresets import presets
from PIL._util import isStringType from PIL._util import isStringType
@ -110,6 +111,9 @@ def APP(self, marker):
pass pass
else: else:
self.info["adobe_transform"] = adobe_transform self.info["adobe_transform"] = adobe_transform
elif marker == 0xFFE2 and s[:4] == b"MPF\0":
# extract MPO information
self.info["mp"] = s[4:]
def COM(self, marker): def COM(self, marker):
@ -380,18 +384,22 @@ class JpegImageFile(ImageFile.ImageFile):
def _getexif(self): def _getexif(self):
return _getexif(self) return _getexif(self)
def _getmp(self):
return _getmp(self)
def _fixup(value):
# Helper function for _getexif() and _getmp()
if len(value) == 1:
return value[0]
return value
def _getexif(self): def _getexif(self):
# Extract EXIF information. This method is highly experimental, # Extract EXIF information. This method is highly experimental,
# and is likely to be replaced with something better in a future # and is likely to be replaced with something better in a future
# version. # version.
from PIL import TiffImagePlugin
import io
def fixup(value):
if len(value) == 1:
return value[0]
return value
# The EXIF record consists of a TIFF file embedded in a JPEG # The EXIF record consists of a TIFF file embedded in a JPEG
# application marker (!). # application marker (!).
try: try:
@ -405,7 +413,7 @@ def _getexif(self):
info = TiffImagePlugin.ImageFileDirectory(head) info = TiffImagePlugin.ImageFileDirectory(head)
info.load(file) info.load(file)
for key, value in info.items(): for key, value in info.items():
exif[key] = fixup(value) exif[key] = _fixup(value)
# get exif extension # get exif extension
try: try:
file.seek(exif[0x8769]) file.seek(exif[0x8769])
@ -415,7 +423,7 @@ def _getexif(self):
info = TiffImagePlugin.ImageFileDirectory(head) info = TiffImagePlugin.ImageFileDirectory(head)
info.load(file) info.load(file)
for key, value in info.items(): for key, value in info.items():
exif[key] = fixup(value) exif[key] = _fixup(value)
# get gpsinfo extension # get gpsinfo extension
try: try:
file.seek(exif[0x8825]) file.seek(exif[0x8825])
@ -426,9 +434,32 @@ def _getexif(self):
info.load(file) info.load(file)
exif[0x8825] = gps = {} exif[0x8825] = gps = {}
for key, value in info.items(): for key, value in info.items():
gps[key] = fixup(value) gps[key] = _fixup(value)
return exif 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 = io.BytesIO(data)
head = file.read(8)
mp = {}
# process dictionary
info = TiffImagePlugin.ImageFileDirectory(head)
info.load(file)
for key, value in info.items():
mp[key] = _fixup(value)
return mp
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# stuff to save JPEG files # stuff to save JPEG files

70
PIL/MpoImagePlugin.py Normal file
View File

@ -0,0 +1,70 @@
#
# The Python Imaging Library.
# $Id$
#
# MPO file handling
#
# See "Multi-Picture Format" (CIPA DC-007-Translation 2009, Standard of the
# Camera & Imaging Products Association)
#
# The multi-picture object combines multiple JPEG images (with a modified EXIF
# data format) into a single file. While it can theoretically be used much like
# a GIF animation, it is commonly used to represent 3D photographs and is (as
# of this writing) the most commonly used format by 3D cameras.
#
# History:
# 2014-03-13 Feneric Created
#
# See the README file for information on usage and redistribution.
#
__version__ = "0.1"
from PIL import Image, JpegImagePlugin
def _accept(prefix):
return JpegImagePlugin._accept(prefix)
def _save(im, fp, filename):
return JpegImagePlugin._save(im, fp, filename)
##
# Image plugin for MPO images.
class MpoImageFile(JpegImagePlugin.JpegImageFile):
format = "MPO"
format_description = "MPO (CIPA DC-007)"
def _open(self):
JpegImagePlugin.JpegImageFile._open(self)
self.__fp = self.fp # FIXME: hack
self.__rewind = self.fp.tell()
self.seek(0) # get ready to read first frame
def seek(self, frame):
if frame == 0:
# rewind
self.__offset = 0
self.dispose = None
self.__frame = -1
self.__fp.seek(self.__rewind)
if frame != self.__frame + 1:
raise ValueError("cannot seek to frame %d" % frame)
self.__frame = frame
def tell(self):
return self.__frame
# -------------------------------------------------------------------q-
# Registry stuff
Image.register_open("MPO", MpoImageFile, _accept)
Image.register_save("MPO", _save)
Image.register_extension("MPO", ".mpo")
Image.register_mime("MPO", "image/mpo")

View File

@ -147,6 +147,38 @@ TAGS = {
# ICC Profile # ICC Profile
34675: "ICCProfile", 34675: "ICCProfile",
# Additional Exif Info
36864: "ExifVersion",
36867: "DateTimeOriginal",
36868: "DateTImeDigitized",
37121: "ComponentsConfiguration",
37377: "ShutterSpeedValue",
37378: "ApertureValue",
37379: "BrightnessValue",
37380: "ExposureBiasValue",
37381: "MaxApertureValue",
37382: "SubjectDistance",
37383: "MeteringMode",
37384: "LightSource",
37385: "Flash",
37386: "FocalLength",
37396: "SubjectArea",
37500: "MakerNote",
37510: "UserComment",
40960: "FlashPixVersion",
40961: "ColorSpace",
40962: "PixelXDimension",
40963: "PixelYDimension",
40965: "InteroperabilityIFDPointer",
42016: "ImageUniqueID",
# MP Info
45056: "MPFVersion",
45057: "NumberOfImages",
45058: "MPEntry",
45059: "ImageUIDList",
45060: "TotalFrames",
# Adobe DNG # Adobe DNG
50706: "DNGVersion", 50706: "DNGVersion",
50707: "DNGBackwardVersion", 50707: "DNGBackwardVersion",

View File

@ -36,6 +36,7 @@ _plugins = ['BmpImagePlugin',
'McIdasImagePlugin', 'McIdasImagePlugin',
'MicImagePlugin', 'MicImagePlugin',
'MpegImagePlugin', 'MpegImagePlugin',
'MpoImagePlugin',
'MspImagePlugin', 'MspImagePlugin',
'PalmImagePlugin', 'PalmImagePlugin',
'PcdImagePlugin', 'PcdImagePlugin',

View File

@ -216,6 +216,10 @@ class TestFileJpeg(PillowTestCase):
info = im._getexif() info = im._getexif()
self.assertEqual(info[305], 'Adobe Photoshop CS Macintosh') self.assertEqual(info[305], 'Adobe Photoshop CS Macintosh')
def test_mp(self):
im = Image.open("Tests/images/pil_sample_rgb.jpg")
self.assertIsNone(im._getmp())
def test_quality_keep(self): def test_quality_keep(self):
im = Image.open("Tests/images/lena.jpg") im = Image.open("Tests/images/lena.jpg")
f = self.tempfile('temp.jpg') f = self.tempfile('temp.jpg')

61
Tests/test_file_mpo.py Normal file
View File

@ -0,0 +1,61 @@
from helper import unittest, PillowTestCase
from io import BytesIO
from PIL import Image
test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
class TestFileMpo(PillowTestCase):
def setUp(self):
codecs = dir(Image.core)
if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs:
self.skipTest("jpeg support not available")
def roundtrip(self, im, **options):
out = BytesIO()
im.save(out, "MPO", **options)
bytes = out.tell()
out.seek(0)
im = Image.open(out)
im.bytes = bytes # for testing only
return im
def test_sanity(self):
for test_file in test_files:
im = Image.open(test_file)
im.load()
self.assertEqual(im.mode, "RGB")
self.assertEqual(im.size, (640, 480))
self.assertEqual(im.format, "MPO")
def test_app(self):
for test_file in test_files:
# Test APP/COM reader (@PIL135)
im = Image.open(test_file)
self.assertEqual(im.applist[0][0], 'APP1')
self.assertEqual(im.applist[1][0], 'APP2')
self.assertEqual(im.applist[1][1][:16],
b'MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00')
self.assertEqual(len(im.applist), 2)
def test_exif(self):
for test_file in test_files:
im = Image.open(test_file)
info = im._getexif()
self.assertEqual(info[272], 'Nintendo 3DS')
self.assertEqual(info[296], 2)
self.assertEqual(info[34665], 188)
def test_mp(self):
for test_file in test_files:
im = Image.open(test_file)
info = im._getmp()
self.assertEqual(info[45056], '0100')
self.assertEqual(info[45057], 2)
if __name__ == '__main__':
unittest.main()
# End of file