This commit is contained in:
Riley Lahd 2019-03-11 08:26:48 -06:00
commit c57bfb9a7b
16 changed files with 209 additions and 57 deletions

View File

@ -8,6 +8,18 @@ Changelog (Pillow)
- Python 2.7 support will be removed in Pillow 7.0.0 #3682 - Python 2.7 support will be removed in Pillow 7.0.0 #3682
[hugovk] [hugovk]
- Add EXIF support for PNG #3674
[radarhere]
- Add option to set dither param on quantize #3699
[glasnt]
- Add reading of DDS uncompressed RGB data #3673
[radarhere]
- Correct length of Tiff BYTE tags #3672
[radarhere]
- Add DIB saving and loading through Image open #3691 - Add DIB saving and loading through Image open #3691
[radarhere] [radarhere]

BIN
Tests/images/exif.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

BIN
Tests/images/uncompressed_rgb.dds Executable file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Binary file not shown.

View File

@ -7,6 +7,7 @@ TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds"
TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds" TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds" TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"
TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/uncompressed_rgb.dds"
class TestFileDds(PillowTestCase): class TestFileDds(PillowTestCase):
@ -67,6 +68,24 @@ class TestFileDds(PillowTestCase):
self.assert_image_equal(target, im) self.assert_image_equal(target, im)
def test_unimplemented_dxgi_format(self):
self.assertRaises(NotImplementedError, Image.open,
"Tests/images/unimplemented_dxgi_format.dds")
def test_uncompressed_rgb(self):
"""Check uncompressed RGB images can be opened"""
target = Image.open(TEST_FILE_UNCOMPRESSED_RGB.replace('.dds', '.png'))
im = Image.open(TEST_FILE_UNCOMPRESSED_RGB)
im.load()
self.assertEqual(im.format, "DDS")
self.assertEqual(im.mode, "RGBA")
self.assertEqual(im.size, (800, 600))
self.assert_image_equal(target, im)
def test__validate_true(self): def test__validate_true(self):
"""Check valid prefix""" """Check valid prefix"""
# Arrange # Arrange
@ -110,3 +129,7 @@ class TestFileDds(PillowTestCase):
im.load() im.load()
self.assertRaises(IOError, short_file) self.assertRaises(IOError, short_file)
def test_unimplemented_pixel_format(self):
self.assertRaises(NotImplementedError, Image.open,
"Tests/images/unimplemented_pixel_format.dds")

View File

@ -234,11 +234,11 @@ class TestFileLibTiff(LibTiffTestCase):
def test_custom_metadata(self): def test_custom_metadata(self):
custom = { custom = {
37000: 4, 37000: [4, TiffTags.SHORT],
37001: 4.2, 37001: [4.2, TiffTags.RATIONAL],
37002: 'custom tag value', 37002: ['custom tag value', TiffTags.ASCII],
37003: u'custom tag value', 37003: [u'custom tag value', TiffTags.ASCII],
37004: b'custom tag value' 37004: [b'custom tag value', TiffTags.BYTE]
} }
libtiff_version = TiffImagePlugin._libtiff_version() libtiff_version = TiffImagePlugin._libtiff_version()
@ -251,17 +251,33 @@ class TestFileLibTiff(LibTiffTestCase):
for libtiff in libtiffs: for libtiff in libtiffs:
TiffImagePlugin.WRITE_LIBTIFF = libtiff TiffImagePlugin.WRITE_LIBTIFF = libtiff
im = hopper() def check_tags(tiffinfo):
im = hopper()
out = self.tempfile("temp.tif") out = self.tempfile("temp.tif")
im.save(out, tiffinfo=custom) im.save(out, tiffinfo=tiffinfo)
TiffImagePlugin.WRITE_LIBTIFF = False
reloaded = Image.open(out) reloaded = Image.open(out)
for tag, value in custom.items(): for tag, value in tiffinfo.items():
if libtiff and isinstance(value, bytes): reloaded_value = reloaded.tag_v2[tag]
value = value.decode() if isinstance(reloaded_value, TiffImagePlugin.IFDRational):
self.assertEqual(reloaded.tag_v2[tag], value) reloaded_value = float(reloaded_value)
if libtiff and isinstance(value, bytes):
value = value.decode()
self.assertEqual(reloaded_value, value)
# Test with types
ifd = TiffImagePlugin.ImageFileDirectory_v2()
for tag, tagdata in custom.items():
ifd[tag] = tagdata[0]
ifd.tagtype[tag] = tagdata[1]
check_tags(ifd)
# Test without types
check_tags({tag: tagdata[0] for tag, tagdata in custom.items()})
TiffImagePlugin.WRITE_LIBTIFF = False
def test_int_dpi(self): def test_int_dpi(self):
# issue #1765 # issue #1765

