mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-24 17:06:16 +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)
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
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
|
||||
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",
|
||||
|
|
|
@ -36,6 +36,7 @@ _plugins = ['BmpImagePlugin',
|
|||
'McIdasImagePlugin',
|
||||
'MicImagePlugin',
|
||||
'MpegImagePlugin',
|
||||
'MpoImagePlugin',
|
||||
'MspImagePlugin',
|
||||
'PalmImagePlugin',
|
||||
'PcdImagePlugin',
|
||||
|
|
|
@ -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')
|
||||
|
|
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