mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-25 17:36:18 +03:00
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:
parent
2609a24221
commit
53b7f6294b
13
PIL/Image.py
13
PIL/Image.py
|
@ -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
|
||||||
|
|
|
@ -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
70
PIL/MpoImagePlugin.py
Normal 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")
|
|
@ -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",
|
||||||
|
|
|
@ -36,6 +36,7 @@ _plugins = ['BmpImagePlugin',
|
||||||
'McIdasImagePlugin',
|
'McIdasImagePlugin',
|
||||||
'MicImagePlugin',
|
'MicImagePlugin',
|
||||||
'MpegImagePlugin',
|
'MpegImagePlugin',
|
||||||
|
'MpoImagePlugin',
|
||||||
'MspImagePlugin',
|
'MspImagePlugin',
|
||||||
'PalmImagePlugin',
|
'PalmImagePlugin',
|
||||||
'PcdImagePlugin',
|
'PcdImagePlugin',
|
||||||
|
|
|
@ -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
61
Tests/test_file_mpo.py
Normal 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
|
Loading…
Reference in New Issue
Block a user