View File

@ -590,6 +590,40 @@ class TestFilePng(PillowTestCase):
im = Image.open("Tests/images/hopper_idat_after_image_end.png") im = Image.open("Tests/images/hopper_idat_after_image_end.png")
self.assertEqual(im.text, {'TXT': 'VALUE', 'ZIP': 'VALUE'}) self.assertEqual(im.text, {'TXT': 'VALUE', 'ZIP': 'VALUE'})
def test_exif(self):
im = Image.open("Tests/images/exif.png")
exif = im._getexif()
self.assertEqual(exif[274], 1)
def test_exif_save(self):
im = Image.open("Tests/images/exif.png")
test_file = self.tempfile("temp.png")
im.save(test_file)
reloaded = Image.open(test_file)
exif = reloaded._getexif()
self.assertEqual(exif[274], 1)
def test_exif_from_jpg(self):
im = Image.open("Tests/images/pil_sample_rgb.jpg")
test_file = self.tempfile("temp.png")
im.save(test_file)
reloaded = Image.open(test_file)
exif = reloaded._getexif()
self.assertEqual(exif[305], "Adobe Photoshop CS Macintosh")
def test_exif_argument(self):
im = Image.open(TEST_PNG_FILE)
test_file = self.tempfile("temp.png")
im.save(test_file, exif=b"exifstring")
reloaded = Image.open(test_file)
self.assertEqual(reloaded.info["exif"], b"Exif\x00\x00exifstring")
@unittest.skipUnless(HAVE_WEBP and _webp.HAVE_WEBPANIM, @unittest.skipUnless(HAVE_WEBP and _webp.HAVE_WEBPANIM,
"WebP support not installed with animation") "WebP support not installed with animation")
def test_apng(self): def test_apng(self):

View File

@ -46,3 +46,19 @@ class TestImageQuantize(PillowTestCase):
converted = image.quantize() converted = image.quantize()
self.assert_image(converted, 'P', converted.size) self.assert_image(converted, 'P', converted.size)
self.assert_image_similar(converted.convert('RGB'), image, 1) self.assert_image_similar(converted.convert('RGB'), image, 1)
def test_quantize_no_dither(self):
image = hopper()
palette = Image.open('Tests/images/caption_6_33_22.png').convert('P')
converted = image.quantize(dither=0, palette=palette)
self.assert_image(converted, 'P', converted.size)
def test_quantize_dither_diff(self):
image = hopper()
palette = Image.open('Tests/images/caption_6_33_22.png').convert('P')
dither = image.quantize(dither=1, palette=palette)
nodither = image.quantize(dither=0, palette=palette)
self.assertNotEqual(dither.tobytes(), nodither.tobytes())

View File

