Merge branch 'master' into tiff-tags

This commit is contained in:
Hugo 2019-06-30 20:58:42 +03:00 committed by GitHub
commit f39bf365af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 336 additions and 41 deletions

View File

@ -5,6 +5,36 @@ Changelog (Pillow)
6.1.0 (unreleased)
------------------
- Update Py_UNICODE to Py_UCS4 #3780
[nulano]
- Consider I;16 pixel size when drawing #3899
[radarhere]
- Add TIFFTAG_SAMPLEFORMAT to blocklist #3926
[cgohlke, radarhere]
- Create GIF deltas from background colour of GIF frames if disposal mode is 2 #3708
[sircinnamon, radarhere]
- Added ImageSequence all_frames #3778
[radarhere]
- Use unsigned int to store TIFF IFD offsets #3923
[cgohlke]
- Include CPPFLAGS when searching for libraries #3819
[jefferyto]
- Updated TIFF tile descriptors to match current decoding functionality #3795
[dmnisson]
- Added an `image.entropy()` method (second revision) #3608
[fish2000]
- Pass the correct types to PyArg_ParseTuple #3880
[QuLogic]
- Fixed crash when loading non-font bytes #3912
[radarhere]

View File

@ -1,5 +1,5 @@
NotoNastaliqUrdu-Regular.ttf, from https://github.com/googlei18n/noto-fonts
NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 B

View File

@ -1,6 +1,6 @@
from .helper import unittest, PillowTestCase, hopper, netpbm_available
from PIL import Image, ImagePalette, GifImagePlugin
from PIL import Image, ImagePalette, GifImagePlugin, ImageDraw
from io import BytesIO
@ -59,7 +59,7 @@ class TestFileGif(PillowTestCase):
return len(test_file.getvalue())
self.assertEqual(test_grayscale(0), 800)
self.assertEqual(test_grayscale(1), 38)
self.assertEqual(test_grayscale(1), 44)
self.assertEqual(test_bilevel(0), 800)
self.assertEqual(test_bilevel(1), 800)
@ -318,6 +318,103 @@ class TestFileGif(PillowTestCase):
img.seek(img.tell() + 1)
self.assertEqual(img.disposal_method, i + 1)
def test_dispose2_palette(self):
out = self.tempfile("temp.gif")
# 4 backgrounds: White, Grey, Black, Red
circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]
im_list = []
for circle in circles:
img = Image.new("RGB", (100, 100), (255, 0, 0))
# Red circle in center of each frame
d = ImageDraw.Draw(img)
d.ellipse([(40, 40), (60, 60)], fill=circle)
im_list.append(img)
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)
img = Image.open(out)
for i, circle in enumerate(circles):
img.seek(i)
rgb_img = img.convert("RGB")
# Check top left pixel matches background
self.assertEqual(rgb_img.getpixel((0, 0)), (255, 0, 0))
# Center remains red every frame
self.assertEqual(rgb_img.getpixel((50, 50)), circle)
def test_dispose2_diff(self):
out = self.tempfile("temp.gif")
# 4 frames: red/blue, red/red, blue/blue, red/blue
circles = [
((255, 0, 0, 255), (0, 0, 255, 255)),
((255, 0, 0, 255), (255, 0, 0, 255)),
((0, 0, 255, 255), (0, 0, 255, 255)),
((255, 0, 0, 255), (0, 0, 255, 255)),
]
im_list = []
for i in range(len(circles)):
# Transparent BG
img = Image.new("RGBA", (100, 100), (255, 255, 255, 0))
# Two circles per frame
d = ImageDraw.Draw(img)
d.ellipse([(0, 30), (40, 70)], fill=circles[i][0])
d.ellipse([(60, 30), (100, 70)], fill=circles[i][1])
im_list.append(img)
im_list[0].save(
out, save_all=True, append_images=im_list[1:], disposal=2, transparency=0
)
img = Image.open(out)
for i, colours in enumerate(circles):
img.seek(i)
rgb_img = img.convert("RGBA")
# Check left circle is correct colour
self.assertEqual(rgb_img.getpixel((20, 50)), colours[0])
# Check right circle is correct colour
self.assertEqual(rgb_img.getpixel((80, 50)), colours[1])
# Check BG is correct colour
self.assertEqual(rgb_img.getpixel((1, 1)), (255, 255, 255, 0))
def test_dispose2_background(self):
out = self.tempfile("temp.gif")
im_list = []
im = Image.new("P", (100, 100))
d = ImageDraw.Draw(im)
d.rectangle([(50, 0), (100, 100)], fill="#f00")
d.rectangle([(0, 0), (50, 100)], fill="#0f0")
im_list.append(im)
im = Image.new("P", (100, 100))
d = ImageDraw.Draw(im)
d.rectangle([(0, 0), (100, 50)], fill="#f00")
d.rectangle([(0, 50), (100, 100)], fill="#0f0")
im_list.append(im)
im_list[0].save(
out, save_all=True, append_images=im_list[1:], disposal=[0, 2], background=1
)
im = Image.open(out)
im.seek(1)
self.assertEqual(im.getpixel((0, 0)), 0)
def test_iss634(self):
img = Image.open("Tests/images/iss634.gif")
# seek to the second frame

