Merge remote-tracking branch 'upstream/master' into pytest

This commit is contained in:
Hugo 2020-02-15 16:56:21 +02:00
commit 9087599e60
17 changed files with 216 additions and 103 deletions

View File

@ -5,6 +5,9 @@ Changelog (Pillow)
7.1.0 (unreleased)
------------------
- Only draw each polygon pixel once #4333
[radarhere]
- Add support for shooting situation Exif IFD tags #4398
[alexagv]

View File

@ -61,15 +61,19 @@ To report a security vulnerability, please follow the procedure described in the
:alt: AppVeyor CI build status (Windows)
.. |gha_lint| image:: https://github.com/python-pillow/Pillow/workflows/Lint/badge.svg
:target: https://github.com/python-pillow/Pillow/actions?query=workflow%3ALint
:alt: GitHub Actions build status (Lint)
.. |gha_docker| image:: https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg
:target: https://github.com/python-pillow/Pillow/actions?query=workflow%3A%22Test+Docker%22
:alt: GitHub Actions build status (Test Docker)
.. |gha| image:: https://github.com/python-pillow/Pillow/workflows/Test/badge.svg
:target: https://github.com/python-pillow/Pillow/actions?query=workflow%3ATest
:alt: GitHub Actions build status (Test Linux and macOS)
.. |gha_windows| image:: https://github.com/python-pillow/Pillow/workflows/Test%20Windows/badge.svg
:target: https://github.com/python-pillow/Pillow/actions?query=workflow%3A%22Test+Windows%22
:alt: GitHub Actions build status (Test Windows)
.. |coverage| image:: https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg

View File

@ -2,7 +2,7 @@
## Main Release
Released quarterly on the first day of January, April, July, October.
Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
* [ ] Develop and prepare release in `master` branch.

View File

@ -4,6 +4,7 @@ Helper functions.
import logging
import os
import shutil
import subprocess
import sys
import tempfile
@ -191,9 +192,12 @@ class PillowTestCase(unittest.TestCase):
raise OSError()
outfile = self.tempfile("temp.png")
if command_succeeds([IMCONVERT, f, outfile]):
return Image.open(outfile)
raise OSError()
rc = subprocess.call(
[IMCONVERT, f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
)
if rc:
raise OSError
return Image.open(outfile)
@unittest.skipIf(sys.platform.startswith("win32"), "requires Unix or macOS")
@ -268,34 +272,20 @@ def hopper(mode=None, cache={}):
return im.copy()
def command_succeeds(cmd):
"""
Runs the command, which must be a list of strings. Returns True if the
command succeeds, or False if an OSError was raised by subprocess.Popen.
"""
try:
subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
except OSError:
return False
return True
def djpeg_available():
return command_succeeds(["djpeg", "-version"])
return bool(shutil.which("djpeg"))
def cjpeg_available():
return command_succeeds(["cjpeg", "-version"])
return bool(shutil.which("cjpeg"))
def netpbm_available():
return command_succeeds(["ppmquant", "--version"]) and command_succeeds(
["ppmtogif", "--version"]
)
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))
def imagemagick_available():
return IMCONVERT and command_succeeds([IMCONVERT, "-version"])
return bool(IMCONVERT and shutil.which(IMCONVERT))
def on_appveyor():

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

View File

@ -20,7 +20,7 @@ def test_isatty():
def test_seek_mode_0():
# Arrange
mode = 0
with open(TEST_FILE) as fh:
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
@ -34,7 +34,7 @@ def test_seek_mode_0():
def test_seek_mode_1():
# Arrange
mode = 1
with open(TEST_FILE) as fh:
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
@ -48,7 +48,7 @@ def test_seek_mode_1():
def test_seek_mode_2():
# Arrange
mode = 2
with open(TEST_FILE) as fh:
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
@ -61,73 +61,87 @@ def test_seek_mode_2():
def test_read_n0():
# Arrange
with open(TEST_FILE) as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
for bytesmode in (True, False):
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
container.seek(81)
data = container.read()
# Act
container.seek(81)
data = container.read()
# Assert
assert data == "7\nThis is line 8\n"
# Assert
if bytesmode:
data = data.decode()
assert data == "7\nThis is line 8\n"
def test_read_n():
# Arrange
with open(TEST_FILE) as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
for bytesmode in (True, False):
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
container.seek(81)
data = container.read(3)
# Act
container.seek(81)
data = container.read(3)
# Assert
assert data == "7\nT"
# Assert
if bytesmode:
data = data.decode()
assert data == "7\nT"
def test_read_eof():
# Arrange
with open(TEST_FILE) as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
for bytesmode in (True, False):
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
container.seek(100)
data = container.read()
# Act
container.seek(100)
data = container.read()
# Assert
assert data == ""
# Assert
if bytesmode:
data = data.decode()
assert data == ""
def test_readline():
# Arrange
with open(TEST_FILE) as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
for bytesmode in (True, False):
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
# Act
data = container.readline()
# Act
data = container.readline()
# Assert
assert data == "This is line 1\n"
# Assert
if bytesmode:
data = data.decode()
assert data == "This is line 1\n"
def test_readlines():
# Arrange
expected = [
"This is line 1\n",
"This is line 2\n",
"This is line 3\n",
"This is line 4\n",
"This is line 5\n",
"This is line 6\n",
"This is line 7\n",
"This is line 8\n",
]
with open(TEST_FILE) as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
for bytesmode in (True, False):
expected = [
"This is line 1\n",
"This is line 2\n",
"This is line 3\n",
"This is line 4\n",
"This is line 5\n",
"This is line 6\n",
"This is line 7\n",
"This is line 8\n",
]
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
# Act
data = container.readlines()
# Act
data = container.readlines()
# Assert
assert data == expected
# Assert
if bytesmode:
data = [line.decode() for line in data]
assert data == expected

