mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-11-11 20:27:06 +03:00
Merge branch 'master' of https://github.com/python-pillow/Pillow
This commit is contained in:
commit
c57bfb9a7b
12
CHANGES.rst
12
CHANGES.rst
|
@ -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
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
BIN
Tests/images/uncompressed_rgb.dds
Executable file
Binary file not shown.
BIN
Tests/images/uncompressed_rgb.png
Normal file
BIN
Tests/images/uncompressed_rgb.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 143 KiB |
BIN
Tests/images/unimplemented_dxgi_format.dds
Normal file
BIN
Tests/images/unimplemented_dxgi_format.dds
Normal file
Binary file not shown.
BIN
Tests/images/unimplemented_pixel_format.dds
Executable file
BIN
Tests/images/unimplemented_pixel_format.dds
Executable file
Binary file not shown.
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
def check_tags(tiffinfo):
|
||||||
im = hopper()
|
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():
|
||||||
|
reloaded_value = reloaded.tag_v2[tag]
|
||||||
|
if isinstance(reloaded_value, TiffImagePlugin.IFDRational):
|
||||||
|
reloaded_value = float(reloaded_value)
|
||||||
|
|
||||||
if libtiff and isinstance(value, bytes):
|
if libtiff and isinstance(value, bytes):
|
||||||
value = value.decode()
|
value = value.decode()
|
||||||
self.assertEqual(reloaded.tag_v2[tag], value)
|
|
||||||
|
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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -123,9 +123,18 @@ 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]
|
||||||
|
|
||||||
|
self.tile = [("raw", (0, 0) + self.size, 0, (rawmode, 0, 1))]
|
||||||
|
else:
|
||||||
data_start = header_size + 4
|
data_start = header_size + 4
|
||||||
n = 0
|
n = 0
|
||||||
if fourcc == b"DXT1":
|
if fourcc == b"DXT1":
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
||||||
|
|
|
@ -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)])
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user