from __future__ import division, print_function from contextlib import contextmanager from helper import unittest, PillowTestCase, hopper from PIL import Image, ImageDraw class TestImagingResampleVulnerability(PillowTestCase): # see https://github.com/python-pillow/Pillow/issues/1710 def test_overflow(self): im = hopper('L') xsize = 0x100000008 // 4 ysize = 1000 # unimportant with self.assertRaises(MemoryError): # any resampling filter will do here im.im.resize((xsize, ysize), Image.BILINEAR) def test_invalid_size(self): im = hopper() # Should not crash im.resize((100, 100)) with self.assertRaises(ValueError): im.resize((-100, 100)) with self.assertRaises(ValueError): im.resize((100, -100)) def test_modify_after_resizing(self): im = hopper('RGB') # get copy with same size copy = im.resize(im.size) # some in-place operation copy.paste('black', (0, 0, im.width // 2, im.height // 2)) # image should be different self.assertNotEqual(im.tobytes(), copy.tobytes()) class TestImagingCoreResampleAccuracy(PillowTestCase): def make_case(self, mode, size, color): """Makes a sample image with two dark and two bright squares. For example: e0 e0 1f 1f e0 e0 1f 1f 1f 1f e0 e0 1f 1f e0 e0 """ case = Image.new('L', size, 255 - color) rectangle = ImageDraw.Draw(case).rectangle rectangle((0, 0, size[0] // 2 - 1, size[1] // 2 - 1), color) rectangle((size[0] // 2, size[1] // 2, size[0], size[1]), color) return Image.merge(mode, [case] * len(mode)) def make_sample(self, data, size): """Restores a sample image from given data string which contains hex-encoded pixels from the top left fourth of a sample. """ data = data.replace(' ', '') sample = Image.new('L', size) s_px = sample.load() w, h = size[0] // 2, size[1] // 2 for y in range(h): for x in range(w): val = int(data[(y * w + x) * 2:(y * w + x + 1) * 2], 16) s_px[x, y] = val s_px[size[0] - x - 1, size[1] - y - 1] = val s_px[x, size[1] - y - 1] = 255 - val s_px[size[0] - x - 1, y] = 255 - val return sample def check_case(self, case, sample): s_px = sample.load() c_px = case.load() for y in range(case.size[1]): for x in range(case.size[0]): if c_px[x, y] != s_px[x, y]: message = '\nHave: \n{}\n\nExpected: \n{}'.format( self.serialize_image(case), self.serialize_image(sample), ) self.assertEqual(s_px[x, y], c_px[x, y], message) def serialize_image(self, image): s_px = image.load() return '\n'.join( ' '.join( '{:02x}'.format(s_px[x, y]) for x in range(image.size[0]) ) for y in range(image.size[1]) ) def test_reduce_box(self): for mode in ['RGBX', 'RGB', 'La', 'L']: case = self.make_case(mode, (8, 8), 0xe1) case = case.resize((4, 4), Image.BOX) data = ('e1 e1' 'e1 e1') for channel in case.split(): self.check_case(channel, self.make_sample(data, (4, 4))) def test_reduce_bilinear(self): for mode in ['RGBX', 'RGB', 'La', 'L']: case = self.make_case(mode, (8, 8), 0xe1) case = case.resize((4, 4), Image.BILINEAR) data = ('e1 c9' 'c9 b7') for channel in case.split(): self.check_case(channel, self.make_sample(data, (4, 4))) def test_reduce_hamming(self): for mode in ['RGBX', 'RGB', 'La', 'L']: case = self.make_case(mode, (8, 8), 0xe1) case = case.resize((4, 4), Image.HAMMING) data = ('e1 da' 'da d3') for channel in case.split(): self.check_case(channel, self.make_sample(data, (4, 4))) def test_reduce_bicubic(self): for mode in ['RGBX', 'RGB', 'La', 'L']: case = self.make_case(mode, (12, 12), 0xe1) case = case.resize((6, 6), Image.BICUBIC) data = ('e1 e3 d4' 'e3 e5 d6' 'd4 d6 c9') for channel in case.split(): self.check_case(channel, self.make_sample(data, (6, 6))) def test_reduce_lanczos(self): for mode in ['RGBX', 'RGB', 'La', 'L']: case = self.make_case(mode, (16, 16), 0xe1) case = case.resize((8, 8), Image.LANCZOS) data = ('e1 e0 e4 d7' 'e0 df e3 d6' 'e4 e3 e7 da' 'd7 d6 d9 ce') for channel in case.split(): self.check_case(channel, self.make_sample(data, (8, 8))) def test_enlarge_box(self): for mode in ['RGBX', 'RGB', 'La', 'L']: case = self.make_case(mode, (2, 2), 0xe1) case = case.resize((4, 4), Image.BOX) data = ('e1 e1' 'e1 e1') for channel in case.split(): self.check_case(channel, self.make_sample(data, (4, 4))) def test_enlarge_bilinear(self): for mode in ['RGBX', 'RGB', 'La', 'L']: case = self.make_case(mode, (2, 2), 0xe1) case = case.resize((4, 4), Image.BILINEAR) data = ('e1 b0' 'b0 98') for channel in case.split(): self.check_case(channel, self.make_sample(data, (4, 4))) def test_enlarge_hamming(self): for mode in ['RGBX', 'RGB', 'La', 'L']: case = self.make_case(mode, (2, 2), 0xe1) case = case.resize((4, 4), Image.HAMMING) data = ('e1 d2' 'd2 c5') for channel in case.split(): self.check_case(channel, self.make_sample(data, (4, 4))) def test_enlarge_bicubic(self): for mode in ['RGBX', 'RGB', 'La', 'L']: case = self.make_case(mode, (4, 4), 0xe1) case = case.resize((8, 8), Image.BICUBIC) data = ('e1 e5 ee b9' 'e5 e9 f3 bc' 'ee f3 fd c1' 'b9 bc c1 a2') for channel in case.split(): self.check_case(channel, self.make_sample(data, (8, 8))) def test_enlarge_lanczos(self): for mode in ['RGBX', 'RGB', 'La', 'L']: case = self.make_case(mode, (6, 6), 0xe1) case = case.resize((12, 12), Image.LANCZOS) data = ('e1 e0 db ed f5 b8' 'e0 df da ec f3 b7' 'db db d6 e7 ee b5' 'ed ec e6 fb ff bf' 'f5 f4 ee ff ff c4' 'b8 b7 b4 bf c4 a0') for channel in case.split(): self.check_case(channel, self.make_sample(data, (12, 12))) class CoreResampleConsistencyTest(PillowTestCase): def make_case(self, mode, fill): im = Image.new(mode, (512, 9), fill) return (im.resize((9, 512), Image.LANCZOS), im.load()[0, 0]) def run_case(self, case): channel, color = case px = channel.load() for x in range(channel.size[0]): for y in range(channel.size[1]): if px[x, y] != color: message = "{} != {} for pixel {}".format( px[x, y], color, (x, y)) self.assertEqual(px[x, y], color, message) def test_8u(self): im, color = self.make_case('RGB', (0, 64, 255)) r, g, b = im.split() self.run_case((r, color[0])) self.run_case((g, color[1])) self.run_case((b, color[2])) self.run_case(self.make_case('L', 12)) def test_32i(self): self.run_case(self.make_case('I', 12)) self.run_case(self.make_case('I', 0x7fffffff)) self.run_case(self.make_case('I', -12)) self.run_case(self.make_case('I', -1 << 31)) def test_32f(self): self.run_case(self.make_case('F', 1)) self.run_case(self.make_case('F', 3.40282306074e+38)) self.run_case(self.make_case('F', 1.175494e-38)) self.run_case(self.make_case('F', 1.192093e-07)) class CoreResampleAlphaCorrectTest(PillowTestCase): def make_levels_case(self, mode): i = Image.new(mode, (256, 16)) px = i.load() for y in range(i.size[1]): for x in range(i.size[0]): pix = [x] * len(mode) pix[-1] = 255 - y * 16 px[x, y] = tuple(pix) return i def run_levels_case(self, i): px = i.load() for y in range(i.size[1]): used_colors = {px[x, y][0] for x in range(i.size[0])} self.assertEqual(256, len(used_colors), 'All colors should present in resized image. ' 'Only {} on {} line.'.format(len(used_colors), y)) @unittest.skip("current implementation isn't precise enough") def test_levels_rgba(self): case = self.make_levels_case('RGBA') self.run_levels_case(case.resize((512, 32), Image.BOX)) self.run_levels_case(case.resize((512, 32), Image.BILINEAR)) self.run_levels_case(case.resize((512, 32), Image.HAMMING)) self.run_levels_case(case.resize((512, 32), Image.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.LANCZOS)) @unittest.skip("current implementation isn't precise enough") def test_levels_la(self): case = self.make_levels_case('LA') self.run_levels_case(case.resize((512, 32), Image.BOX)) self.run_levels_case(case.resize((512, 32), Image.BILINEAR)) self.run_levels_case(case.resize((512, 32), Image.HAMMING)) self.run_levels_case(case.resize((512, 32), Image.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.LANCZOS)) def make_dirty_case(self, mode, clean_pixel, dirty_pixel): i = Image.new(mode, (64, 64), dirty_pixel) px = i.load() xdiv4 = i.size[0] // 4 ydiv4 = i.size[1] // 4 for y in range(ydiv4 * 2): for x in range(xdiv4 * 2): px[x + xdiv4, y + ydiv4] = clean_pixel return i def run_dirty_case(self, i, clean_pixel): px = i.load() for y in range(i.size[1]): for x in range(i.size[0]): if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel: message = 'pixel at ({}, {}) is differ:\n{}\n{}'\ .format(x, y, px[x, y], clean_pixel) self.assertEqual(px[x, y][:3], clean_pixel, message) def test_dirty_pixels_rgba(self): case = self.make_dirty_case('RGBA', (255, 255, 0, 128), (0, 0, 255, 0)) self.run_dirty_case(case.resize((20, 20), Image.BOX), (255, 255, 0)) self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), (255, 255, 0)) self.run_dirty_case(case.resize((20, 20), Image.HAMMING), (255, 255, 0)) self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), (255, 255, 0)) self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255, 255, 0)) def test_dirty_pixels_la(self): case = self.make_dirty_case('LA', (255, 128), (0, 0)) self.run_dirty_case(case.resize((20, 20), Image.BOX), (255,)) self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), (255,)) self.run_dirty_case(case.resize((20, 20), Image.HAMMING), (255,)) self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), (255,)) self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255,)) class CoreResamplePassesTest(PillowTestCase): @contextmanager def count(self, diff): count = Image.core.get_stats()['new_count'] yield self.assertEqual(Image.core.get_stats()['new_count'] - count, diff) def test_horizontal(self): im = hopper('L') with self.count(1): im.resize((im.size[0] - 10, im.size[1]), Image.BILINEAR) def test_vertical(self): im = hopper('L') with self.count(1): im.resize((im.size[0], im.size[1] - 10), Image.BILINEAR) def test_both(self): im = hopper('L') with self.count(2): im.resize((im.size[0] - 10, im.size[1] - 10), Image.BILINEAR) def test_box_horizontal(self): im = hopper('L') box = (20, 0, im.size[0] - 20, im.size[1]) with self.count(1): # the same size, but different box with_box = im.resize(im.size, Image.BILINEAR, box) with self.count(2): cropped = im.crop(box).resize(im.size, Image.BILINEAR) self.assert_image_similar(with_box, cropped, 0.1) def test_box_vertical(self): im = hopper('L') box = (0, 20, im.size[0], im.size[1] - 20) with self.count(1): # the same size, but different box with_box = im.resize(im.size, Image.BILINEAR, box) with self.count(2): cropped = im.crop(box).resize(im.size, Image.BILINEAR) self.assert_image_similar(with_box, cropped, 0.1) class CoreResampleCoefficientsTest(PillowTestCase): def test_reduce(self): test_color = 254 # print() for size in range(400000, 400010, 2): # print(size) i = Image.new('L', (size, 1), 0) draw = ImageDraw.Draw(i) draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color) px = i.resize((5, i.size[1]), Image.BICUBIC).load() if px[2, 0] != test_color // 2: self.assertEqual(test_color // 2, px[2, 0]) # print('>', size, test_color // 2, px[2, 0]) def test_nonzero_coefficients(self): # regression test for the wrong coefficients calculation # due to bug https://github.com/python-pillow/Pillow/issues/2161 im = Image.new('RGBA', (1280, 1280), (0x20, 0x40, 0x60, 0xff)) histogram = im.resize((256, 256), Image.BICUBIC).histogram() self.assertEqual(histogram[0x100 * 0 + 0x20], 0x10000) # first channel self.assertEqual(histogram[0x100 * 1 + 0x40], 0x10000) # second channel self.assertEqual(histogram[0x100 * 2 + 0x60], 0x10000) # third channel self.assertEqual(histogram[0x100 * 3 + 0xff], 0x10000) # fourth channel class CoreResampleBoxTest(PillowTestCase): def test_wrong_arguments(self): im = hopper() for resample in (Image.NEAREST, Image.BOX, Image.BILINEAR, Image.HAMMING, Image.BICUBIC, Image.LANCZOS): im.resize((32, 32), resample, (0, 0, im.width, im.height)) im.resize((32, 32), resample, (20, 20, im.width, im.height)) im.resize((32, 32), resample, (20, 20, 20, 100)) im.resize((32, 32), resample, (20, 20, 100, 20)) with self.assertRaisesRegexp(TypeError, "must be sequence of length 4"): im.resize((32, 32), resample, (im.width, im.height)) with self.assertRaisesRegexp(ValueError, "can't be negative"): im.resize((32, 32), resample, (-20, 20, 100, 100)) with self.assertRaisesRegexp(ValueError, "can't be negative"): im.resize((32, 32), resample, (20, -20, 100, 100)) with self.assertRaisesRegexp(ValueError, "can't be empty"): im.resize((32, 32), resample, (20.1, 20, 20, 100)) with self.assertRaisesRegexp(ValueError, "can't be empty"): im.resize((32, 32), resample, (20, 20.1, 100, 20)) with self.assertRaisesRegexp(ValueError, "can't be empty"): im.resize((32, 32), resample, (20.1, 20.1, 20, 20)) with self.assertRaisesRegexp(ValueError, "can't exceed"): im.resize((32, 32), resample, (0, 0, im.width + 1, im.height)) with self.assertRaisesRegexp(ValueError, "can't exceed"): im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) def resize_tiled(self, im, dst_size, xtiles, ytiles): def split_range(size, tiles): scale = size / tiles for i in range(tiles): yield (int(round(scale * i)), int(round(scale * (i + 1)))) tiled = Image.new(im.mode, dst_size) scale = (im.size[0] / tiled.size[0], im.size[1] / tiled.size[1]) for y0, y1 in split_range(dst_size[1], ytiles): for x0, x1 in split_range(dst_size[0], xtiles): box = (x0 * scale[0], y0 * scale[1], x1 * scale[0], y1 * scale[1]) tile = im.resize((x1 - x0, y1 - y0), Image.BICUBIC, box) tiled.paste(tile, (x0, y0)) return tiled def test_tiles(self): im = Image.open("Tests/images/flower.jpg") assert im.size == (480, 360) dst_size = (251, 188) reference = im.resize(dst_size, Image.BICUBIC) for tiles in [(1, 1), (3, 3), (9, 7), (100, 100)]: tiled = self.resize_tiled(im, dst_size, *tiles) self.assert_image_similar(reference, tiled, 0.01) def test_subsample(self): # This test shows advantages of the subpixel resizing # after supersampling (e.g. during JPEG decoding). im = Image.open("Tests/images/flower.jpg") assert im.size == (480, 360) dst_size = (48, 36) # Reference is cropped image resized to destination reference = im.crop((0, 0, 473, 353)).resize(dst_size, Image.BICUBIC) # Image.BOX emulates supersampling (480 / 8 = 60, 360 / 8 = 45) supersampled = im.resize((60, 45), Image.BOX) with_box = supersampled.resize(dst_size, Image.BICUBIC, (0, 0, 59.125, 44.125)) without_box = supersampled.resize(dst_size, Image.BICUBIC) # error with box should be much smaller than without self.assert_image_similar(reference, with_box, 6) with self.assertRaisesRegexp(AssertionError, "difference 29\."): self.assert_image_similar(reference, without_box, 5) def test_formats(self): for resample in [Image.NEAREST, Image.BILINEAR]: for mode in ['RGB', 'L', 'RGBA', 'LA', 'I', '']: im = hopper(mode) box = (20, 20, im.size[0] - 20, im.size[1] - 20) with_box = im.resize((32, 32), resample, box) cropped = im.crop(box).resize((32, 32), resample) self.assert_image_similar(cropped, with_box, 0.4) def test_passthrough(self): # When no resize is required im = hopper() for size, box in [ ((40, 50), (0, 0, 40, 50)), ((40, 50), (0, 10, 40, 60)), ((40, 50), (10, 0, 50, 50)), ((40, 50), (10, 20, 50, 70)), ]: try: res = im.resize(size, Image.LANCZOS, box) self.assertEqual(res.size, size) self.assert_image_equal(res, im.crop(box)) except AssertionError: print('>>>', size, box) raise def test_no_passthrough(self): # When resize is required im = hopper() for size, box in [ ((40, 50), (0.4, 0.4, 40.4, 50.4)), ((40, 50), (0.4, 10.4, 40.4, 60.4)), ((40, 50), (10.4, 0.4, 50.4, 50.4)), ((40, 50), (10.4, 20.4, 50.4, 70.4)), ]: try: res = im.resize(size, Image.LANCZOS, box) self.assertEqual(res.size, size) with self.assertRaisesRegexp(AssertionError, "difference \d"): # check that the difference at least that much self.assert_image_similar(res, im.crop(box), 20) except AssertionError: print('>>>', size, box) raise def test_skip_horizontal(self): # Can skip resize for one dimension im = hopper() for flt in [Image.NEAREST, Image.BICUBIC]: for size, box in [ ((40, 50), (0, 0, 40, 90)), ((40, 50), (0, 20, 40, 90)), ((40, 50), (10, 0, 50, 90)), ((40, 50), (10, 20, 50, 90)), ]: try: res = im.resize(size, flt, box) self.assertEqual(res.size, size) # Borders should be slightly different self.assert_image_similar( res, im.crop(box).resize(size, flt), 0.4) except AssertionError: print('>>>', size, box, flt) raise def test_skip_vertical(self): # Can skip resize for one dimension im = hopper() for flt in [Image.NEAREST, Image.BICUBIC]: for size, box in [ ((40, 50), (0, 0, 90, 50)), ((40, 50), (20, 0, 90, 50)), ((40, 50), (0, 10, 90, 60)), ((40, 50), (20, 10, 90, 60)), ]: try: res = im.resize(size, flt, box) self.assertEqual(res.size, size) # Borders should be slightly different self.assert_image_similar( res, im.crop(box).resize(size, flt), 0.4) except AssertionError: print('>>>', size, box, flt) raise if __name__ == '__main__': unittest.main()