diff --git a/CHANGES.rst b/CHANGES.rst index 96239a6e2..c79b9d1ef 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 7.2.0 (unreleased) ------------------ +- Deprecate Image.show(command="...") #4646 + [nulano, hugovk, radarhere] + +- Updated JPEG magic number #4707 + [Cykooz, radarhere] + - Change STRIPBYTECOUNTS to LONG if necessary when saving #4626 [radarhere, hugovk] diff --git a/Tests/images/imagedraw_wide_line_larger_than_int.png b/Tests/images/imagedraw_wide_line_larger_than_int.png new file mode 100644 index 000000000..68073ce48 Binary files /dev/null and b/Tests/images/imagedraw_wide_line_larger_than_int.png differ diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index be8e21d5a..1801cd8ff 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -3,7 +3,14 @@ import re from io import BytesIO import pytest -from PIL import ExifTags, Image, ImageFile, JpegImagePlugin, features +from PIL import ( + ExifTags, + Image, + ImageFile, + JpegImagePlugin, + UnidentifiedImageError, + features, +) from .helper import ( assert_image, @@ -709,6 +716,24 @@ class TestFileJpeg: with Image.open("Tests/images/icc-after-SOF.jpg") as im: assert im.info["icc_profile"] == b"profile" + def test_jpeg_magic_number(self): + size = 4097 + buffer = BytesIO(b"\xFF" * size) # Many xFF bytes + buffer.max_pos = 0 + orig_read = buffer.read + + def read(n=-1): + res = orig_read(n) + buffer.max_pos = max(buffer.max_pos, buffer.tell()) + return res + + buffer.read = read + with pytest.raises(UnidentifiedImageError): + Image.open(buffer) + + # Assert the entire file has not been read + assert 0 < buffer.max_pos < size + @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") diff --git a/Tests/test_image.py b/Tests/test_image.py index 4d1b66dff..f284f89a7 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -664,6 +664,18 @@ class TestImage: except OSError as e: assert str(e) == "buffer overrun when reading image file" + def test_show_deprecation(self, monkeypatch): + monkeypatch.setattr(Image, "_show", lambda *args, **kwargs: None) + + im = Image.new("RGB", (50, 50), "white") + + with pytest.warns(None) as raised: + im.show() + assert not raised + + with pytest.warns(DeprecationWarning): + im.show(command="mock") + class MockEncoder: pass diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 08fb589fc..37bc99559 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -5,7 +5,7 @@ from PIL import Image, ImageColor, ImageDraw, ImageFont from .helper import ( assert_image_equal, - assert_image_similar, + assert_image_similar_tofile, hopper, skip_unless_feature, ) @@ -71,7 +71,7 @@ def helper_arc(bbox, start, end): draw.arc(bbox, start, end) # Assert - assert_image_similar(im, Image.open("Tests/images/imagedraw_arc.png"), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) def test_arc1(): @@ -110,20 +110,19 @@ def test_arc_no_loops(): draw.arc(BBOX1, start=start, end=end) # Assert - assert_image_similar(im, Image.open("Tests/images/imagedraw_arc_no_loops.png"), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_no_loops.png", 1) def test_arc_width(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_arc_width.png" # Act draw.arc(BBOX1, 10, 260, width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width.png", 1) def test_arc_width_pieslice_large(): @@ -131,26 +130,24 @@ def test_arc_width_pieslice_large(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_arc_width_pieslice.png" # Act draw.arc(BBOX1, 10, 260, fill="yellow", width=100) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_pieslice.png", 1) def test_arc_width_fill(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_arc_width_fill.png" # Act draw.arc(BBOX1, 10, 260, fill="yellow", width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_fill.png", 1) def test_arc_width_non_whole_angle(): @@ -163,7 +160,7 @@ def test_arc_width_non_whole_angle(): draw.arc(BBOX1, 10, 259.5, width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, expected, 1) def test_bitmap(): @@ -190,7 +187,7 @@ def helper_chord(mode, bbox, start, end): draw.chord(bbox, start, end, fill="red", outline="yellow") # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, expected, 1) def test_chord1(): @@ -209,26 +206,24 @@ def test_chord_width(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_chord_width.png" # Act draw.chord(BBOX1, 10, 260, outline="yellow", width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width.png", 1) def test_chord_width_fill(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_chord_width_fill.png" # Act draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width_fill.png", 1) def test_chord_zero_width(): @@ -254,7 +249,7 @@ def helper_ellipse(mode, bbox): draw.ellipse(bbox, fill="green", outline="blue") # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, expected, 1) def test_ellipse1(): @@ -276,8 +271,8 @@ def test_ellipse_translucent(): draw.ellipse(BBOX1, fill=(0, 255, 0, 127)) # Assert - expected = Image.open("Tests/images/imagedraw_ellipse_translucent.png") - assert_image_similar(im, expected, 1) + expected = "Tests/images/imagedraw_ellipse_translucent.png" + assert_image_similar_tofile(im, expected, 1) def test_ellipse_edge(): @@ -289,7 +284,7 @@ def test_ellipse_edge(): draw.ellipse(((0, 0), (W - 1, H)), fill="white") # Assert - assert_image_similar(im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) def test_ellipse_symmetric(): @@ -304,39 +299,36 @@ def test_ellipse_width(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_ellipse_width.png" # Act draw.ellipse(BBOX1, outline="blue", width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1) def test_ellipse_width_large(): # Arrange im = Image.new("RGB", (500, 500)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_ellipse_width_large.png" # Act draw.ellipse((25, 25, 475, 475), outline="blue", width=75) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_large.png", 1) def test_ellipse_width_fill(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_ellipse_width_fill.png" # Act draw.ellipse(BBOX1, fill="green", outline="blue", width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_fill.png", 1) def test_ellipse_zero_width(): @@ -423,7 +415,7 @@ def helper_pieslice(bbox, start, end): draw.pieslice(bbox, start, end, fill="white", outline="blue") # Assert - assert_image_similar(im, Image.open("Tests/images/imagedraw_pieslice.png"), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) def test_pieslice1(): @@ -440,13 +432,12 @@ def test_pieslice_width(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_pieslice_width.png" # Act draw.pieslice(BBOX1, 10, 260, outline="blue", width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice_width.png", 1) def test_pieslice_width_fill(): @@ -459,7 +450,7 @@ def test_pieslice_width_fill(): draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, expected, 1) def test_pieslice_zero_width(): @@ -571,13 +562,12 @@ def test_big_rectangle(): im = Image.new("RGB", (W, H)) bbox = [(-1, -1), (W + 1, H + 1)] draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_big_rectangle.png" # Act draw.rectangle(bbox, fill="orange") # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_big_rectangle.png", 1) def test_rectangle_width(): @@ -878,13 +868,25 @@ def test_wide_line_dot(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_wide_line_dot.png" # Act draw.line([(50, 50), (50, 50)], width=3) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_wide_line_dot.png", 1) + + +def test_wide_line_larger_than_int(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_wide_line_larger_than_int.png" + + # Act + draw.line([(0, 0), (32768, 32768)], width=3) + + # Assert + assert_image_similar_tofile(im, expected, 1) @pytest.mark.parametrize( @@ -971,13 +973,12 @@ def test_wide_line_dot(): def test_line_joint(xy): im = Image.new("RGB", (500, 325)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_line_joint_curve.png" # Act draw.line(xy, GRAY, 50, "curve") # Assert - assert_image_similar(im, Image.open(expected), 3) + assert_image_similar_tofile(im, "Tests/images/imagedraw_line_joint_curve.png", 3) def test_textsize_empty_string(): @@ -1018,8 +1019,8 @@ def test_stroke(): draw.text((12, 12), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill) # Assert - assert_image_similar( - im, Image.open("Tests/images/imagedraw_stroke_" + suffix + ".png"), 3.1 + assert_image_similar_tofile( + im, "Tests/images/imagedraw_stroke_" + suffix + ".png", 3.1 ) @@ -1034,9 +1035,7 @@ def test_stroke_descender(): draw.text((12, 2), "y", "#f00", font, stroke_width=2, stroke_fill="#0f0") # Assert - assert_image_similar( - im, Image.open("Tests/images/imagedraw_stroke_descender.png"), 6.78 - ) + assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.78) @skip_unless_feature("freetype2") @@ -1052,9 +1051,7 @@ def test_stroke_multiline(): ) # Assert - assert_image_similar( - im, Image.open("Tests/images/imagedraw_stroke_multiline.png"), 3.3 - ) + assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3) def test_same_color_outline(): @@ -1093,4 +1090,4 @@ def test_same_color_outline(): expected = "Tests/images/imagedraw_outline_{}_{}.png".format( operation, mode ) - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, expected, 1) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 885fba4cd..38d2143c4 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,6 +12,14 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. +Image.show command parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 7.2.0 + +The ``command`` parameter was deprecated and will be removed in a future release. +Use a subclass of ``ImageShow.Viewer`` instead. + ImageFile.raise_ioerror ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/7.2.0.rst b/docs/releasenotes/7.2.0.rst index 26a1464a4..6b91a1b01 100644 --- a/docs/releasenotes/7.2.0.rst +++ b/docs/releasenotes/7.2.0.rst @@ -37,6 +37,12 @@ are now read as just a single bytestring. Deprecations ^^^^^^^^^^^^ +Image.show command parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``command`` parameter was deprecated and will be removed in a future release. +Use a subclass of :py:class:`PIL.ImageShow.Viewer` instead. + ImageFile.raise_ioerror ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 85e2350c5..7bd5a0308 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -263,7 +263,7 @@ class BmpImageFile(ImageFile.ImageFile): # read 14 bytes: magic number, filesize, reserved, header final offset head_data = self.fp.read(14) # choke if the file does not have the required magic bytes - if head_data[0:2] != b"BM": + if not _accept(head_data): raise SyntaxError("Not a BMP file") # read the start position of the BMP image data (u32) offset = i32(head_data[10:14]) diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index a12d9195b..de21db8f0 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -46,7 +46,7 @@ class DcxImageFile(PcxImageFile): # Header s = self.fp.read(4) - if i32(s) != MAGIC: + if not _accept(s): raise SyntaxError("not a DCX file") # Component directory diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 989094569..f09d62ce3 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -42,9 +42,8 @@ class FliImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(128) - magic = i16(s[4:6]) if not ( - magic in [0xAF11, 0xAF12] + _accept(s) and i16(s[14:16]) in [0, 3] # flags and s[20:22] == b"\x00\x00" # reserved ): @@ -60,6 +59,7 @@ class FliImageFile(ImageFile.ImageFile): # animation speed duration = i32(s[16:20]) + magic = i16(s[4:6]) if magic == 0xAF11: duration = (duration * 1000) // 70 self.info["duration"] = duration diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 9d360beae..6fd6182ab 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -63,7 +63,7 @@ class GifImageFile(ImageFile.ImageFile): # Screen s = self.fp.read(13) - if s[:6] not in [b"GIF87a", b"GIF89a"]: + if not _accept(s): raise SyntaxError("not a GIF file") self.info["version"] = s[:6] diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 2fc37d9ce..02c73bc49 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2181,8 +2181,10 @@ class Image: def show(self, title=None, command=None): """ - Displays this image. This method is mainly intended for - debugging purposes. + Displays this image. This method is mainly intended for debugging purposes. + + This method calls :py:func:`PIL.ImageShow.show` internally. You can use + :py:func:`PIL.ImageShow.register` to override its default behaviour. The image is first saved to a temporary file. By default, it will be in PNG format. @@ -2194,11 +2196,16 @@ class Image: On Windows, the image is opened with the standard PNG display utility. - :param title: Optional title to use for the image window, - where possible. - :param command: command used to show the image + :param title: Optional title to use for the image window, where possible. """ + if command is not None: + warnings.warn( + "The command parameter is deprecated and will be removed in a future " + "release. Use a subclass of ImageShow.Viewer instead.", + DeprecationWarning, + ) + _show(self, title=title, command=command) def split(self): diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 89e70f0e9..f846569b2 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -323,7 +323,8 @@ MARKER = { def _accept(prefix): - return prefix[0:1] == b"\377" + # Magic number was taken from https://en.wikipedia.org/wiki/JPEG + return prefix[0:3] == b"\xFF\xD8\xFF" ## @@ -337,10 +338,11 @@ class JpegImageFile(ImageFile.ImageFile): def _open(self): - s = self.fp.read(1) + s = self.fp.read(3) - if i8(s) != 255: + if not _accept(s): raise SyntaxError("not a JPEG file") + s = b"\xFF" # Create attributes self.bits = self.layers = 0 diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 2b2937ecf..26acdd898 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -51,7 +51,7 @@ class MspImageFile(ImageFile.ImageFile): # Header s = self.fp.read(32) - if s[:4] not in [b"DanM", b"LinS"]: + if not _accept(s): raise SyntaxError("not an MSP file") # Header checksum diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 5ea32ba89..91f0314b5 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -43,7 +43,7 @@ class PixarImageFile(ImageFile.ImageFile): # assuming a 4-byte magic label s = self.fp.read(4) - if s != b"\200\350\000\000": + if not _accept(s): raise SyntaxError("not a PIXAR file") # read rest of header diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index f62bf8542..51b78c7de 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -633,7 +633,7 @@ class PngImageFile(ImageFile.ImageFile): def _open(self): - if self.fp.read(8) != _MAGIC: + if not _accept(self.fp.read(8)): raise SyntaxError("not a PNG file") self.__fp = self.fp self.__frame = 0 diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 044df443d..89e55a4ce 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -61,7 +61,7 @@ class PsdImageFile(ImageFile.ImageFile): # header s = read(26) - if s[:4] != b"8BPS" or i16(s[4:]) != 1: + if not _accept(s) or i16(s[4:]) != 1: raise SyntaxError("not a PSD file") psd_bits = i16(s[22:]) diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index ddd3de379..ec9855e77 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -58,8 +58,7 @@ class SgiImageFile(ImageFile.ImageFile): headlen = 512 s = self.fp.read(headlen) - # magic number : 474 - if i16(s) != 474: + if not _accept(s): raise ValueError("Not an SGI image file") # compression : verbatim or RLE diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index fd7ca8a40..d99884293 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -53,7 +53,7 @@ class SunImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(32) - if i32(s) != 0x59A66A95: + if not _accept(s): raise SyntaxError("not an SUN raster file") offset = 32 diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 947d7f70f..08f6c3843 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -683,7 +683,7 @@ ImagingDrawWideLine(Imaging im, int x0, int y0, int x1, int y1, return 0; } - big_hypotenuse = sqrt((double) (dx*dx + dy*dy)); + big_hypotenuse = hypot(dx, dy); small_hypotenuse = (width - 1) / 2.0; ratio_max = ROUND_UP(small_hypotenuse) / big_hypotenuse; ratio_min = ROUND_DOWN(small_hypotenuse) / big_hypotenuse; diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index beafda4a4..553ed6365 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -251,9 +251,9 @@ deps = { "libs": [r"*.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/2.6.7.zip", - "filename": "harfbuzz-2.6.7.zip", - "dir": "harfbuzz-2.6.7", + "url": "https://github.com/harfbuzz/harfbuzz/archive/2.6.8.zip", + "filename": "harfbuzz-2.6.8.zip", + "dir": "harfbuzz-2.6.8", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"),