View File

@ -122,7 +122,7 @@ class TestFileLibTiff(LibTiffTestCase):
self.assertEqual(im.mode, "RGB")
self.assertEqual(im.size, (278, 374))
self.assertEqual(im.tile[0][:3], ("tiff_adobe_deflate", (0, 0, 278, 374), 0))
self.assertEqual(im.tile[0][:3], ("libtiff", (0, 0, 278, 374), 0))
im.load()
self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
@ -684,10 +684,10 @@ class TestFileLibTiff(LibTiffTestCase):
im.tile,
[
(
"tiff_adobe_deflate",
"libtiff",
(0, 0, 100, 40),
0,
("RGB;16N", "tiff_adobe_deflate", False),
("RGB;16N", "tiff_adobe_deflate", False, 8),
)
],
)
@ -701,7 +701,8 @@ class TestFileLibTiff(LibTiffTestCase):
self.assertEqual(im.mode, "RGBA")
self.assertEqual(im.size, (100, 40))
self.assertEqual(
im.tile, [("tiff_lzw", (0, 0, 100, 40), 0, ("RGBa;16N", "tiff_lzw", False))]
im.tile,
[("libtiff", (0, 0, 100, 40), 0, ("RGBa;16N", "tiff_lzw", False, 38236))],
)
im.load()
@ -720,7 +721,7 @@ class TestFileLibTiff(LibTiffTestCase):
self.assertEqual(im.mode, "RGB")
self.assertEqual(im.size, (256, 256))
self.assertEqual(
im.tile, [("jpeg", (0, 0, 256, 256), 0, ("RGB", "jpeg", False))]
im.tile, [("libtiff", (0, 0, 256, 256), 0, ("RGB", "jpeg", False, 5122))]
)
im.load()

View File

@ -4,7 +4,7 @@ import sys
from .helper import unittest, PillowTestCase, hopper
from PIL import Image, TiffImagePlugin
from PIL import Image, TiffImagePlugin, features
from PIL._util import py3
from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION, RESOLUTION_UNIT
@ -587,6 +587,30 @@ class TestFileTiff(PillowTestCase):
im.load()
self.assertFalse(fp.closed)
@unittest.skipUnless(features.check("libtiff"), "libtiff not installed")
def test_sampleformat_not_corrupted(self):
# Assert that a TIFF image with SampleFormat=UINT tag is not corrupted
# when saving to a new file.
# Pillow 6.0 fails with "OSError: cannot identify image file".
import base64
tiff = BytesIO(
base64.b64decode(
b"SUkqAAgAAAAPAP4ABAABAAAAAAAAAAABBAABAAAAAQAAAAEBBAABAAAAAQAA"
b"AAIBAwADAAAAwgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAA"
b"4AAAABUBAwABAAAAAwAAABYBBAABAAAAAQAAABcBBAABAAAACwAAABoBBQAB"
b"AAAAyAAAABsBBQABAAAA0AAAABwBAwABAAAAAQAAACgBAwABAAAAAQAAAFMB"
b"AwADAAAA2AAAAAAAAAAIAAgACAABAAAAAQAAAAEAAAABAAAAAQABAAEAAAB4"
b"nGNgYAAAAAMAAQ=="
)
)
out = BytesIO()
with Image.open(tiff) as im:
im.save(out, format="tiff")
out.seek(0)
with Image.open(out) as im:
im.load()
@unittest.skipUnless(sys.platform.startswith("win32"), "Windows only")
class TestFileTiffW32(PillowTestCase):

View File

