diff --git a/CHANGES.rst b/CHANGES.rst index 8b48ac1a8..c17b5fdb3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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] diff --git a/README.rst b/README.rst index 253dc89a5..f02268df5 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/RELEASING.md b/RELEASING.md index 9614b133f..3a285662c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -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. diff --git a/Tests/helper.py b/Tests/helper.py index e4b1d917c..3b2341012 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -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(): diff --git a/Tests/images/imagedraw_chord_zero_width.png b/Tests/images/imagedraw_chord_zero_width.png new file mode 100644 index 000000000..c1c0058d7 Binary files /dev/null and b/Tests/images/imagedraw_chord_zero_width.png differ diff --git a/Tests/images/imagedraw_ellipse_translucent.png b/Tests/images/imagedraw_ellipse_translucent.png new file mode 100644 index 000000000..964dce678 Binary files /dev/null and b/Tests/images/imagedraw_ellipse_translucent.png differ diff --git a/Tests/images/imagedraw_ellipse_zero_width.png b/Tests/images/imagedraw_ellipse_zero_width.png new file mode 100644 index 000000000..f14a279f2 Binary files /dev/null and b/Tests/images/imagedraw_ellipse_zero_width.png differ diff --git a/Tests/images/imagedraw_pieslice_zero_width.png b/Tests/images/imagedraw_pieslice_zero_width.png new file mode 100644 index 000000000..4ca051583 Binary files /dev/null and b/Tests/images/imagedraw_pieslice_zero_width.png differ diff --git a/Tests/images/imagedraw_rectangle_zero_width.png b/Tests/images/imagedraw_rectangle_zero_width.png new file mode 100644 index 000000000..989c95761 Binary files /dev/null and b/Tests/images/imagedraw_rectangle_zero_width.png differ diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 91166b39e..b752e217f 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -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 diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index e5951fb02..bd8c06560 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -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)) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index ad0fe4abe..6700deab4 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -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. diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py index 9727601ab..5bb0086f6 100644 --- a/src/PIL/ContainerIO.py +++ b/src/PIL/ContainerIO.py @@ -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 diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index c6e12150e..7abd459f9 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -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): diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index c1f7684ea..58adc1b63 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -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); diff --git a/src/libImaging/QuantHeap.c b/src/libImaging/QuantHeap.c index 121b87275..498d44b1d 100644 --- a/src/libImaging/QuantHeap.c +++ b/src/libImaging/QuantHeap.c @@ -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 (newsizeheapsize) 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) { diff --git a/src/libImaging/QuantOctree.c b/src/libImaging/QuantOctree.c index 6c0f605c9..83d987544 100644 --- a/src/libImaging/QuantOctree.c +++ b/src/libImaging/QuantOctree.c @@ -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;