View File

@ -228,6 +228,19 @@ def test_chord_width_fill():
assert_image_similar(im, Image.open(expected), 1)
def test_chord_zero_width():
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=0)
# Assert
with Image.open("Tests/images/imagedraw_chord_zero_width.png") as expected:
assert_image_equal(im, expected)
def helper_ellipse(mode, bbox):
# Arrange
im = Image.new(mode, (W, H))
@ -310,6 +323,19 @@ def test_ellipse_width_fill():
assert_image_similar(im, Image.open(expected), 1)
def test_ellipse_zero_width():
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.ellipse(BBOX1, fill="green", outline="blue", width=0)
# Assert
with Image.open("Tests/images/imagedraw_ellipse_zero_width.png") as expected:
assert_image_equal(im, expected)
def helper_line(points):
# Arrange
im = Image.new("RGB", (W, H))
@ -420,6 +446,19 @@ def test_pieslice_width_fill():
assert_image_similar(im, Image.open(expected), 1)
def test_pieslice_zero_width():
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=0)
# Assert
with Image.open("Tests/images/imagedraw_pieslice_zero_width.png") as expected:
assert_image_equal(im, expected)
def helper_point(points):
# Arrange
im = Image.new("RGB", (W, H))
@ -537,6 +576,19 @@ def test_rectangle_width_fill():
assert_image_equal(im, Image.open(expected))
def test_rectangle_zero_width():
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.rectangle(BBOX1, fill="blue", outline="green", width=0)
# Assert
with Image.open("Tests/images/imagedraw_rectangle_zero_width.png") as expected:
assert_image_equal(im, expected)
def test_rectangle_I16():
# Arrange
im = Image.new("I;16", (W, H))

View File

@ -154,7 +154,7 @@ Methods
To paste pixel data into an image, use the
:py:meth:`~PIL.Image.Image.paste` method on the image itself.
.. py:method:: PIL.ImageDraw.ImageDraw.chord(xy, start, end, fill=None, outline=None, width=0)
.. py:method:: PIL.ImageDraw.ImageDraw.chord(xy, start, end, fill=None, outline=None, width=1)
Same as :py:meth:`~PIL.ImageDraw.ImageDraw.arc`, but connects the end points
with a straight line.
@ -168,7 +168,7 @@ Methods
.. versionadded:: 5.3.0
.. py:method:: PIL.ImageDraw.ImageDraw.ellipse(xy, fill=None, outline=None, width=0)
.. py:method:: PIL.ImageDraw.ImageDraw.ellipse(xy, fill=None, outline=None, width=1)
Draws an ellipse inside the given bounding box.
@ -198,7 +198,7 @@ Methods
.. versionadded:: 5.3.0
.. py:method:: PIL.ImageDraw.ImageDraw.pieslice(xy, start, end, fill=None, outline=None, width=0)
.. py:method:: PIL.ImageDraw.ImageDraw.pieslice(xy, start, end, fill=None, outline=None, width=1)
Same as arc, but also draws straight lines between the end points and the
center of the bounding box.
@ -236,7 +236,7 @@ Methods
:param outline: Color to use for the outline.
:param fill: Color to use for the fill.
.. py:method:: PIL.ImageDraw.ImageDraw.rectangle(xy, fill=None, outline=None, width=0)
.. py:method:: PIL.ImageDraw.ImageDraw.rectangle(xy, fill=None, outline=None, width=1)
Draws a rectangle.