@ -479,6 +479,19 @@ class TestImageDraw(PillowTestCase):
# Assert
self.assert_image_equal(im, Image.open(expected))
def test_rectangle_I16(self):
# Arrange
im = Image.new("I;16", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.rectangle(BBOX1, fill="black", outline="green")
# Assert
self.assert_image_equal(
im.convert("I"), Image.open("Tests/images/imagedraw_rectangle_I.png")
)
def test_floodfill(self):
red = ImageColor.getrgb("red")

View File

@ -463,6 +463,26 @@ class TestImageFont(PillowTestCase):
with self.assertRaises(UnicodeEncodeError):
font.getsize(u"")
@unittest.skipIf(
sys.platform.startswith("win32") and sys.version.startswith("2"),
"requires Python 3.x on Windows",
)
def test_unicode_extended(self):
# issue #3777
text = u"A\u278A\U0001F12B"
target = "Tests/images/unicode_extended.png"
ttf = ImageFont.truetype(
"Tests/fonts/NotoSansSymbols-Regular.ttf",
FONT_SIZE,
layout_engine=self.LAYOUT_ENGINE,
)
img = Image.new("RGB", (100, 60))
d = ImageDraw.Draw(img)
d.text((10, 10), text, font=ttf)
self.assert_image_similar_tofile(img, target, self.metrics["multiline"])
def _test_fake_loading_font(self, path_to_fake, fontname):
# Make a copy of FreeTypeFont so we can patch the original
free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)

View File

@ -74,3 +74,25 @@ class TestImageSequence(PillowTestCase):
im.seek(0)
color2 = im.getpalette()[0:3]
self.assertEqual(color1, color2)
def test_all_frames(self):
# Test a single image
im = Image.open("Tests/images/iss634.gif")
ims = ImageSequence.all_frames(im)
self.assertEqual(len(ims), 42)
for i, im_frame in enumerate(ims):
self.assertFalse(im_frame is im)
im.seek(i)
self.assert_image_equal(im, im_frame)
# Test a series of images
ims = ImageSequence.all_frames([im, hopper(), im])
self.assertEqual(len(ims), 85)
# Test an operation
ims = ImageSequence.all_frames(im, lambda im_frame: im_frame.rotate(90))
for i, im_frame in enumerate(ims):
im.seek(i)
self.assert_image_equal(im.rotate(90), im_frame)

View File

@ -11,6 +11,14 @@ An optional ``include_layered_windows`` parameter has been added to ``ImageGrab.
defaulting to ``False``. If true, layered windows will be included in the resulting
image on Windows.
ImageSequence.all_frames
^^^^^^^^^^^^^^^^^^^^^^^^
A new method to facilitate applying a given function to all frames in an image, or to
all frames in a list of images. The frames are returned as a list of separate images.
For example, ``ImageSequence.all_frames(im, lambda im_frame: im_frame.rotate(90))``
could be used to return all frames from an image, each rotated 90 degrees.
Variation fonts
^^^^^^^^^^^^^^^

View File

@ -386,8 +386,8 @@ class pil_build_ext(build_ext):
_add_directory(library_dirs, lib_root)
_add_directory(include_dirs, include_root)
# respect CFLAGS/LDFLAGS
for k in ("CFLAGS", "LDFLAGS"):
# respect CFLAGS/CPPFLAGS/LDFLAGS
for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"):
if k in os.environ:
for match in re.finditer(r"-I([^\s]+)", os.environ[k]):
_add_directory(include_dirs, match.group(1))

View File