@ -462,6 +462,10 @@ PNG
Pillow identifies, reads, and writes PNG files containing ``1``, ``L``, ``P``, Pillow identifies, reads, and writes PNG files containing ``1``, ``L``, ``P``,
``RGB``, or ``RGBA`` data. Interlaced files are supported as of v1.1.7. ``RGB``, or ``RGBA`` data. Interlaced files are supported as of v1.1.7.
As of Pillow 6.0, EXIF data can be read from PNG images. However, unlike other
image formats, EXIF data is not guaranteed to have been read until
:py:meth:`~PIL.Image.Image.load` has been called.
The :py:meth:`~PIL.Image.Image.open` method sets the following The :py:meth:`~PIL.Image.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties, when appropriate: :py:attr:`~PIL.Image.Image.info` properties, when appropriate:
@ -527,6 +531,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**icc_profile** **icc_profile**
The ICC Profile to include in the saved file. The ICC Profile to include in the saved file.
**exif**
The exif data to include in the saved file.
.. versionadded:: 6.0.0
**bits (experimental)** **bits (experimental)**
For ``P`` images, this option controls how many bits to store. If omitted, For ``P`` images, this option controls how many bits to store. If omitted,
the PNG writer uses 8 bits (256 colors). the PNG writer uses 8 bits (256 colors).
@ -849,8 +858,8 @@ DDS
DDS is a popular container texture format used in video games and natively DDS is a popular container texture format used in video games and natively
supported by DirectX. supported by DirectX.
Currently, DXT1, DXT3, and DXT5 pixel formats are supported and only in ``RGBA`` Currently, uncompressed RGB data and DXT1, DXT3, and DXT5 pixel formats are
mode. supported, and only in ``RGBA`` mode.
.. versionadded:: 3.4.0 DXT3 .. versionadded:: 3.4.0 DXT3

View File

@ -107,10 +107,22 @@ DIB File Format
Pillow now supports reading and writing the DIB "Device Independent Bitmap" file format. Pillow now supports reading and writing the DIB "Device Independent Bitmap" file format.
Image.quantize
^^^^^^^^^^^^^^
The `dither` option is now a customisable parameter (was previously hardcoded to `1`). This parameter takes the same values used in `Image.convert`
PNG EXIF Data
^^^^^^^^^^^^^
EXIF data can now be read from and saved to PNG images. However, unlike other image
formats, EXIF data is not guaranteed to have been read until
:py:meth:`~PIL.Image.Image.load` has been called.
Other Changes Other Changes
============= =============
TODO Reading new DDS image format
^^^^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
TODO Pillow can now read uncompressed RGB data from DDS images.

View File

@ -123,43 +123,52 @@ class DdsImageFile(ImageFile.ImageFile):
# pixel format # pixel format
pfsize, pfflags = struct.unpack("<2I", header.read(8)) pfsize, pfflags = struct.unpack("<2I", header.read(8))
fourcc = header.read(4) fourcc = header.read(4)
bitcount, rmask, gmask, bmask, amask = struct.unpack("<5I", bitcount, = struct.unpack("<I", header.read(4))
header.read(20)) masks = struct.unpack("<4I", header.read(16))
if pfflags & 0x40:
# DDPF_RGB - Texture contains uncompressed RGB data
masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)}
rawmode = ""
if bitcount == 32:
rawmode += masks[0xff000000]
rawmode += masks[0xff0000] + masks[0xff00] + masks[0xff]
data_start = header_size + 4 self.tile = [("raw", (0, 0) + self.size, 0, (rawmode, 0, 1))]
n = 0
if fourcc == b"DXT1":
self.pixel_format = "DXT1"
n = 1
elif fourcc == b"DXT3":
self.pixel_format = "DXT3"
n = 2
elif fourcc == b"DXT5":
self.pixel_format = "DXT5"
n = 3
elif fourcc == b"DX10":
data_start += 20
# ignoring flags which pertain to volume textures and cubemaps
dxt10 = BytesIO(self.fp.read(20))
dxgi_format, dimension = struct.unpack("<II", dxt10.read(8))
if dxgi_format in (DXGI_FORMAT_BC7_TYPELESS,
DXGI_FORMAT_BC7_UNORM):
self.pixel_format = "BC7"
n = 7
elif dxgi_format == DXGI_FORMAT_BC7_UNORM_SRGB:
self.pixel_format = "BC7"
self.im_info["gamma"] = 1/2.2
n = 7
else:
raise NotImplementedError("Unimplemented DXGI format %d" %
(dxgi_format))
else: else:
raise NotImplementedError("Unimplemented pixel format %r" % data_start = header_size + 4
(fourcc)) n = 0
if fourcc == b"DXT1":
self.pixel_format = "DXT1"
n = 1
elif fourcc == b"DXT3":
self.pixel_format = "DXT3"
n = 2
elif fourcc == b"DXT5":
self.pixel_format = "DXT5"
n = 3
elif fourcc == b"DX10":
data_start += 20
# ignoring flags which pertain to volume textures and cubemaps
dxt10 = BytesIO(self.fp.read(20))
dxgi_format, dimension = struct.unpack("<II", dxt10.read(8))
if dxgi_format in (DXGI_FORMAT_BC7_TYPELESS,
DXGI_FORMAT_BC7_UNORM):
self.pixel_format = "BC7"
n = 7
elif dxgi_format == DXGI_FORMAT_BC7_UNORM_SRGB:
self.pixel_format = "BC7"
self.im_info["gamma"] = 1/2.2
n = 7
else:
raise NotImplementedError("Unimplemented DXGI format %d" %
(dxgi_format))
else:
raise NotImplementedError("Unimplemented pixel format %r" %
(fourcc))
self.tile = [ self.tile = [
("bcn", (0, 0) + self.size, data_start, (n)) ("bcn", (0, 0) + self.size, data_start, (n))
] ]
def load_seek(self, pos): def load_seek(self, pos):
pass pass

View File

@ -1049,7 +1049,7 @@ class Image(object):
new_im.info['transparency'] = trns new_im.info['transparency'] = trns
return new_im return new_im
def quantize(self, colors=256, method=None, kmeans=0, palette=None): def quantize(self, colors=256, method=None, kmeans=0, palette=None, dither=1):
""" """
Convert the image to 'P' mode with the specified number Convert the image to 'P' mode with the specified number
of colors. of colors.
@ -1062,6 +1062,10 @@ class Image(object):
:param kmeans: Integer :param kmeans: Integer
:param palette: Quantize to the palette of given :param palette: Quantize to the palette of given
:py:class:`PIL.Image.Image`. :py:class:`PIL.Image.Image`.
:param dither: Dithering method, used when converting from
mode "RGB" to "P" or from "RGB" or "L" to "1".
Available methods are NONE or FLOYDSTEINBERG (default).
Default: 1 (legacy setting)
:returns: A new image :returns: A new image
""" """
@ -1089,7 +1093,7 @@ class Image(object):
raise ValueError( raise ValueError(
"only RGB or L mode images can be quantized to a palette" "only RGB or L mode images can be quantized to a palette"
) )
im = self.im.convert("P", 1, palette.im) im = self.im.convert("P", dither, palette.im)
return self._new(im) return self._new(im)
return self._new(self.im.quantize(colors, method, kmeans)) return self._new(self.im.quantize(colors, method, kmeans))
@ -2002,7 +2006,7 @@ class Image(object):
library automatically seeks to frame 0. library automatically seeks to frame 0.
Note that in the current version of the library, most sequence Note that in the current version of the library, most sequence
formats only allows you to seek to the next frame. formats only allow you to seek to the next frame.
See :py:meth:`~PIL.Image.Image.tell`. See :py:meth:`~PIL.Image.Image.tell`.

View File

@ -529,6 +529,11 @@ class PngStream(ChunkStream):
return s return s
def chunk_eXIf(self, pos, length):
s = ImageFile._safe_read(self.fp, length)
self.im_info["exif"] = b"Exif\x00\x00"+s
return s
# APNG chunks # APNG chunks
def chunk_acTL(self, pos, length): def chunk_acTL(self, pos, length):
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
@ -683,6 +688,12 @@ class PngImageFile(ImageFile.ImageFile):
self.png.close() self.png.close()
self.png = None self.png = None
def _getexif(self):
if "exif" not in self.info:
self.load()
from .JpegImagePlugin import _getexif
return _getexif(self)
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# PNG writer # PNG writer
@ -861,6 +872,12 @@ def _save(im, fp, filename, chunk=putchunk):
chunks.remove(cid) chunks.remove(cid)
chunk(fp, cid, data) chunk(fp, cid, data)
exif = im.encoderinfo.get("exif", im.info.get("exif"))
if exif:
if exif.startswith(b"Exif\x00\x00"):
exif = exif[6:]
chunk(fp, b"eXIf", exif)
ImageFile._save(im, _idat(fp, chunk), ImageFile._save(im, _idat(fp, chunk),
[("zip", (0, 0)+im.size, 0, rawmode)]) [("zip", (0, 0)+im.size, 0, rawmode)])

View File

@ -819,7 +819,7 @@ class ImageFileDirectory_v2(MutableMapping):
print("- value:", values) print("- value:", values)
# count is sum of lengths for string and arbitrary data # count is sum of lengths for string and arbitrary data
if typ in [TiffTags.ASCII, TiffTags.UNDEFINED]: if typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]:
count = len(data) count = len(data)
else: else:
count = len(values) count = len(values)