diff --git a/PIL/Image.py b/PIL/Image.py index ea8cc6155..2aa2a1f31 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -2233,6 +2233,19 @@ def open(fp, mode="r"): fp.seek(0) im = factory(fp, filename) _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 except (SyntaxError, IndexError, TypeError): # import traceback diff --git a/PIL/JpegImagePlugin.py b/PIL/JpegImagePlugin.py index a434c5581..7dfdfa308 100644 --- a/PIL/JpegImagePlugin.py +++ b/PIL/JpegImagePlugin.py @@ -36,7 +36,8 @@ __version__ = "0.6" import array import struct -from PIL import Image, ImageFile, _binary +import io +from PIL import Image, ImageFile, TiffImagePlugin, _binary from PIL.JpegPresets import presets from PIL._util import isStringType @@ -110,6 +111,9 @@ def APP(self, marker): 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:] def COM(self, marker): @@ -380,18 +384,22 @@ class JpegImageFile(ImageFile.ImageFile): def _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): # Extract EXIF information. This method is highly experimental, # and is likely to be replaced with something better in a future # 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 # application marker (!). try: @@ -405,7 +413,7 @@ def _getexif(self): info = TiffImagePlugin.ImageFileDirectory(head) info.load(file) for key, value in info.items(): - exif[key] = fixup(value) + exif[key] = _fixup(value) # get exif extension try: file.seek(exif[0x8769]) @@ -415,7 +423,7 @@ def _getexif(self): info = TiffImagePlugin.ImageFileDirectory(head) info.load(file) for key, value in info.items(): - exif[key] = fixup(value) + exif[key] = _fixup(value) # get gpsinfo extension try: file.seek(exif[0x8825]) @@ -426,9 +434,32 @@ def _getexif(self): info.load(file) exif[0x8825] = gps = {} for key, value in info.items(): - gps[key] = fixup(value) + gps[key] = _fixup(value) 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 diff --git a/PIL/MpoImagePlugin.py b/PIL/MpoImagePlugin.py new file mode 100644 index 000000000..18f32bd48 --- /dev/null +++ b/PIL/MpoImagePlugin.py @@ -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") diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index 92a4b5afc..ccbd56507 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -147,6 +147,38 @@ TAGS = { # ICC Profile 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 50706: "DNGVersion", 50707: "DNGBackwardVersion", diff --git a/PIL/__init__.py b/PIL/__init__.py index d446aa19b..56edaf247 100644 --- a/PIL/__init__.py +++ b/PIL/__init__.py @@ -36,6 +36,7 @@ _plugins = ['BmpImagePlugin', 'McIdasImagePlugin', 'MicImagePlugin', 'MpegImagePlugin', + 'MpoImagePlugin', 'MspImagePlugin', 'PalmImagePlugin', 'PcdImagePlugin', diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 69c07d2dc..3bf757332 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -216,6 +216,10 @@ class TestFileJpeg(PillowTestCase): info = im._getexif() 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): im = Image.open("Tests/images/lena.jpg") f = self.tempfile('temp.jpg') diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py new file mode 100644 index 000000000..17f9a66fb --- /dev/null +++ b/Tests/test_file_mpo.py @@ -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