@ -426,6 +426,7 @@ def _write_multiple_frames(im, fp, palette):
im_frames = []
frame_count = 0
background_im = None
for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
for im_frame in ImageSequence.Iterator(imSequence):
# a copy is required here since seek can still mutate the image
@ -445,11 +446,22 @@ def _write_multiple_frames(im, fp, palette):
if im_frames:
# delta frame
previous = im_frames[-1]
if _get_palette_bytes(im_frame) == _get_palette_bytes(previous["im"]):
delta = ImageChops.subtract_modulo(im_frame, previous["im"])
if encoderinfo.get("disposal") == 2:
if background_im is None:
background = _get_background(
im,
im.encoderinfo.get("background", im.info.get("background")),
)
background_im = Image.new("P", im_frame.size, background)
background_im.putpalette(im_frames[0]["im"].palette)
base_im = background_im
else:
base_im = previous["im"]
if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im):
delta = ImageChops.subtract_modulo(im_frame, base_im)
else:
delta = ImageChops.subtract_modulo(
im_frame.convert("RGB"), previous["im"].convert("RGB")
im_frame.convert("RGB"), base_im.convert("RGB")
)
bbox = delta.getbbox()
if not bbox:
@ -683,10 +695,12 @@ def _get_color_table_size(palette_bytes):
# calculate the palette size for the header
import math
color_table_size = int(math.ceil(math.log(len(palette_bytes) // 3, 2))) - 1
if color_table_size < 0:
color_table_size = 0
return color_table_size
if not palette_bytes:
return 0
elif len(palette_bytes) < 9:
return 1
else:
return int(math.ceil(math.log(len(palette_bytes) // 3, 2))) - 1
def _get_header_palette(palette_bytes):
@ -717,6 +731,18 @@ def _get_palette_bytes(im):
return im.palette.palette
def _get_background(im, infoBackground):
background = 0
if infoBackground:
background = infoBackground
if isinstance(background, tuple):
# WebPImagePlugin stores an RGBA value in info["background"]
# So it must be converted to the same format as GifImagePlugin's
# info["background"] - a global color table index
background = im.palette.getcolor(background)
return background
def _get_global_header(im, info):
"""Return a list of strings representing a GIF header"""
@ -736,14 +762,7 @@ def _get_global_header(im, info):
if im.info.get("version") == b"89a":
version = b"89a"
background = 0
if "background" in info:
background = info["background"]
if isinstance(background, tuple):
# WebPImagePlugin stores an RGBA value in info["background"]
# So it must be converted to the same format as GifImagePlugin's
# info["background"] - a global color table index
background = im.palette.getcolor(background)
background = _get_background(im, info.get("background"))
palette_bytes = _get_palette_bytes(im)
color_table_size = _get_color_table_size(palette_bytes)

View File

@ -54,3 +54,25 @@ class Iterator(object):
def next(self):
return self.__next__()
def all_frames(im, func=None):
"""
Applies a given function to all frames in an image or a list of images.
The frames are returned as a list of separate images.
:param im: An image, or a list of images.
:param func: The function to apply to all of the image frames.
:returns: A list of images.
"""
if not isinstance(im, list):
im = [im]
ims = []
for imSequence in im:
current = imSequence.tell()
ims += [im_frame.copy() for im_frame in Iterator(imSequence)]
imSequence.seek(current)
return [func(im) for im in ims] if func else ims

View File

@ -1123,7 +1123,7 @@ class TiffImageFile(ImageFile.ImageFile):
# (self._compression, (extents tuple),
# 0, (rawmode, self._compression, fp))
extents = self.tile[0][1]
args = list(self.tile[0][3]) + [self.tag_v2.offset]
args = list(self.tile[0][3])
# To be nice on memory footprint, if there's a
# file descriptor, use that instead of reading
@ -1330,8 +1330,8 @@ class TiffImageFile(ImageFile.ImageFile):
# Offset in the tile tuple is 0, we go from 0,0 to
# w,h, and we only do this once -- eds
a = (rawmode, self._compression, False)
self.tile.append((self._compression, (0, 0, xsize, ysize), 0, a))
a = (rawmode, self._compression, False, self.tag_v2.offset)
self.tile.append(("libtiff", (0, 0, xsize, ysize), 0, a))
elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2:
# striped image
@ -1542,6 +1542,8 @@ def _save(im, fp, filename):
# optional types for non core tags
types = {}
# SAMPLEFORMAT is determined by the image format and should not be copied
# from legacy_ifd.
# STRIPOFFSETS and STRIPBYTECOUNTS are added by the library
# based on the data in the strip.
# The other tags expect arrays with a certain length (fixed or depending on
@ -1550,6 +1552,7 @@ def _save(im, fp, filename):
blocklist = [
COLORMAP,
REFERENCEBLACKWHITE,
SAMPLEFORMAT,
STRIPBYTECOUNTS,
STRIPOFFSETS,
TRANSFERFUNCTION,

View File

@ -327,6 +327,7 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw)
static int
font_getchar(PyObject* string, int index, FT_ULong* char_out)
{
#if PY_VERSION_HEX < 0x03000000
if (PyUnicode_Check(string)) {
Py_UNICODE* p = PyUnicode_AS_UNICODE(string);
int size = PyUnicode_GET_SIZE(string);
@ -336,7 +337,6 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out)
return 1;
}
#if PY_VERSION_HEX < 0x03000000
if (PyString_Check(string)) {
unsigned char* p = (unsigned char*) PyString_AS_STRING(string);
int size = PyString_GET_SIZE(string);
@ -345,6 +345,13 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out)
*char_out = (unsigned char) p[index];
return 1;
}
#else
if (PyUnicode_Check(string)) {
if (index >= PyUnicode_GET_LENGTH(string))
return 0;
*char_out = PyUnicode_READ_CHAR(string, index);
return 1;
}
#endif
return 0;
@ -366,6 +373,7 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject *
goto failed;
}
#if PY_VERSION_HEX < 0x03000000
if (PyUnicode_Check(string)) {
Py_UNICODE *text = PyUnicode_AS_UNICODE(string);
Py_ssize_t size = PyUnicode_GET_SIZE(string);
@ -385,9 +393,7 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject *
}
}
}
#if PY_VERSION_HEX < 0x03000000
else if (PyString_Check(string)) {
} else if (PyString_Check(string)) {
char *text = PyString_AS_STRING(string);
int size = PyString_GET_SIZE(string);
if (! size) {
@ -404,6 +410,28 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject *
}
}
}
#else
if (PyUnicode_Check(string)) {
Py_UCS4 *text = PyUnicode_AsUCS4Copy(string);
Py_ssize_t size = PyUnicode_GET_LENGTH(string);
if (!text || !size) {
/* return 0 and clean up, no glyphs==no size,
and raqm fails with empty strings */
goto failed;
}
int set_text = (*p_raqm.set_text)(rq, (const uint32_t *)(text), size);
PyMem_Free(text);
if (!set_text) {
PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed");
goto failed;
}
if (lang) {
if (!(*p_raqm.set_language)(rq, lang, start, size)) {
PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed");
goto failed;
}
}
}
#endif
else {
PyErr_SetString(PyExc_TypeError, "expected string");

View File

@ -503,9 +503,9 @@ PyImaging_LibTiffDecoderNew(PyObject* self, PyObject* args)
char* rawmode;
char* compname;
int fp;
int ifdoffset;
uint32 ifdoffset;
if (! PyArg_ParseTuple(args, "sssii", &mode, &rawmode, &compname, &fp, &ifdoffset))
if (! PyArg_ParseTuple(args, "sssiI", &mode, &rawmode, &compname, &fp, &ifdoffset))
return NULL;
TRACE(("new tiff decoder %s\n", compname));

View File

@ -68,7 +68,12 @@ static inline void
point8(Imaging im, int x, int y, int ink)
{
if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize)
if (strncmp(im->mode, "I;16", 4) == 0) {
im->image8[y][x*2] = (UINT8) ink;
im->image8[y][x*2+1] = (UINT8) ink;
} else {
im->image8[y][x] = (UINT8) ink;
}
}
static inline void
@ -95,7 +100,7 @@ point32rgba(Imaging im, int x, int y, int ink)
static inline void
hline8(Imaging im, int x0, int y0, int x1, int ink)
{
int tmp;
int tmp, pixelwidth;
if (y0 >= 0 && y0 < im->ysize) {
if (x0 > x1)
@ -108,8 +113,11 @@ hline8(Imaging im, int x0, int y0, int x1, int ink)
return;
else if (x1 >= im->xsize)
x1 = im->xsize-1;
if (x0 <= x1)
memset(im->image8[y0] + x0, (UINT8) ink, x1 - x0 + 1);
if (x0 <= x1) {
pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1;
memset(im->image8[y0] + x0 * pixelwidth, (UINT8) ink,
(x1 - x0 + 1) * pixelwidth);
}
}
}

View File

@ -147,7 +147,7 @@ void _tiffUnmapProc(thandle_t hdata, tdata_t base, toff_t size) {
(void) hdata; (void) base; (void) size;
}
int ImagingLibTiffInit(ImagingCodecState state, int fp, int offset) {
int ImagingLibTiffInit(ImagingCodecState state, int fp, uint32 offset) {
TIFFSTATE *clientstate = (TIFFSTATE *)state->context;
TRACE(("initing libtiff\n"));

View File

@ -43,7 +43,7 @@ typedef struct {
extern int ImagingLibTiffInit(ImagingCodecState state, int fp, int offset);
extern int ImagingLibTiffInit(ImagingCodecState state, int fp, uint32 offset);
extern int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp);
extern int ImagingLibTiffMergeFieldInfo(ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length);
extern int ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...);