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]):
rc = subprocess.call(
[IMCONVERT, f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
)
if rc:
raise OSError
return Image.open(outfile)
raise OSError()
@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,7 +61,8 @@ def test_seek_mode_2():
def test_read_n0():
# Arrange
with open(TEST_FILE) as fh:
for bytesmode in (True, False):
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
@ -69,12 +70,15 @@ def test_read_n0():
data = container.read()
# 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:
for bytesmode in (True, False):
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
@ -82,12 +86,15 @@ def test_read_n():
data = container.read(3)
# Assert
if bytesmode:
data = data.decode()
assert data == "7\nT"
def test_read_eof():
# Arrange
with open(TEST_FILE) as fh:
for bytesmode in (True, False):
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
@ -95,23 +102,29 @@ def test_read_eof():
data = container.read()
# Assert
if bytesmode:
data = data.decode()
assert data == ""
def test_readline():
# Arrange
with open(TEST_FILE) as fh:
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()
# Assert
if bytesmode:
data = data.decode()
assert data == "This is line 1\n"
def test_readlines():
# Arrange
for bytesmode in (True, False):
expected = [
"This is line 1\n",
"This is line 2\n",
@ -122,12 +135,13 @@ def test_readlines():
"This is line 7\n",
"This is line 8\n",
]
with open(TEST_FILE) as fh:
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
# Act
data = container.readlines()
# 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;