From ae194096d064d6dd02d2a0cfa147fa4730d81f57 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Fri, 8 Mar 2019 12:48:22 -0700 Subject: [PATCH 01/55] Allow correct delta generation for GIFs with disposal 2 (Fixes #3665) --- src/PIL/GifImagePlugin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 2ebd8b248..44c32faeb 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -443,7 +443,10 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if _get_palette_bytes(im_frame) == \ + if encoderinfo["disposal"] == 2: + # If diposing whole frame, treat full new frame as delta + delta = im_frame + elif _get_palette_bytes(im_frame) == \ _get_palette_bytes(previous['im']): delta = ImageChops.subtract_modulo(im_frame, previous['im']) From 1f6d1be7fc609ae436b8ab9e8674ba05163ed8e9 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Fri, 8 Mar 2019 12:57:53 -0700 Subject: [PATCH 02/55] Ensure disposal key exists before checking --- src/PIL/GifImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 44c32faeb..7ba178d39 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -443,7 +443,7 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if encoderinfo["disposal"] == 2: + if "disposal" in encoderinfo and encoderinfo["disposal"] == 2: # If diposing whole frame, treat full new frame as delta delta = im_frame elif _get_palette_bytes(im_frame) == \ From 4a2be2af2d74e049cf53aa894688cb6aecf22522 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Fri, 8 Mar 2019 15:56:31 -0700 Subject: [PATCH 03/55] Create gif frame delta by subtracting 0x0 image if disposal is mode 2 --- src/PIL/GifImagePlugin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 7ba178d39..d6fd27440 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -443,9 +443,11 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if "disposal" in encoderinfo and encoderinfo["disposal"] == 2: - # If diposing whole frame, treat full new frame as delta - delta = im_frame + if encoderinfo["disposal"] == 2: + # Entire frame should be delta + # Create delta by subtracting empty image from frame (This is required) + delta = ImageChops.subtract_modulo( + im_frame, Image.new('P', (0,0))) elif _get_palette_bytes(im_frame) == \ _get_palette_bytes(previous['im']): delta = ImageChops.subtract_modulo(im_frame, From c73da62ce949547b1ec6ac32027de6f1708c422b Mon Sep 17 00:00:00 2001 From: Sir Cinnamon Date: Fri, 8 Mar 2019 16:34:44 -0700 Subject: [PATCH 04/55] Ensure disposal key exists before checking --- src/PIL/GifImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index d6fd27440..776875138 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -443,7 +443,7 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if encoderinfo["disposal"] == 2: + if "disposal" in encoderinfo and encoderinfo["disposal"] == 2: # Entire frame should be delta # Create delta by subtracting empty image from frame (This is required) delta = ImageChops.subtract_modulo( From 3b1a1fbfd2863be6141fc024ff69e23bd70ffdc1 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Mon, 11 Mar 2019 07:41:14 -0600 Subject: [PATCH 05/55] Create background image for calculating gif deltas --- src/PIL/GifImagePlugin.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index d6fd27440..f301a09eb 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -443,20 +443,20 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if encoderinfo["disposal"] == 2: - # Entire frame should be delta - # Create delta by subtracting empty image from frame (This is required) - delta = ImageChops.subtract_modulo( - im_frame, Image.new('P', (0,0))) - elif _get_palette_bytes(im_frame) == \ - _get_palette_bytes(previous['im']): + if ("disposal" in im.encoderinfo and im.encoderinfo["disposal"] == 2): + base_image = background + else: + base_image = previous["im"] + + if _get_palette_bytes(im_frame) == \ + _get_palette_bytes(base_image): delta = ImageChops.subtract_modulo(im_frame, - previous['im']) + base_image) else: delta = ImageChops.subtract_modulo( - im_frame.convert('RGB'), previous['im'].convert('RGB')) + im_frame.convert('RGB'), base_image.convert('RGB')) bbox = delta.getbbox() - if not bbox: + if not bbox and not ("disposal" in im.encoderinfo and im.encoderinfo["disposal"] == 2): # This frame is identical to the previous frame if duration: previous['encoderinfo']['duration'] += \ @@ -464,6 +464,7 @@ def _write_multiple_frames(im, fp, palette): continue else: bbox = None + background = Image.new("P", im_frame.size, 0) im_frames.append({ 'im': im_frame, 'bbox': bbox, From 3b74281a2ff91d807fbdcea864374cef58b34124 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Mon, 11 Mar 2019 07:55:37 -0600 Subject: [PATCH 06/55] Fix line lengths and init background out of loop --- src/PIL/GifImagePlugin.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index f301a09eb..9d0ce5404 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -423,6 +423,7 @@ def _write_multiple_frames(im, fp, palette): im_frames = [] frame_count = 0 + background = None for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): for im_frame in ImageSequence.Iterator(imSequence): @@ -443,20 +444,21 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if ("disposal" in im.encoderinfo and im.encoderinfo["disposal"] == 2): + if ("disposal" in im.encoderinfo \ + and im.encoderinfo["disposal"] == 2): base_image = background else: base_image = previous["im"] if _get_palette_bytes(im_frame) == \ _get_palette_bytes(base_image): - delta = ImageChops.subtract_modulo(im_frame, - base_image) + delta = ImageChops.subtract_modulo(im_frame, base_image) else: delta = ImageChops.subtract_modulo( im_frame.convert('RGB'), base_image.convert('RGB')) bbox = delta.getbbox() - if not bbox and not ("disposal" in im.encoderinfo and im.encoderinfo["disposal"] == 2): + if not bbox and not ("disposal" in im.encoderinfo \ + and im.encoderinfo["disposal"] == 2): # This frame is identical to the previous frame if duration: previous['encoderinfo']['duration'] += \ From 583d731a967c2e1d9b0bfe31bc1e36696115a866 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Mon, 11 Mar 2019 08:02:04 -0600 Subject: [PATCH 07/55] Fix line indents for linting --- src/PIL/GifImagePlugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 9d0ce5404..dafe6a3f9 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -444,8 +444,8 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if ("disposal" in im.encoderinfo \ - and im.encoderinfo["disposal"] == 2): + if "disposal" in im.encoderinfo \ + and im.encoderinfo["disposal"] == 2: base_image = background else: base_image = previous["im"] @@ -457,8 +457,8 @@ def _write_multiple_frames(im, fp, palette): delta = ImageChops.subtract_modulo( im_frame.convert('RGB'), base_image.convert('RGB')) bbox = delta.getbbox() - if not bbox and not ("disposal" in im.encoderinfo \ - and im.encoderinfo["disposal"] == 2): + if not bbox and not ("disposal" in im.encoderinfo + and im.encoderinfo["disposal"] == 2): # This frame is identical to the previous frame if duration: previous['encoderinfo']['duration'] += \ From 96c5a4c5438c9f95acb47c530935e9abc8d1c31a Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Thu, 14 Mar 2019 14:40:31 -0600 Subject: [PATCH 08/55] Add test for disposal mode 2 gifs --- Tests/test_file_gif.py | 51 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 6a4b14d40..9b0d61541 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -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 @@ -315,6 +315,55 @@ class TestFileGif(PillowTestCase): img.seek(img.tell() + 1) self.assertEqual(img.disposal_method, i+1) + def test_dispose2_diff(self): + out = self.tempfile('temp.gif') + # 4 backgrounds: White, Grey, Black, Red + im_list = [ + Image.new('RGB', (100, 100), '#fff'), + Image.new('RGB', (100, 100), '#999'), + Image.new('RGB', (100, 100), '#000'), + Image.new('RGB', (100, 100), '#f00'), + ] + # Red circle in center of each frame + for img in im_list: + d = ImageDraw.Draw(img) + d.ellipse([(40,40),(60,60)], fill='#f00') + + # check per frame disposal + im_list[0].save( + out, + save_all=True, + append_images=im_list[1:], + disposal=2 + ) + + img = Image.open(out) + top_left_pixels = [] + center_pixels = [] + + # # Get pixel in top left + # rgb_img = img.convert('RGB') + # r, g, b = rgb_img.getpixel((1,1)) + # top_left_pixels += [(r,g,b)] + # r, g, b = rgb_img.getpixel((50,50)) + # center_pixels += [(r,g,b)] + + for i in range(3): + rgb_img = img.convert('RGB') + # Get pixel in top left + r, g, b = rgb_img.getpixel((1,1)) + top_left_pixels += [(r,g,b)] + # Get pixel in center + r, g, b = rgb_img.getpixel((50,50)) + center_pixels += [(r,g,b)] + for prev in top_left_pixels[:i]: + # Change background every frame + self.assertNotEqual((r,g,b), prev) + for prev in center_pixels[:i]: + # Center remains red every frame + self.assertEqual((r,g,b), (255,0,0)) + img.seek(img.tell() + 1) + def test_iss634(self): img = Image.open("Tests/images/iss634.gif") # seek to the second frame From 8a36a15ebd8635e4d2be0ea4629d99c50628cb3b Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Thu, 14 Mar 2019 14:41:10 -0600 Subject: [PATCH 09/55] Force include colour table for disposal=2 gifs and pad colour table to be valid when flag is set --- src/PIL/GifImagePlugin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index dafe6a3f9..13036b1b4 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -476,6 +476,9 @@ def _write_multiple_frames(im, fp, palette): if len(im_frames) > 1: for frame_data in im_frames: im_frame = frame_data['im'] + if("disposal" in frame_data["encoderinfo"] \ + and frame_data["encoderinfo"]["disposal"]==2): + frame_data['encoderinfo']['include_color_table'] = True if not frame_data['bbox']: # global header for s in _get_global_header(im_frame, @@ -585,6 +588,8 @@ def _write_local_header(fp, im, offset, flags): include_color_table = im.encoderinfo.get('include_color_table') if include_color_table: palette_bytes = _get_palette_bytes(im) + # If needed, expand palette to minimum size + while(len(palette_bytes)<9): palette_bytes = palette_bytes*2 color_table_size = _get_color_table_size(palette_bytes) if color_table_size: flags = flags | 128 # local color table flag From 53cfd19a44b49da1603608b747facc4d374678b5 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Thu, 14 Mar 2019 14:44:15 -0600 Subject: [PATCH 10/55] Check encoder info for disposal mode --- src/PIL/GifImagePlugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 13036b1b4..1171c1fe1 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -444,8 +444,8 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if "disposal" in im.encoderinfo \ - and im.encoderinfo["disposal"] == 2: + if "disposal" in encoderinfo \ + and encoderinfo["disposal"] == 2: base_image = background else: base_image = previous["im"] @@ -457,8 +457,8 @@ def _write_multiple_frames(im, fp, palette): delta = ImageChops.subtract_modulo( im_frame.convert('RGB'), base_image.convert('RGB')) bbox = delta.getbbox() - if not bbox and not ("disposal" in im.encoderinfo - and im.encoderinfo["disposal"] == 2): + if not bbox and not ("disposal" in encoderinfo + and encoderinfo["disposal"] == 2): # This frame is identical to the previous frame if duration: previous['encoderinfo']['duration'] += \ From 85a07bb3852b76a5f205de27945ed3ee5a41b442 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Thu, 14 Mar 2019 14:51:13 -0600 Subject: [PATCH 11/55] Linting changes --- Tests/test_file_gif.py | 22 +++++++--------------- src/PIL/GifImagePlugin.py | 7 ++++--- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 9b0d61541..7110e34e4 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -327,9 +327,8 @@ class TestFileGif(PillowTestCase): # Red circle in center of each frame for img in im_list: d = ImageDraw.Draw(img) - d.ellipse([(40,40),(60,60)], fill='#f00') + d.ellipse([(40, 40), (60, 60)], fill='#f00') - # check per frame disposal im_list[0].save( out, save_all=True, @@ -341,27 +340,20 @@ class TestFileGif(PillowTestCase): top_left_pixels = [] center_pixels = [] - # # Get pixel in top left - # rgb_img = img.convert('RGB') - # r, g, b = rgb_img.getpixel((1,1)) - # top_left_pixels += [(r,g,b)] - # r, g, b = rgb_img.getpixel((50,50)) - # center_pixels += [(r,g,b)] - for i in range(3): rgb_img = img.convert('RGB') # Get pixel in top left - r, g, b = rgb_img.getpixel((1,1)) - top_left_pixels += [(r,g,b)] + r, g, b = rgb_img.getpixel((1, 1)) + top_left_pixels += [(r, g, b)] # Get pixel in center - r, g, b = rgb_img.getpixel((50,50)) - center_pixels += [(r,g,b)] + r, g, b = rgb_img.getpixel((50, 50)) + center_pixels += [(r, g, b)] for prev in top_left_pixels[:i]: # Change background every frame - self.assertNotEqual((r,g,b), prev) + self.assertNotEqual((r, g, b), prev) for prev in center_pixels[:i]: # Center remains red every frame - self.assertEqual((r,g,b), (255,0,0)) + self.assertEqual((r, g, b), (255, 0, 0)) img.seek(img.tell() + 1) def test_iss634(self): diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 1171c1fe1..49417c303 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -476,8 +476,8 @@ def _write_multiple_frames(im, fp, palette): if len(im_frames) > 1: for frame_data in im_frames: im_frame = frame_data['im'] - if("disposal" in frame_data["encoderinfo"] \ - and frame_data["encoderinfo"]["disposal"]==2): + if("disposal" in frame_data["encoderinfo"] + and frame_data["encoderinfo"]["disposal"] == 2): frame_data['encoderinfo']['include_color_table'] = True if not frame_data['bbox']: # global header @@ -589,7 +589,8 @@ def _write_local_header(fp, im, offset, flags): if include_color_table: palette_bytes = _get_palette_bytes(im) # If needed, expand palette to minimum size - while(len(palette_bytes)<9): palette_bytes = palette_bytes*2 + while(len(palette_bytes) < 9): + palette_bytes = palette_bytes*2 color_table_size = _get_color_table_size(palette_bytes) if color_table_size: flags = flags | 128 # local color table flag From 0b630e06dc38b6b4ad301571979a35b1eee8c391 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 15 Mar 2019 10:29:33 +1100 Subject: [PATCH 12/55] Test that background colours read are equal to saved colours --- Tests/test_file_gif.py | 42 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 7110e34e4..21a22630e 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -317,18 +317,20 @@ class TestFileGif(PillowTestCase): def test_dispose2_diff(self): out = self.tempfile('temp.gif') + # 4 backgrounds: White, Grey, Black, Red - im_list = [ - Image.new('RGB', (100, 100), '#fff'), - Image.new('RGB', (100, 100), '#999'), - Image.new('RGB', (100, 100), '#000'), - Image.new('RGB', (100, 100), '#f00'), - ] - # Red circle in center of each frame - for img in im_list: + backgrounds = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] + + im_list = [] + for background in backgrounds: + img = Image.new('RGB', (100, 100), background) + + # Red circle in center of each frame d = ImageDraw.Draw(img) d.ellipse([(40, 40), (60, 60)], fill='#f00') + im_list.append(img) + im_list[0].save( out, save_all=True, @@ -337,24 +339,16 @@ class TestFileGif(PillowTestCase): ) img = Image.open(out) - top_left_pixels = [] - center_pixels = [] - for i in range(3): + for i, background in enumerate(backgrounds): + img.seek(i) rgb_img = img.convert('RGB') - # Get pixel in top left - r, g, b = rgb_img.getpixel((1, 1)) - top_left_pixels += [(r, g, b)] - # Get pixel in center - r, g, b = rgb_img.getpixel((50, 50)) - center_pixels += [(r, g, b)] - for prev in top_left_pixels[:i]: - # Change background every frame - self.assertNotEqual((r, g, b), prev) - for prev in center_pixels[:i]: - # Center remains red every frame - self.assertEqual((r, g, b), (255, 0, 0)) - img.seek(img.tell() + 1) + + # Check top left pixel matches background + self.assertEqual(rgb_img.getpixel((0, 0)), background) + + # Center remains red every frame + self.assertEqual(rgb_img.getpixel((50, 50)), (255, 0, 0)) def test_iss634(self): img = Image.open("Tests/images/iss634.gif") From 4b2746fc39afe17abcaf2096250e61bf784d631c Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Fri, 22 Mar 2019 08:19:01 -0600 Subject: [PATCH 13/55] Remove disposal 2 duplicate frame exemption and add true delta test --- Tests/test_file_gif.py | 63 ++++++++++++++++++++++++++++++++++----- src/PIL/GifImagePlugin.py | 3 +- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 21a22630e..c754d6ed5 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -315,19 +315,19 @@ class TestFileGif(PillowTestCase): img.seek(img.tell() + 1) self.assertEqual(img.disposal_method, i+1) - def test_dispose2_diff(self): + def test_dispose2_palette(self): out = self.tempfile('temp.gif') # 4 backgrounds: White, Grey, Black, Red - backgrounds = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] + circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] im_list = [] - for background in backgrounds: - img = Image.new('RGB', (100, 100), background) + 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='#f00') + d.ellipse([(40, 40), (60, 60)], fill=circle) im_list.append(img) @@ -340,15 +340,62 @@ class TestFileGif(PillowTestCase): img = Image.open(out) - for i, background in enumerate(backgrounds): + 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)), background) + self.assertEqual(rgb_img.getpixel((0, 0)), (255, 0, 0)) # Center remains red every frame - self.assertEqual(rgb_img.getpixel((50, 50)), (255, 0, 0)) + 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_iss634(self): img = Image.open("Tests/images/iss634.gif") diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 49417c303..64ef63b2e 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -457,8 +457,7 @@ def _write_multiple_frames(im, fp, palette): delta = ImageChops.subtract_modulo( im_frame.convert('RGB'), base_image.convert('RGB')) bbox = delta.getbbox() - if not bbox and not ("disposal" in encoderinfo - and encoderinfo["disposal"] == 2): + if not bbox: # This frame is identical to the previous frame if duration: previous['encoderinfo']['duration'] += \ From ad70fc73cbd17da4fb28cb00303ec2a52f82ba38 Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Fri, 22 Mar 2019 08:26:16 -0600 Subject: [PATCH 14/55] Linting changes --- Tests/test_file_gif.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index c754d6ed5..36a2b2648 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -323,7 +323,7 @@ class TestFileGif(PillowTestCase): im_list = [] for circle in circles: - img = Image.new('RGB', (100, 100), (255,0,0)) + img = Image.new('RGB', (100, 100), (255, 0, 0)) # Red circle in center of each frame d = ImageDraw.Draw(img) @@ -355,16 +355,16 @@ class TestFileGif(PillowTestCase): # 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)) + ((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)) + img = Image.new('RGBA', (100, 100), (255, 255, 255, 0)) # Two circles per frame d = ImageDraw.Draw(img) @@ -394,8 +394,7 @@ class TestFileGif(PillowTestCase): 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)) - + self.assertEqual(rgb_img.getpixel((1, 1)), (255, 255, 255, 0)) def test_iss634(self): img = Image.open("Tests/images/iss634.gif") From c4daa8741529aaa482159642f86992876001dddd Mon Sep 17 00:00:00 2001 From: abojja9 Date: Sat, 6 Apr 2019 13:42:22 -0700 Subject: [PATCH 15/55] Add documentation to Image module --- .gitignore | 3 ++ docs/reference/Image.rst | 87 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/.gitignore b/.gitignore index 861b801b5..ef7520c0d 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,9 @@ docs/_build/ \#*# .#* +#VS Code +.vscode + #Komodo *.komodoproject diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 388116a10..7ef679bb8 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -121,10 +121,58 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.copy .. automethod:: PIL.Image.Image.crop + +The following script crops the input image with the provided coordinates: + +.. code-block:: python + + from PIL import Image + im = Image.open('cat.jpg') + + # crop method from Image module takes four coordinates as input. + # The right can also be represented as (left+width) + # and lower can be represented as (upper+height) + (left, upper, right, lower) = (200, 20, 520, 260) + + # Here the image "im" is cropped and assiged to new variable im_crop + im_crop = im.crop((left, upper, right, lower)) + + .. automethod:: PIL.Image.Image.draft .. automethod:: PIL.Image.Image.filter + +The following script blurs the input image using a filter from ImageFilter module: + +.. code-block:: python + + from PIL import Image + from PIL import ImageFilter + im = Image.open('cat.jpg') + + # Blur the input image using the filter ImageFilter.BLUR. + im_blurred = im.filter(filter=ImageFilter.BLUR) + .. automethod:: PIL.Image.Image.getbands + +The following script helps to get the bands of the input image: + +.. code-block:: python + + from PIL import Image + im = Image.open('cat.jpg') + print (im.getbands()) # Returns ('R', 'G', 'B') + .. automethod:: PIL.Image.Image.getbbox + +The following script helps to get the bounding box coordinates of the input image: + +.. code-block:: python + + from PIL import Image + im = Image.open('cat.jpg') + print (im.getbbox()) + # Returns four coordinates in the format (left, upper, right, lower) + .. automethod:: PIL.Image.Image.getcolors .. automethod:: PIL.Image.Image.getdata .. automethod:: PIL.Image.Image.getextrema @@ -140,8 +188,33 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.putpixel .. automethod:: PIL.Image.Image.quantize .. automethod:: PIL.Image.Image.resize + +The following script resizes the given image from (width, height) to (width/2, height/2): + +.. code-block:: python + + from PIL import Image + im = Image.open('cat.jpg') + + # Provide the target width and height of the image + (width, height) = (width//2, height//2) + im_resized = im.resize((width, height)) + .. automethod:: PIL.Image.Image.remap_palette .. automethod:: PIL.Image.Image.rotate + +The following script rotates the input image by `theta` degrees counter clockwise: + +.. code-block:: python + + from PIL import Image + im = Image.open('cat.jpg') + + # Rotate the image by 60 degrees counter clockwise. + theta = 60 + # Angle is in degrees counter clockwise. + im_rotated = im.rotate(angle=theta) + .. automethod:: PIL.Image.Image.save .. automethod:: PIL.Image.Image.seek .. automethod:: PIL.Image.Image.show @@ -154,6 +227,20 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.tostring .. automethod:: PIL.Image.Image.transform .. automethod:: PIL.Image.Image.transpose + +The following script flips the input image by using the method "Image.FLIP_LEFT_RIGHT". + +.. code-block:: python + + from PIL import Image + im = Image.open('cat.jpg') + + # Flip the image from left to right + im_flipped = im.transpose(method=Image.FLIP_LEFT_RIGHT) + # To flip the image from top to bottom, + # use the method "Image.FLIP_TOP_BOTTOM" + + .. automethod:: PIL.Image.Image.verify .. automethod:: PIL.Image.Image.fromstring From 7443e6d36beec01674e36e6ee11424cbb0e8786e Mon Sep 17 00:00:00 2001 From: Riley Lahd Date: Tue, 9 Apr 2019 08:23:59 -0600 Subject: [PATCH 16/55] Clean up disposal flag check --- src/PIL/GifImagePlugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 64ef63b2e..4f4e62d6d 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -444,8 +444,7 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if "disposal" in encoderinfo \ - and encoderinfo["disposal"] == 2: + if disposal == 2: base_image = background else: base_image = previous["im"] From e073f4add0a4ceb18f09c153b2b429f7dfe10c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kul=C3=ADk?= Date: Mon, 20 May 2019 14:25:27 +0200 Subject: [PATCH 17/55] Fix SPARC memory alignment issues in Pack/Unpack functions --- src/libImaging/Pack.c | 10 ++++++++++ src/libImaging/Unpack.c | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index 56aebf58e..000ee384a 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -251,6 +251,15 @@ ImagingPackRGB(UINT8* out, const UINT8* in, int pixels) { int i = 0; /* RGB triplets */ +#ifdef __sparc + /* SPARC CPUs cannot read integers from nonaligned addresses. */ + for (; i < pixels; i++) { + out[0] = in[R]; + out[1] = in[G]; + out[2] = in[B]; + out += 3; in += 4; + } +#else for (; i < pixels-1; i++) { ((UINT32*)out)[0] = ((UINT32*)in)[i]; out += 3; @@ -261,6 +270,7 @@ ImagingPackRGB(UINT8* out, const UINT8* in, int pixels) out[2] = in[i*4+B]; out += 3; } +#endif } void diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 02196d255..cd72c9724 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -480,6 +480,16 @@ void ImagingUnpackRGB(UINT8* _out, const UINT8* in, int pixels) { int i = 0; +#ifdef __sparc + /* SPARC CPUs cannot read integers from nonaligned addresses. */ + for (; i < pixels; i++) { + _out[R] = in[0]; + _out[G] = in[1]; + _out[B] = in[2]; + _out[A] = 255; + _out += 4; in += 3; + } +#else UINT32* out = (UINT32*) _out; /* RGB triplets */ for (; i < pixels-1; i++) { @@ -490,6 +500,7 @@ ImagingUnpackRGB(UINT8* _out, const UINT8* in, int pixels) out[i] = MAKE_UINT32(in[0], in[1], in[2], 255); in += 3; } +#endif } void @@ -1085,22 +1096,44 @@ static void copy4skip1(UINT8* _out, const UINT8* in, int pixels) { int i; +#ifdef __sparc + /* SPARC CPUs cannot read integers from nonaligned addresses. */ + for (i = 0; i < pixels; i++) { + _out[0] = in[0]; + _out[1] = in[1]; + _out[2] = in[2]; + _out[3] = in[3]; + _out += 4; in += 5; + } +#else UINT32* out = (UINT32*) _out; for (i = 0; i < pixels; i++) { out[i] = *(UINT32*)&in[0]; in += 5; } +#endif } static void copy4skip2(UINT8* _out, const UINT8* in, int pixels) { int i; +#ifdef __sparc + /* SPARC CPUs cannot read integers from nonaligned addresses. */ + for (i = 0; i < pixels; i++) { + _out[0] = in[0]; + _out[1] = in[1]; + _out[2] = in[2]; + _out[3] = in[3]; + _out += 4; in += 6; + } +#else UINT32* out = (UINT32*) _out; for (i = 0; i < pixels; i++) { out[i] = *(UINT32*)&in[0]; in += 6; } +#endif } From 560bc337314c35c599bf75ef3f925d35dc879245 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 31 May 2019 05:55:13 -0400 Subject: [PATCH 18/55] Pass the correct types to PyArg_ParseTuple. Py_ssize_t uses the 'n' specifier, not 'i'. --- src/_imagingft.c | 2 +- src/encode.c | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 00c0c5174..b604c8b2c 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -257,7 +257,7 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) return NULL; } - if (!PyArg_ParseTupleAndKeywords(args, kw, "eti|is"PY_ARG_BYTES_LENGTH"i", + if (!PyArg_ParseTupleAndKeywords(args, kw, "etn|ns"PY_ARG_BYTES_LENGTH"n", kwlist, Py_FileSystemDefaultEncoding, &filename, &size, &index, &encoding, &font_bytes, diff --git a/src/encode.c b/src/encode.c index 6832f90c8..40fbd4595 100644 --- a/src/encode.c +++ b/src/encode.c @@ -126,7 +126,7 @@ _encode(ImagingEncoderObject* encoder, PyObject* args) Py_ssize_t bufsize = 16384; - if (!PyArg_ParseTuple(args, "|i", &bufsize)) + if (!PyArg_ParseTuple(args, "|n", &bufsize)) return NULL; buf = PyBytes_FromStringAndSize(NULL, bufsize); @@ -180,7 +180,7 @@ _encode_to_file(ImagingEncoderObject* encoder, PyObject* args) Py_ssize_t fh; Py_ssize_t bufsize = 16384; - if (!PyArg_ParseTuple(args, "i|i", &fh, &bufsize)) + if (!PyArg_ParseTuple(args, "n|n", &fh, &bufsize)) return NULL; /* Allocate an encoder buffer */ @@ -229,7 +229,7 @@ _setimage(ImagingEncoderObject* encoder, PyObject* args) x0 = y0 = x1 = y1 = 0; /* FIXME: should publish the ImagingType descriptor */ - if (!PyArg_ParseTuple(args, "O|(iiii)", &op, &x0, &y0, &x1, &y1)) + if (!PyArg_ParseTuple(args, "O|(nnnn)", &op, &x0, &y0, &x1, &y1)) return NULL; im = PyImaging_AsImaging(op); if (!im) @@ -409,7 +409,7 @@ PyImaging_GifEncoderNew(PyObject* self, PyObject* args) char *rawmode; Py_ssize_t bits = 8; Py_ssize_t interlace = 0; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &bits, &interlace)) + if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &bits, &interlace)) return NULL; encoder = PyImaging_EncoderNew(sizeof(GIFENCODERSTATE)); @@ -441,7 +441,7 @@ PyImaging_PcxEncoderNew(PyObject* self, PyObject* args) char *rawmode; Py_ssize_t bits = 8; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &bits)) { + if (!PyArg_ParseTuple(args, "ss|n", &mode, &rawmode, &bits)) { return NULL; } @@ -474,7 +474,7 @@ PyImaging_RawEncoderNew(PyObject* self, PyObject* args) Py_ssize_t stride = 0; Py_ssize_t ystep = 1; - if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &stride, &ystep)) + if (!PyArg_ParseTuple(args, "ss|nn", &mode, &rawmode, &stride, &ystep)) return NULL; encoder = PyImaging_EncoderNew(0); @@ -506,7 +506,7 @@ PyImaging_TgaRleEncoderNew(PyObject* self, PyObject* args) char *rawmode; Py_ssize_t ystep = 1; - if (!PyArg_ParseTuple(args, "ss|i", &mode, &rawmode, &ystep)) + if (!PyArg_ParseTuple(args, "ss|n", &mode, &rawmode, &ystep)) return NULL; encoder = PyImaging_EncoderNew(0); @@ -567,7 +567,7 @@ PyImaging_ZipEncoderNew(PyObject* self, PyObject* args) Py_ssize_t compress_type = -1; char* dictionary = NULL; Py_ssize_t dictionary_size = 0; - if (!PyArg_ParseTuple(args, "ss|iii"PY_ARG_BYTES_LENGTH, &mode, &rawmode, + if (!PyArg_ParseTuple(args, "ss|nnn"PY_ARG_BYTES_LENGTH, &mode, &rawmode, &optimize, &compress_level, &compress_type, &dictionary, &dictionary_size)) @@ -717,7 +717,7 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) char* rawExif = NULL; Py_ssize_t rawExifLen = 0; - if (!PyArg_ParseTuple(args, "ss|iiiiiiiiO"PY_ARG_BYTES_LENGTH""PY_ARG_BYTES_LENGTH, + if (!PyArg_ParseTuple(args, "ss|nnnnnnnnO"PY_ARG_BYTES_LENGTH""PY_ARG_BYTES_LENGTH, &mode, &rawmode, &quality, &progressive, &smooth, &optimize, &streamtype, &xdpi, &ydpi, &subsampling, &qtables, &extra, &extra_size, @@ -823,7 +823,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) PyObject *keys, *values; - if (! PyArg_ParseTuple(args, "sssisO", &mode, &rawmode, &compname, &fp, &filename, &dir)) { + if (! PyArg_ParseTuple(args, "sssnsO", &mode, &rawmode, &compname, &fp, &filename, &dir)) { return NULL; } @@ -995,7 +995,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) OPJ_CINEMA_MODE cine_mode; Py_ssize_t fd = -1; - if (!PyArg_ParseTuple(args, "ss|OOOsOIOOOssi", &mode, &format, + if (!PyArg_ParseTuple(args, "ss|OOOsOnOOOssn", &mode, &format, &offset, &tile_offset, &tile_size, &quality_mode, &quality_layers, &num_resolutions, &cblk_size, &precinct_size, From db4916849ec255a9f4312e02dced7b3f893471ea Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 23 Jun 2019 07:34:39 +1000 Subject: [PATCH 19/55] Added ImageSequence all_frames --- Tests/test_imagesequence.py | 22 ++++++++++++++++++++++ src/PIL/ImageSequence.py | 22 ++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 38645f133..5d90dc4c5 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -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) diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index 84199fe27..f9be92d48 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -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 From 7bb16de81c12b456f9f1bf58475c5833534294e4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 23 Jun 2019 11:53:01 +1000 Subject: [PATCH 20/55] Fixed crash when loading non-font bytes --- Tests/test_imagefont.py | 4 ++++ src/PIL/ImageFont.py | 2 ++ src/_imagingft.c | 1 + 3 files changed, 7 insertions(+) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 3388c2055..0ee3b979e 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -420,6 +420,10 @@ class TestImageFont(PillowTestCase): self.assertRaises(IOError, ImageFont.load_path, filename) self.assertRaises(IOError, ImageFont.truetype, filename) + def test_load_non_font_bytes(self): + with open("Tests/images/hopper.jpg", "rb") as f: + self.assertRaises(IOError, ImageFont.truetype, f) + def test_default_font(self): # Arrange txt = 'This is a "better than nothing" default font.' diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 7074a70c0..f43f95b9a 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -545,6 +545,8 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): try: return freetype(font) except IOError: + if not isPath(font): + raise ttf_filename = os.path.basename(font) dirs = [] diff --git a/src/_imagingft.c b/src/_imagingft.c index f6bd787ef..28e6d2b5e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -315,6 +315,7 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) if (error) { if (self->font_bytes) { PyMem_Free(self->font_bytes); + self->font_bytes = NULL; } Py_DECREF(self); return geterror(error); From 4119d3015b29b3eac582a431173970bd33a44cdf Mon Sep 17 00:00:00 2001 From: Jeffery To Date: Thu, 2 May 2019 05:35:27 +0800 Subject: [PATCH 21/55] Include CPPFLAGS when searching for libraries This adds CPPFLAGS to the list of environment variables (currently CFLAGS and LDFLAGS) searched when looking for includes and library directories, as CPPFLAGS may also include -I options. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 5ccf7133d..2c25543a9 100755 --- a/setup.py +++ b/setup.py @@ -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)) From cb7d9bcd1213cae7fbb659402a0c87ebb91d204b Mon Sep 17 00:00:00 2001 From: David Nisson Date: Tue, 16 Apr 2019 21:20:29 -0700 Subject: [PATCH 22/55] updated TIFF tile descriptors to match current decoding functionality --- src/PIL/TiffImagePlugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 7596aae65..91da55882 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1328,8 +1328,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 @@ -1356,10 +1356,10 @@ class TiffImageFile(ImageFile.ImageFile): # adjust stride width accordingly stride /= bps_count - a = (tile_rawmode, int(stride), 1) + a = (tile_rawmode, int(stride), 1, offset.offset) self.tile.append( ( - self._compression, + "libtiff", (x, y, min(x + w, xsize), min(y + h, ysize)), offset, a, From 5857bf8243cbd64d1ee779cfee408f9f46121dd9 Mon Sep 17 00:00:00 2001 From: David Nisson Date: Tue, 16 Apr 2019 22:01:21 -0700 Subject: [PATCH 23/55] corrected args to reflect change to tile descriptors --- src/PIL/TiffImagePlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 91da55882..835c460a5 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1121,7 +1121,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 @@ -1356,10 +1356,10 @@ class TiffImageFile(ImageFile.ImageFile): # adjust stride width accordingly stride /= bps_count - a = (tile_rawmode, int(stride), 1, offset.offset) + a = (tile_rawmode, int(stride), 1) self.tile.append( ( - "libtiff", + self._compression, (x, y, min(x + w, xsize), min(y + h, ysize)), offset, a, From ddb61cfbd2c39f7a1877dd37403aea0836693a6c Mon Sep 17 00:00:00 2001 From: David Nisson Date: Tue, 16 Apr 2019 22:42:21 -0700 Subject: [PATCH 24/55] updated tests to match new tile descriptors --- Tests/test_file_libtiff.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index dedcc179a..5bbceddbf 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -121,7 +121,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") @@ -661,7 +661,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() @@ -680,7 +681,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() From d00e18b0179142a1b3542deccd9fc6c60c472821 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 23 Jun 2019 14:01:51 +1000 Subject: [PATCH 25/55] Updated test result --- Tests/test_file_libtiff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 5bbceddbf..79955a5af 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -644,10 +644,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), ) ], ) From fd1d779ae15b4e3a9f733628e5ba75ea93d24bfc Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 24 Jun 2019 10:45:53 +0300 Subject: [PATCH 26/55] Strip trailing whitespace --- docs/reference/Image.rst | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 7ef679bb8..1cb0afde4 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -126,14 +126,14 @@ The following script crops the input image with the provided coordinates: .. code-block:: python - from PIL import Image - im = Image.open('cat.jpg') + from PIL import Image + im = Image.open('cat.jpg') # crop method from Image module takes four coordinates as input. # The right can also be represented as (left+width) # and lower can be represented as (upper+height) (left, upper, right, lower) = (200, 20, 520, 260) - + # Here the image "im" is cropped and assiged to new variable im_crop im_crop = im.crop((left, upper, right, lower)) @@ -145,9 +145,9 @@ The following script blurs the input image using a filter from ImageFilter modul .. code-block:: python - from PIL import Image - from PIL import ImageFilter - im = Image.open('cat.jpg') + from PIL import Image + from PIL import ImageFilter + im = Image.open('cat.jpg') # Blur the input image using the filter ImageFilter.BLUR. im_blurred = im.filter(filter=ImageFilter.BLUR) @@ -158,7 +158,7 @@ The following script helps to get the bands of the input image: .. code-block:: python - from PIL import Image + from PIL import Image im = Image.open('cat.jpg') print (im.getbands()) # Returns ('R', 'G', 'B') @@ -168,11 +168,11 @@ The following script helps to get the bounding box coordinates of the input imag .. code-block:: python - from PIL import Image - im = Image.open('cat.jpg') + from PIL import Image + im = Image.open('cat.jpg') print (im.getbbox()) # Returns four coordinates in the format (left, upper, right, lower) - + .. automethod:: PIL.Image.Image.getcolors .. automethod:: PIL.Image.Image.getdata .. automethod:: PIL.Image.Image.getextrema @@ -193,8 +193,8 @@ The following script resizes the given image from (width, height) to (width/2, h .. code-block:: python - from PIL import Image - im = Image.open('cat.jpg') + from PIL import Image + im = Image.open('cat.jpg') # Provide the target width and height of the image (width, height) = (width//2, height//2) @@ -207,13 +207,13 @@ The following script rotates the input image by `theta` degrees counter clockwis .. code-block:: python - from PIL import Image - im = Image.open('cat.jpg') + from PIL import Image + im = Image.open('cat.jpg') # Rotate the image by 60 degrees counter clockwise. theta = 60 # Angle is in degrees counter clockwise. - im_rotated = im.rotate(angle=theta) + im_rotated = im.rotate(angle=theta) .. automethod:: PIL.Image.Image.save .. automethod:: PIL.Image.Image.seek @@ -232,14 +232,14 @@ The following script flips the input image by using the method "Image.FLIP_LEFT_ .. code-block:: python - from PIL import Image - im = Image.open('cat.jpg') + from PIL import Image + im = Image.open('cat.jpg') # Flip the image from left to right - im_flipped = im.transpose(method=Image.FLIP_LEFT_RIGHT) + im_flipped = im.transpose(method=Image.FLIP_LEFT_RIGHT) # To flip the image from top to bottom, # use the method "Image.FLIP_TOP_BOTTOM" - + .. automethod:: PIL.Image.Image.verify From 72bf9f652919434f4730e4adc54e1d6f20f13032 Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 24 Jun 2019 10:48:33 +0300 Subject: [PATCH 27/55] Use the common test-suite image --- docs/reference/Image.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 1cb0afde4..3b2541d69 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -127,7 +127,7 @@ The following script crops the input image with the provided coordinates: .. code-block:: python from PIL import Image - im = Image.open('cat.jpg') + im = Image.open("hopper.jpg") # crop method from Image module takes four coordinates as input. # The right can also be represented as (left+width) @@ -147,7 +147,7 @@ The following script blurs the input image using a filter from ImageFilter modul from PIL import Image from PIL import ImageFilter - im = Image.open('cat.jpg') + im = Image.open("hopper.jpg") # Blur the input image using the filter ImageFilter.BLUR. im_blurred = im.filter(filter=ImageFilter.BLUR) @@ -159,7 +159,7 @@ The following script helps to get the bands of the input image: .. code-block:: python from PIL import Image - im = Image.open('cat.jpg') + im = Image.open("hopper.jpg") print (im.getbands()) # Returns ('R', 'G', 'B') .. automethod:: PIL.Image.Image.getbbox @@ -169,7 +169,7 @@ The following script helps to get the bounding box coordinates of the input imag .. code-block:: python from PIL import Image - im = Image.open('cat.jpg') + im = Image.open("hopper.jpg") print (im.getbbox()) # Returns four coordinates in the format (left, upper, right, lower) @@ -194,7 +194,7 @@ The following script resizes the given image from (width, height) to (width/2, h .. code-block:: python from PIL import Image - im = Image.open('cat.jpg') + im = Image.open("hopper.jpg") # Provide the target width and height of the image (width, height) = (width//2, height//2) @@ -208,7 +208,7 @@ The following script rotates the input image by `theta` degrees counter clockwis .. code-block:: python from PIL import Image - im = Image.open('cat.jpg') + im = Image.open("hopper.jpg") # Rotate the image by 60 degrees counter clockwise. theta = 60 @@ -233,7 +233,7 @@ The following script flips the input image by using the method "Image.FLIP_LEFT_ .. code-block:: python from PIL import Image - im = Image.open('cat.jpg') + im = Image.open("hopper.jpg") # Flip the image from left to right im_flipped = im.transpose(method=Image.FLIP_LEFT_RIGHT) From 73884576d46c9c1fb6c5c66ecbbce63827380a7d Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 24 Jun 2019 11:04:13 +0300 Subject: [PATCH 28/55] Some styling and wording --- docs/reference/Image.rst | 48 ++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 3b2541d69..944a7e88e 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -16,7 +16,7 @@ Open, rotate, and display an image (using the default viewer) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The following script loads an image, rotates it 45 degrees, and displays it -using an external viewer (usually xv on Unix, and the paint program on +using an external viewer (usually xv on Unix, and the Paint program on Windows). .. code-block:: python @@ -116,61 +116,64 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: rgb2xyz = ( 0.412453, 0.357580, 0.180423, 0, 0.212671, 0.715160, 0.072169, 0, - 0.019334, 0.119193, 0.950227, 0 ) + 0.019334, 0.119193, 0.950227, 0) out = im.convert("RGB", rgb2xyz) .. automethod:: PIL.Image.Image.copy .. automethod:: PIL.Image.Image.crop -The following script crops the input image with the provided coordinates: +This crops the input image with the provided coordinates: .. code-block:: python from PIL import Image + im = Image.open("hopper.jpg") - # crop method from Image module takes four coordinates as input. + # The crop method from the Image module takes four coordinates as input. # The right can also be represented as (left+width) - # and lower can be represented as (upper+height) - (left, upper, right, lower) = (200, 20, 520, 260) + # and lower can be represented as (upper+height). + (left, upper, right, lower) = (20, 20, 100, 100) - # Here the image "im" is cropped and assiged to new variable im_crop + # Here the image "im" is cropped and assigned to new variable im_crop im_crop = im.crop((left, upper, right, lower)) .. automethod:: PIL.Image.Image.draft .. automethod:: PIL.Image.Image.filter -The following script blurs the input image using a filter from ImageFilter module: +This blurs the input image using a filter from the ``ImageFilter`` module: .. code-block:: python - from PIL import Image - from PIL import ImageFilter + from PIL import Image, ImageFilter + im = Image.open("hopper.jpg") - # Blur the input image using the filter ImageFilter.BLUR. + # Blur the input image using the filter ImageFilter.BLUR im_blurred = im.filter(filter=ImageFilter.BLUR) .. automethod:: PIL.Image.Image.getbands -The following script helps to get the bands of the input image: +This helps to get the bands of the input image: .. code-block:: python from PIL import Image + im = Image.open("hopper.jpg") - print (im.getbands()) # Returns ('R', 'G', 'B') + print(im.getbands()) # Returns ('R', 'G', 'B') .. automethod:: PIL.Image.Image.getbbox -The following script helps to get the bounding box coordinates of the input image: +This helps to get the bounding box coordinates of the input image: .. code-block:: python from PIL import Image + im = Image.open("hopper.jpg") - print (im.getbbox()) + prin (im.getbbox()) # Returns four coordinates in the format (left, upper, right, lower) .. automethod:: PIL.Image.Image.getcolors @@ -189,30 +192,32 @@ The following script helps to get the bounding box coordinates of the input imag .. automethod:: PIL.Image.Image.quantize .. automethod:: PIL.Image.Image.resize -The following script resizes the given image from (width, height) to (width/2, height/2): +This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``: .. code-block:: python from PIL import Image + im = Image.open("hopper.jpg") # Provide the target width and height of the image - (width, height) = (width//2, height//2) + (width, height) = (im.width // 2, im.height // 2) im_resized = im.resize((width, height)) .. automethod:: PIL.Image.Image.remap_palette .. automethod:: PIL.Image.Image.rotate -The following script rotates the input image by `theta` degrees counter clockwise: +This rotates the input image by ``theta`` degrees counter clockwise: .. code-block:: python from PIL import Image + im = Image.open("hopper.jpg") - # Rotate the image by 60 degrees counter clockwise. + # Rotate the image by 60 degrees counter clockwise theta = 60 - # Angle is in degrees counter clockwise. + # Angle is in degrees counter clockwise im_rotated = im.rotate(angle=theta) .. automethod:: PIL.Image.Image.save @@ -228,11 +233,12 @@ The following script rotates the input image by `theta` degrees counter clockwis .. automethod:: PIL.Image.Image.transform .. automethod:: PIL.Image.Image.transpose -The following script flips the input image by using the method "Image.FLIP_LEFT_RIGHT". +This flips the input image by using the ``Image.FLIP_LEFT_RIGHT`` method. .. code-block:: python from PIL import Image + im = Image.open("hopper.jpg") # Flip the image from left to right From 47f7eba279296ed2977cfd4108ed62ac38c4ce7f Mon Sep 17 00:00:00 2001 From: Christoph Gohlke Date: Tue, 25 Jun 2019 13:33:49 -0700 Subject: [PATCH 29/55] Fix memory leak --- src/_imagingft.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/_imagingft.c b/src/_imagingft.c index f6bd787ef..964ed6ab1 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1006,6 +1006,9 @@ font_render(FontObject* self, PyObject* args) num_coords = PyObject_Length(axes); coords = malloc(2 * sizeof(coords)); + if (coords == NULL) { + return PyErr_NoMemory(); + } for (i = 0; i < num_coords; i++) { item = PyList_GET_ITEM(axes, i); if (PyFloat_Check(item)) @@ -1015,6 +1018,7 @@ font_render(FontObject* self, PyObject* args) else if (PyNumber_Check(item)) coord = PyFloat_AsDouble(item); else { + free(coords); PyErr_SetString(PyExc_TypeError, "list must contain numbers"); return NULL; } @@ -1022,6 +1026,7 @@ font_render(FontObject* self, PyObject* args) } error = FT_Set_Var_Design_Coordinates(self->face, num_coords, coords); + free(coords); if (error) return geterror(error); From a78341e26c7bf5733abe7c984b25e718a15aeb48 Mon Sep 17 00:00:00 2001 From: Christoph Gohlke Date: Tue, 25 Jun 2019 14:23:20 -0700 Subject: [PATCH 30/55] Fix potential null pointer is passed into memcpy --- src/libImaging/TiffDecode.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index a68f73993..871b0bcdb 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -194,6 +194,9 @@ int ReadTile(TIFF* tiff, UINT32 col, UINT32 row, UINT32* buffer) { } swap_line = (UINT32*)malloc(swap_line_size); + if (swap_line == NULL) { + return -1; + } /* * For some reason the TIFFReadRGBATile() function chooses the * lower left corner as the origin. Vertically mirror scanlines. From c34cefb576c6a4ba5acd322dd2cd06b230595d1c Mon Sep 17 00:00:00 2001 From: Christoph Gohlke Date: Tue, 25 Jun 2019 14:41:06 -0700 Subject: [PATCH 31/55] Use ssize_t to store number of elements in buffer --- src/path.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path.c b/src/path.c index eb1e065f9..3eaacb33b 100644 --- a/src/path.c +++ b/src/path.c @@ -133,7 +133,7 @@ PyPath_Flatten(PyObject* data, double **pxy) /* Assume the buffer contains floats */ Py_buffer buffer; if (PyImaging_GetBuffer(data, &buffer) == 0) { - int n = buffer.len / (2 * sizeof(float)); + n = buffer.len / (2 * sizeof(float)); float *ptr = (float*) buffer.buf; xy = alloc_array(n); if (!xy) From afed559e4f179e719da36f732012f7545d6356fe Mon Sep 17 00:00:00 2001 From: Christoph Gohlke Date: Tue, 25 Jun 2019 17:18:37 -0700 Subject: [PATCH 32/55] Fix msvc9 compile error --- src/path.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path.c b/src/path.c index 3eaacb33b..5f0541b0b 100644 --- a/src/path.c +++ b/src/path.c @@ -133,8 +133,8 @@ PyPath_Flatten(PyObject* data, double **pxy) /* Assume the buffer contains floats */ Py_buffer buffer; if (PyImaging_GetBuffer(data, &buffer) == 0) { - n = buffer.len / (2 * sizeof(float)); float *ptr = (float*) buffer.buf; + n = buffer.len / (2 * sizeof(float)); xy = alloc_array(n); if (!xy) return -1; From 37516fb665c2c9f99ba0681a25fa3e2b1caf7deb Mon Sep 17 00:00:00 2001 From: Jayakrishna Menon Date: Fri, 8 Mar 2019 11:30:35 -0700 Subject: [PATCH 33/55] adding an upper limit for blocks_max in _set_blocks_max --- src/_imaging.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/_imaging.c b/src/_imaging.c index 1391ffae6..dffef3689 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3625,6 +3625,12 @@ _set_blocks_max(PyObject* self, PyObject* args) "blocks_max should be greater than 0"); return NULL; } + else if ( blocks_max > SIZE_MAX/sizeof(ImagingDefaultArena.blocks_pool[0])) { + PyErr_SetString(PyExc_ValueError, + "blocks_max is too large"); + return NULL; + } + if ( ! ImagingMemorySetBlocksMax(&ImagingDefaultArena, blocks_max)) { ImagingError_MemoryError(); From 494fb25bdb81b35cf792b7c956d3d631f3bb6102 Mon Sep 17 00:00:00 2001 From: Jayakrishna Menon Date: Fri, 8 Mar 2019 14:23:48 -0700 Subject: [PATCH 34/55] adding testcase for overflow --- Tests/32bit_overflow_check.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Tests/32bit_overflow_check.py diff --git a/Tests/32bit_overflow_check.py b/Tests/32bit_overflow_check.py new file mode 100644 index 000000000..dfe1371f1 --- /dev/null +++ b/Tests/32bit_overflow_check.py @@ -0,0 +1,6 @@ +from PIL import Image +import sys + + +if sys.maxsize < 2**32: + Image.core.set_blocks_max(2**29) From 2acc098cabbbdeabae7688a41ed36dec2cc36d60 Mon Sep 17 00:00:00 2001 From: Jayakrishna Menon Date: Fri, 8 Mar 2019 14:35:03 -0700 Subject: [PATCH 35/55] adding testcase for overflow --- Tests/32bit_overflow_check.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/32bit_overflow_check.py b/Tests/32bit_overflow_check.py index dfe1371f1..3001fec37 100644 --- a/Tests/32bit_overflow_check.py +++ b/Tests/32bit_overflow_check.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + from PIL import Image import sys From 590b5b7f9ef599afd1910b103894e060195b60a2 Mon Sep 17 00:00:00 2001 From: Jayakrishna Menon Date: Mon, 18 Mar 2019 15:13:48 -0700 Subject: [PATCH 36/55] test case for set_blocks_max --- Tests/test_core_resources.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index d5e358f31..cd34a0414 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -105,6 +105,7 @@ class TestCoreMemory(PillowTestCase): Image.new("RGB", (10, 10)) self.assertRaises(ValueError, Image.core.set_blocks_max, -1) + self.assertRaises(ValueError, Image.core.set_blocks_max, 2**29) @unittest.skipIf(is_pypy, "images are not collected") def test_set_blocks_max_stats(self): From d591cf8d3f92f1bd07f8b001b5ed261467f1c2cf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 23 Mar 2019 17:16:43 +1100 Subject: [PATCH 37/55] Error is not raised on 64-bit systems --- Tests/32bit_overflow_check.py | 8 -------- Tests/test_core_resources.py | 3 ++- 2 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 Tests/32bit_overflow_check.py diff --git a/Tests/32bit_overflow_check.py b/Tests/32bit_overflow_check.py deleted file mode 100644 index 3001fec37..000000000 --- a/Tests/32bit_overflow_check.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python - -from PIL import Image -import sys - - -if sys.maxsize < 2**32: - Image.core.set_blocks_max(2**29) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index cd34a0414..f3d8753e1 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -105,7 +105,8 @@ class TestCoreMemory(PillowTestCase): Image.new("RGB", (10, 10)) self.assertRaises(ValueError, Image.core.set_blocks_max, -1) - self.assertRaises(ValueError, Image.core.set_blocks_max, 2**29) + if sys.maxsize < 2**32: + self.assertRaises(ValueError, Image.core.set_blocks_max, 2**29) @unittest.skipIf(is_pypy, "images are not collected") def test_set_blocks_max_stats(self): From f22198e3bc157fd6c56b1660c30a5c9c9d7f4184 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 26 Jun 2019 19:09:02 +1000 Subject: [PATCH 38/55] Lint fixes --- Tests/test_core_resources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index f3d8753e1..c8ba4b4d5 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -105,8 +105,8 @@ class TestCoreMemory(PillowTestCase): Image.new("RGB", (10, 10)) self.assertRaises(ValueError, Image.core.set_blocks_max, -1) - if sys.maxsize < 2**32: - self.assertRaises(ValueError, Image.core.set_blocks_max, 2**29) + if sys.maxsize < 2 ** 32: + self.assertRaises(ValueError, Image.core.set_blocks_max, 2 ** 29) @unittest.skipIf(is_pypy, "images are not collected") def test_set_blocks_max_stats(self): From 7b815a5f1df4e9fb616765b06510bf10464b52e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20B=C3=B6hn?= Date: Fri, 25 Jan 2019 12:38:11 -0500 Subject: [PATCH 39/55] Added an `image.entropy()` method This calculates the entropy for the image, based on the histogram. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because this uses image histogram data directly, the existing C function underpinning the `image.histogram()` method was abstracted into a static function to parse extrema tuple arguments, and a new C function was added to calculate image entropy, making use of the new static extrema function. The extrema-parsing function was written by @homm, based on the macro abstraction I wrote, during the discussion of my first entropy-method pull request: https://git.io/fhodS The new `image.entropy()` method is based on `image.histogram()`, and will accept the same arguments to calculate the histogram data it will use to assess the entropy of the image. The algorithm and methodology is based on existing Python code: * https://git.io/fhmIU ... A test case in the `Tests/` directory, and doctest lines in `selftest.py`, have both been added and checked. Changes proposed in this pull request: * Added “math.h” include to _imaging.c * The addition of an `image.entropy()` method to the `Image` Python class, * The abstraction of the extrema-parsing logic of of the C function `_histogram` into a static function, and * The use of that static function in both the `_histogram` and `_entropy` C functions. * Minor documentation addenda in the docstrings for both the `image.entropy()` and `image.histogram()` methods were also added. * Removed outdated boilerplate from testing code * Removed unused “unittest” import --- Tests/test_image_entropy.py | 19 +++++ selftest.py | 2 + src/PIL/Image.py | 27 +++++++ src/_imaging.c | 144 ++++++++++++++++++++++++++---------- 4 files changed, 152 insertions(+), 40 deletions(-) create mode 100644 Tests/test_image_entropy.py diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py new file mode 100644 index 000000000..1c0efcd80 --- /dev/null +++ b/Tests/test_image_entropy.py @@ -0,0 +1,19 @@ +from .helper import PillowTestCase, hopper + + +class TestImageEntropy(PillowTestCase): + + def test_entropy(self): + + def entropy(mode): + return hopper(mode).entropy() + + self.assertAlmostEqual(entropy("1"), 0.9138803254693582) + self.assertAlmostEqual(entropy("L"), 7.06650513081286) + self.assertAlmostEqual(entropy("I"), 7.06650513081286) + self.assertAlmostEqual(entropy("F"), 7.06650513081286) + self.assertAlmostEqual(entropy("P"), 5.0530452472519745) + self.assertAlmostEqual(entropy("RGB"), 8.821286587714319) + self.assertAlmostEqual(entropy("RGBA"), 7.42724306524488) + self.assertAlmostEqual(entropy("CMYK"), 7.4272430652448795) + self.assertAlmostEqual(entropy("YCbCr"), 7.698360534903628) diff --git a/selftest.py b/selftest.py index 3e4112ffe..4dea3363c 100755 --- a/selftest.py +++ b/selftest.py @@ -90,6 +90,8 @@ def testimage(): 2 >>> len(im.histogram()) 768 + >>> '%.7f' % im.entropy() + '8.8212866' >>> _info(im.point(list(range(256))*3)) (None, 'RGB', (128, 128)) >>> _info(im.resize((64, 64))) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ee7d2c523..973325d8b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1405,6 +1405,7 @@ class Image(object): bi-level image (mode "1") or a greyscale image ("L"). :param mask: An optional mask. + :param extrema: An optional tuple of manually-specified extrema. :returns: A list containing pixel counts. """ self.load() @@ -1417,6 +1418,32 @@ class Image(object): return self.im.histogram(extrema) return self.im.histogram() + def entropy(self, mask=None, extrema=None): + """ + Calculates and returns the entropy for the image. + + A bilevel image (mode "1") is treated as a greyscale ("L") + image by this method. + + If a mask is provided, the method employs the histogram for + those parts of the image where the mask image is non-zero. + The mask image must have the same size as the image, and be + either a bi-level image (mode "1") or a greyscale image ("L"). + + :param mask: An optional mask. + :param extrema: An optional tuple of manually-specified extrema. + :returns: A float value representing the image entropy + """ + self.load() + if mask: + mask.load() + return self.im.entropy((0, 0), mask.im) + if self.mode in ("I", "F"): + if extrema is None: + extrema = self.getextrema() + return self.im.entropy(extrema) + return self.im.entropy() + def offset(self, xoffset, yoffset=None): raise NotImplementedError( "offset() has been removed. Please call ImageChops.offset() instead." diff --git a/src/_imaging.c b/src/_imaging.c index 1391ffae6..cd0c165ce 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -86,6 +86,9 @@ #include "py3.h" +#define _USE_MATH_DEFINES +#include + /* Configuration stuff. Feel free to undef things you don't need. */ #define WITH_IMAGECHOPS /* ImageChops support */ #define WITH_IMAGEDRAW /* ImageDraw support */ @@ -1176,59 +1179,68 @@ _getpixel(ImagingObject* self, PyObject* args) return getpixel(self->image, self->access, x, y); } +union hist_extrema { + UINT8 u[2]; + INT32 i[2]; + FLOAT32 f[2]; +}; + +static union hist_extrema* +parse_histogram_extremap(ImagingObject* self, PyObject* extremap, + union hist_extrema* ep) +{ + int i0, i1; + double f0, f1; + + if (extremap) { + switch (self->image->type) { + case IMAGING_TYPE_UINT8: + if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) + return NULL; + ep->u[0] = CLIP8(i0); + ep->u[1] = CLIP8(i1); + break; + case IMAGING_TYPE_INT32: + if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) + return NULL; + ep->i[0] = i0; + ep->i[1] = i1; + break; + case IMAGING_TYPE_FLOAT32: + if (!PyArg_ParseTuple(extremap, "dd", &f0, &f1)) + return NULL; + ep->f[0] = (FLOAT32) f0; + ep->f[1] = (FLOAT32) f1; + break; + default: + return NULL; + } + } else { + return NULL; + } + return ep; +} + static PyObject* _histogram(ImagingObject* self, PyObject* args) { ImagingHistogram h; PyObject* list; int i; - union { - UINT8 u[2]; - INT32 i[2]; - FLOAT32 f[2]; - } extrema; - void* ep; - int i0, i1; - double f0, f1; + union hist_extrema extrema; + union hist_extrema* ep; PyObject* extremap = NULL; ImagingObject* maskp = NULL; if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) - return NULL; - - if (extremap) { - ep = &extrema; - switch (self->image->type) { - case IMAGING_TYPE_UINT8: - if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) - return NULL; - /* FIXME: clip */ - extrema.u[0] = i0; - extrema.u[1] = i1; - break; - case IMAGING_TYPE_INT32: - if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) - return NULL; - extrema.i[0] = i0; - extrema.i[1] = i1; - break; - case IMAGING_TYPE_FLOAT32: - if (!PyArg_ParseTuple(extremap, "dd", &f0, &f1)) - return NULL; - extrema.f[0] = (FLOAT32) f0; - extrema.f[1] = (FLOAT32) f1; - break; - default: - ep = NULL; - break; - } - } else - ep = NULL; + return NULL; + /* Using a var to avoid allocations. */ + ep = parse_histogram_extremap(self, extremap, &extrema); h = ImagingGetHistogram(self->image, (maskp) ? maskp->image : NULL, ep); if (!h) - return NULL; + return NULL; /* Build an integer list containing the histogram */ list = PyList_New(h->bands * 256); @@ -1243,11 +1255,63 @@ _histogram(ImagingObject* self, PyObject* args) PyList_SetItem(list, i, item); } + /* Destroy the histogram structure */ ImagingHistogramDelete(h); return list; } +static PyObject* +_entropy(ImagingObject* self, PyObject* args) +{ + ImagingHistogram h; + PyObject* entropy; + int idx, length; + long sum; + double fentropy, fsum, p; + union hist_extrema extrema; + union hist_extrema* ep; + + PyObject* extremap = NULL; + ImagingObject* maskp = NULL; + if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) + return NULL; + + /* Using a local var to avoid allocations. */ + ep = parse_histogram_extremap(self, extremap, &extrema); + h = ImagingGetHistogram(self->image, (maskp) ? maskp->image : NULL, ep); + + if (!h) + return NULL; + + /* Calculate the histogram entropy */ + /* First, sum the histogram data */ + length = h->bands * 256; + sum = 0; + for (idx = 0; idx < length; idx++) { + sum += h->histogram[idx]; + } + + /* Next, normalize the histogram data, */ + /* using the histogram sum value */ + fsum = (double)sum; + fentropy = 0.0; + for (idx = 0; idx < length; idx++) { + p = (double)h->histogram[idx] / fsum; + if (p != 0.0) { + fentropy += p * log(p) * M_LOG2E; + } + } + + /* Finally, allocate a PyObject* for return */ + entropy = PyFloat_FromDouble(-fentropy); + + /* Destroy the histogram structure */ + ImagingHistogramDelete(h); + + return entropy; +} + #ifdef WITH_MODEFILTER static PyObject* _modefilter(ImagingObject* self, PyObject* args) @@ -3193,6 +3257,7 @@ static struct PyMethodDef methods[] = { {"expand", (PyCFunction)_expand_image, 1}, {"filter", (PyCFunction)_filter, 1}, {"histogram", (PyCFunction)_histogram, 1}, + {"entropy", (PyCFunction)_entropy, 1}, #ifdef WITH_MODEFILTER {"modefilter", (PyCFunction)_modefilter, 1}, #endif @@ -3912,4 +3977,3 @@ init_imaging(void) setup_module(m); } #endif - From 1a71faf8ea81879ff730e939ad5d0d1a468ec469 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Mar 2019 22:04:51 +1100 Subject: [PATCH 40/55] Removed variable --- src/_imaging.c | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index cd0c165ce..60eed7af6 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1265,10 +1265,9 @@ static PyObject* _entropy(ImagingObject* self, PyObject* args) { ImagingHistogram h; - PyObject* entropy; int idx, length; long sum; - double fentropy, fsum, p; + double entropy, fsum, p; union hist_extrema extrema; union hist_extrema* ep; @@ -1295,21 +1294,18 @@ _entropy(ImagingObject* self, PyObject* args) /* Next, normalize the histogram data, */ /* using the histogram sum value */ fsum = (double)sum; - fentropy = 0.0; + entropy = 0.0; for (idx = 0; idx < length; idx++) { p = (double)h->histogram[idx] / fsum; if (p != 0.0) { - fentropy += p * log(p) * M_LOG2E; + entropy += p * log(p) * M_LOG2E; } } - /* Finally, allocate a PyObject* for return */ - entropy = PyFloat_FromDouble(-fentropy); - /* Destroy the histogram structure */ ImagingHistogramDelete(h); - return entropy; + return PyFloat_FromDouble(-entropy); } #ifdef WITH_MODEFILTER From 8b447c4840487bc4fa1995f7955cfe32a83d81da Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 26 Jun 2019 19:16:36 +1000 Subject: [PATCH 41/55] Lint fixes --- Tests/test_image_entropy.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py index 1c0efcd80..bc792bca6 100644 --- a/Tests/test_image_entropy.py +++ b/Tests/test_image_entropy.py @@ -2,9 +2,7 @@ from .helper import PillowTestCase, hopper class TestImageEntropy(PillowTestCase): - def test_entropy(self): - def entropy(mode): return hopper(mode).entropy() From e319e32cec162a17720149321842056756270e11 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 27 Jun 2019 22:53:46 +1000 Subject: [PATCH 42/55] Improved parameter documentation [ci skip] --- src/PIL/_binary.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index 80c86dab8..e5ee0bf28 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -37,8 +37,8 @@ def i16le(c, o=0): """ Converts a 2-bytes (16 bits) string to an unsigned integer. - c: string containing bytes to convert - o: offset of bytes to convert in string + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string """ return unpack_from(" Date: Thu, 27 Jun 2019 15:07:52 -0700 Subject: [PATCH 43/55] Use unsigned int to store TIFF IFD offsets --- src/decode.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/decode.c b/src/decode.c index 10e740aaf..79133f48f 100644 --- a/src/decode.c +++ b/src/decode.c @@ -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)); From 129df60c4844224b949c202333b4133acd7c9942 Mon Sep 17 00:00:00 2001 From: Christoph Gohlke Date: Thu, 27 Jun 2019 15:09:31 -0700 Subject: [PATCH 44/55] Use unsigned int to store TIFF IFD offsets --- src/libImaging/TiffDecode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 871b0bcdb..50bc2bfc2 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -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")); From 59e1328272a154685fc69aca3df6e8e4cdc4637e Mon Sep 17 00:00:00 2001 From: Christoph Gohlke Date: Thu, 27 Jun 2019 15:30:19 -0700 Subject: [PATCH 45/55] Use unsigned int to store TIFF IFD offsets --- src/libImaging/TiffDecode.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h index e29a6c88f..1c1337715 100644 --- a/src/libImaging/TiffDecode.h +++ b/src/libImaging/TiffDecode.h @@ -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); extern int ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); From 57e3af4afbdfa37496a479409a0a0b9ca46f991c Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 28 Jun 2019 18:47:17 +0300 Subject: [PATCH 46/55] Fix typo --- docs/reference/Image.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 944a7e88e..81abe17b8 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -173,7 +173,7 @@ This helps to get the bounding box coordinates of the input image: from PIL import Image im = Image.open("hopper.jpg") - prin (im.getbbox()) + print(im.getbbox()) # Returns four coordinates in the format (left, upper, right, lower) .. automethod:: PIL.Image.Image.getcolors From 07ec99d545c15dae5e4597b4b7e156b9907c22c5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Jun 2019 08:03:40 +1000 Subject: [PATCH 47/55] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 599eaa345..fcddd8da0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 6.1.0 (unreleased) ------------------ +- Fixed crash when loading non-font bytes #3912 + [radarhere] + +- Fix SPARC memory alignment issues in Pack/Unpack functions #3858 + [kulikjak] + +- Added CMYK;16B and CMYK;16N unpackers #3913 + [radarhere] + - Fixed bugs in calculating text size #3864 [radarhere] From 6940c1142c6dec3cfc7fb7e4f3012b9c3f61ada4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Jun 2019 15:47:46 +1000 Subject: [PATCH 48/55] Split pytest flags --- .travis/script.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis/script.sh b/.travis/script.sh index d6e02f01d..ae2e8aad4 100755 --- a/.travis/script.sh +++ b/.travis/script.sh @@ -7,7 +7,7 @@ make clean make install-coverage python selftest.py -python -m pytest -vx --cov PIL --cov-report term Tests +python -m pytest -v -x --cov PIL --cov-report term Tests pushd /tmp/check-manifest && check-manifest --ignore ".coveragerc,.editorconfig,*.yml,*.yaml,tox.ini" && popd From 1abcada1c52c041347ac1882d66bf530ad645e73 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Jun 2019 17:47:55 +1000 Subject: [PATCH 49/55] Added release notes [ci skip] --- docs/releasenotes/6.1.0.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst index 98568fb52..783c4d323 100644 --- a/docs/releasenotes/6.1.0.rst +++ b/docs/releasenotes/6.1.0.rst @@ -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 used to return all frames from an image, each rotated 90 degrees. + Variation fonts ^^^^^^^^^^^^^^^ From d2fdb4818c8e51e683f0f038846bd44882c815f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 29 Jun 2019 18:02:08 +1000 Subject: [PATCH 50/55] Fixed wording Co-Authored-By: Hugo --- docs/releasenotes/6.1.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst index 783c4d323..851dcb2d0 100644 --- a/docs/releasenotes/6.1.0.rst +++ b/docs/releasenotes/6.1.0.rst @@ -17,7 +17,7 @@ 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 used to return all frames from an image, each rotated 90 degrees. +could be used to return all frames from an image, each rotated 90 degrees. Variation fonts ^^^^^^^^^^^^^^^ From 3e4db05249dd4d409243668bcd377034bf40abc8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Jun 2019 23:02:17 +1000 Subject: [PATCH 51/55] Removed code not required by tests --- src/PIL/GifImagePlugin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index d6d646c59..d541483b8 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -471,9 +471,6 @@ def _write_multiple_frames(im, fp, palette): if len(im_frames) > 1: for frame_data in im_frames: im_frame = frame_data["im"] - if("disposal" in frame_data["encoderinfo"] - and frame_data["encoderinfo"]["disposal"] == 2): - frame_data['encoderinfo']['include_color_table'] = True if not frame_data["bbox"]: # global header for s in _get_global_header(im_frame, frame_data["encoderinfo"]): From 97c15a245c603f615417f28a9e4b31b06fe9060a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Jun 2019 19:24:12 +1000 Subject: [PATCH 52/55] Corrected color table size calculation --- Tests/test_file_gif.py | 2 +- src/PIL/GifImagePlugin.py | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 1f0358bec..0c4a13a5b 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -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) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index d541483b8..f55ca4471 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -580,9 +580,6 @@ def _write_local_header(fp, im, offset, flags): include_color_table = im.encoderinfo.get("include_color_table") if include_color_table: palette_bytes = _get_palette_bytes(im) - # If needed, expand palette to minimum size - while(len(palette_bytes) < 9): - palette_bytes = palette_bytes*2 color_table_size = _get_color_table_size(palette_bytes) if color_table_size: flags = flags | 128 # local color table flag @@ -693,10 +690,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): From 90d3d3716457c1bf69fd4814759350fc46383c0d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Jun 2019 23:06:45 +1000 Subject: [PATCH 53/55] Do not presume that the background color index is 0 --- Tests/test_file_gif.py | 52 +++++++++++++++++++++++++-------------- src/PIL/GifImagePlugin.py | 48 ++++++++++++++++++++++-------------- 2 files changed, 63 insertions(+), 37 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 0c4a13a5b..45409cbc6 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -319,14 +319,14 @@ class TestFileGif(PillowTestCase): self.assertEqual(img.disposal_method, i + 1) def test_dispose2_palette(self): - out = self.tempfile('temp.gif') + 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)) + img = Image.new("RGB", (100, 100), (255, 0, 0)) # Red circle in center of each frame d = ImageDraw.Draw(img) @@ -334,18 +334,13 @@ class TestFileGif(PillowTestCase): im_list.append(img) - im_list[0].save( - out, - save_all=True, - append_images=im_list[1:], - disposal=2 - ) + 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') + rgb_img = img.convert("RGB") # Check top left pixel matches background self.assertEqual(rgb_img.getpixel((0, 0)), (255, 0, 0)) @@ -354,20 +349,20 @@ class TestFileGif(PillowTestCase): self.assertEqual(rgb_img.getpixel((50, 50)), circle) def test_dispose2_diff(self): - out = self.tempfile('temp.gif') + 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)) + ((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)) + img = Image.new("RGBA", (100, 100), (255, 255, 255, 0)) # Two circles per frame d = ImageDraw.Draw(img) @@ -377,18 +372,14 @@ class TestFileGif(PillowTestCase): im_list.append(img) im_list[0].save( - out, - save_all=True, - append_images=im_list[1:], - disposal=2, - transparency=0 + 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') + rgb_img = img.convert("RGBA") # Check left circle is correct colour self.assertEqual(rgb_img.getpixel((20, 50)), colours[0]) @@ -399,6 +390,31 @@ class TestFileGif(PillowTestCase): # 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 diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index f55ca4471..bbf1c603f 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -426,9 +426,8 @@ def _write_multiple_frames(im, fp, palette): im_frames = [] frame_count = 0 - background = None - for imSequence in itertools.chain([im], - im.encoderinfo.get("append_images", [])): + 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 im_frame = _normalize_mode(im_frame.copy()) @@ -447,16 +446,23 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if disposal == 2: - base_image = background + 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_image = previous["im"] - - if _get_palette_bytes(im_frame) == _get_palette_bytes(base_frame): - delta = ImageChops.subtract_modulo(im_frame, base_image) + 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"), base_image.convert("RGB")) + im_frame.convert("RGB"), base_im.convert("RGB") + ) bbox = delta.getbbox() if not bbox: # This frame is identical to the previous frame @@ -465,7 +471,6 @@ def _write_multiple_frames(im, fp, palette): continue else: bbox = None - background = Image.new("P", im_frame.size, 0) im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) if len(im_frames) > 1: @@ -726,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""" @@ -745,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) From a7bdd6487fb6f7358aa47b625fe34b89782807cc Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 29 Jun 2019 23:09:35 +0300 Subject: [PATCH 54/55] Update CHANGES.rst --- CHANGES.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fcddd8da0..3789879e9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,27 @@ Changelog (Pillow) 6.1.0 (unreleased) ------------------ +- Create GIF deltas from background colour of GIF frames if disposal mode is 2 #3708 + [sircinnamon, radarhere] + +- Added ImageSequence all_frames #3778 + [radargere] + +- 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] From d11aa4b21da1abe6cb0407923112015e15c348de Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 30 Jun 2019 07:58:11 +1000 Subject: [PATCH 55/55] Fixed typo [ci skip] --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3789879e9..03c878dc9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,7 +9,7 @@ Changelog (Pillow) [sircinnamon, radarhere] - Added ImageSequence all_frames #3778 - [radargere] + [radarhere] - Use unsigned int to store TIFF IFD offsets #3923 [cgohlke]