View File

@ -82,7 +82,7 @@ class ContainerIO:
else:
n = self.length - self.pos
if not n: # EOF
return ""
return b"" if "b" in self.fh.mode else ""
self.pos = self.pos + n
return self.fh.read(n)
@ -92,13 +92,14 @@ class ContainerIO:
:returns: An 8-bit string.
"""
s = ""
s = b"" if "b" in self.fh.mode else ""
newline_character = b"\n" if "b" in self.fh.mode else "\n"
while True:
c = self.read(1)
if not c:
break
s = s + c
if c == "\n":
if c == newline_character:
break
return s

View File

@ -134,20 +134,20 @@ class ImageDraw:
if ink is not None:
self.draw.draw_bitmap(xy, bitmap.im, ink)
def chord(self, xy, start, end, fill=None, outline=None, width=0):
def chord(self, xy, start, end, fill=None, outline=None, width=1):
"""Draw a chord."""
ink, fill = self._getink(outline, fill)
if fill is not None:
self.draw.draw_chord(xy, start, end, fill, 1)
if ink is not None and ink != fill:
if ink is not None and ink != fill and width != 0:
self.draw.draw_chord(xy, start, end, ink, 0, width)
def ellipse(self, xy, fill=None, outline=None, width=0):
def ellipse(self, xy, fill=None, outline=None, width=1):
"""Draw an ellipse."""
ink, fill = self._getink(outline, fill)
if fill is not None:
self.draw.draw_ellipse(xy, fill, 1)
if ink is not None and ink != fill:
if ink is not None and ink != fill and width != 0:
self.draw.draw_ellipse(xy, ink, 0, width)
def line(self, xy, fill=None, width=0, joint=None):
@ -219,12 +219,12 @@ class ImageDraw:
if ink is not None and ink != fill:
self.draw.draw_outline(shape, ink, 0)
def pieslice(self, xy, start, end, fill=None, outline=None, width=0):
def pieslice(self, xy, start, end, fill=None, outline=None, width=1):
"""Draw a pieslice."""
ink, fill = self._getink(outline, fill)
if fill is not None:
self.draw.draw_pieslice(xy, start, end, fill, 1)
if ink is not None and ink != fill:
if ink is not None and ink != fill and width != 0:
self.draw.draw_pieslice(xy, start, end, ink, 0, width)
def point(self, xy, fill=None):
@ -241,12 +241,12 @@ class ImageDraw:
if ink is not None and ink != fill:
self.draw.draw_polygon(xy, ink, 0)
def rectangle(self, xy, fill=None, outline=None, width=0):
def rectangle(self, xy, fill=None, outline=None, width=1):
"""Draw a rectangle."""
ink, fill = self._getink(outline, fill)
if fill is not None:
self.draw.draw_rectangle(xy, fill, 1)
if ink is not None and ink != fill:
if ink is not None and ink != fill and width != 0:
self.draw.draw_rectangle(xy, ink, 0, width)
def _multiline_check(self, text):

View File

@ -415,6 +415,35 @@ x_cmp(const void *x0, const void *x1)
}
static void
draw_horizontal_lines(Imaging im, int n, Edge *e, int ink, int *x_pos, int y, hline_handler hline)
{
int i;
for (i = 0; i < n; i++) {
if (e[i].ymin == y && e[i].ymin == e[i].ymax) {
int xmax;
int xmin = e[i].xmin;
if (*x_pos < xmin) {
// Line would be after the current position
continue;
}
xmax = e[i].xmax;
if (*x_pos > xmin) {
// Line would be partway through x_pos, so increase the starting point
xmin = *x_pos;
if (xmax < xmin) {
// Line would now end before it started
continue;
}
}
(*hline)(im, xmin, e[i].ymin, xmax, ink);
*x_pos = xmax+1;
}
}
}
/*
* Filled polygon draw function using scan line algorithm.
*/
@ -442,10 +471,7 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill,
}
for (i = 0; i < n; i++) {
/* This causes the pixels of horizontal edges to be drawn twice :(
* but without it there are inconsistencies in ellipses */
if (e[i].ymin == e[i].ymax) {
(*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink);
continue;
}
if (ymin > e[i].ymin) {
@ -472,6 +498,7 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill,
}
for (; ymin <= ymax; ymin++) {
int j = 0;
int x_pos = 0;
for (i = 0; i < edge_count; i++) {
Edge* current = edge_table[i];
if (ymin >= current->ymin && ymin <= current->ymax) {
@ -485,8 +512,30 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill,
}
qsort(xx, j, sizeof(float), x_cmp);
for (i = 1; i < j; i += 2) {
(*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink);
int x_end = ROUND_DOWN(xx[i]);
if (x_end < x_pos) {
// Line would be before the current position
continue;
}
draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline);
if (x_end < x_pos) {
// Line would be before the current position
continue;
}
int x_start = ROUND_UP(xx[i-1]);
if (x_pos > x_start) {
// Line would be partway through x_pos, so increase the starting point
x_start = x_pos;
if (x_end < x_start) {
// Line would now end before it started
continue;
}
}
(*hline)(im, x_start, ymin, x_end, ink);
x_pos = x_end+1;
}
draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline);
}
free(xx);

View File

@ -26,8 +26,8 @@
struct _Heap {
void **heap;
int heapsize;
int heapcount;
unsigned int heapsize;
unsigned int heapcount;
HeapCmpFunc cf;
};
@ -44,7 +44,7 @@ void ImagingQuantHeapFree(Heap *h) {
free(h);
}
static int _heap_grow(Heap *h,int newsize) {
static int _heap_grow(Heap *h,unsigned int newsize) {
void *newheap;
if (!newsize) newsize=h->heapsize<<1;
if (newsize<h->heapsize) return 0;
@ -64,7 +64,7 @@ static int _heap_grow(Heap *h,int newsize) {
#ifdef DEBUG
static int _heap_test(Heap *h) {
int k;
unsigned int k;
for (k=1;k*2<=h->heapcount;k++) {
if (h->cf(h,h->heap[k],h->heap[k*2])<0) {
printf ("heap is bad\n");
@ -80,7 +80,7 @@ static int _heap_test(Heap *h) {
#endif
int ImagingQuantHeapRemove(Heap* h,void **r) {
int k,l;
unsigned int k,l;
void *v;
if (!h->heapcount) {

View File

@ -44,7 +44,7 @@ typedef struct _ColorCube{
unsigned int rWidth, gWidth, bWidth, aWidth;
unsigned int rOffset, gOffset, bOffset, aOffset;
long size;
unsigned long size;
ColorBucket buckets;
} *ColorCube;
@ -134,10 +134,10 @@ add_color_to_color_cube(const ColorCube cube, const Pixel *p) {
bucket->a += p->c.a;
}
static long
static unsigned long
count_used_color_buckets(const ColorCube cube) {
long usedBuckets = 0;
long i;
unsigned long usedBuckets = 0;
unsigned long i;
for (i=0; i < cube->size; i++) {
if (cube->buckets[i].count > 0) {
usedBuckets += 1;
@ -194,7 +194,7 @@ void add_bucket_values(ColorBucket src, ColorBucket dst) {
/* expand or shrink a given cube to level */
static ColorCube copy_color_cube(const ColorCube cube,
int rBits, int gBits, int bBits, int aBits)
unsigned int rBits, unsigned int gBits, unsigned int bBits, unsigned int aBits)
{
unsigned int r, g, b, a;
long src_pos, dst_pos;
@ -302,7 +302,7 @@ void add_lookup_buckets(ColorCube cube, ColorBucket palette, long nColors, long
}
ColorBucket
combined_palette(ColorBucket bucketsA, long nBucketsA, ColorBucket bucketsB, long nBucketsB) {
combined_palette(ColorBucket bucketsA, unsigned long nBucketsA, ColorBucket bucketsB, unsigned long nBucketsB) {
ColorBucket result;
if (nBucketsA > LONG_MAX - nBucketsB ||
(nBucketsA+nBucketsB) > LONG_MAX / sizeof(struct _ColorBucket)) {
@ -345,8 +345,8 @@ map_image_pixels(const Pixel *pixelData,
}
}
const int CUBE_LEVELS[8] = {4, 4, 4, 0, 2, 2, 2, 0};
const int CUBE_LEVELS_ALPHA[8] = {3, 4, 3, 3, 2, 2, 2, 2};
const unsigned int CUBE_LEVELS[8] = {4, 4, 4, 0, 2, 2, 2, 0};
const unsigned int CUBE_LEVELS_ALPHA[8] = {3, 4, 3, 3, 2, 2, 2, 2};
int quantize_octree(Pixel *pixelData,
uint32_t nPixels,
@ -365,8 +365,8 @@ int quantize_octree(Pixel *pixelData,
ColorBucket paletteBuckets = NULL;
uint32_t *qp = NULL;
long i;
long nCoarseColors, nFineColors, nAlreadySubtracted;
const int *cubeBits;
unsigned long nCoarseColors, nFineColors, nAlreadySubtracted;
const unsigned int *cubeBits;
if (withAlpha) {
cubeBits = CUBE_LEVELS_ALPHA;