mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-02-03 21:24:31 +03:00
Merge pull request #2382 from hugovk/test-effects
Test linear and radial gradient effects
This commit is contained in:
commit
9c4eafc188
63
PIL/Image.py
63
PIL/Image.py
|
@ -144,6 +144,7 @@ def isImageType(t):
|
||||||
"""
|
"""
|
||||||
return hasattr(t, "im")
|
return hasattr(t, "im")
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Constants (also defined in _imagingmodule.c!)
|
# Constants (also defined in _imagingmodule.c!)
|
||||||
|
|
||||||
|
@ -339,6 +340,7 @@ def getmodebands(mode):
|
||||||
"""
|
"""
|
||||||
return len(ImageMode.getmode(mode).bands)
|
return len(ImageMode.getmode(mode).bands)
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
# Helpers
|
# Helpers
|
||||||
|
|
||||||
|
@ -691,8 +693,8 @@ class Image(object):
|
||||||
return b"".join(data)
|
return b"".join(data)
|
||||||
|
|
||||||
def tostring(self, *args, **kw):
|
def tostring(self, *args, **kw):
|
||||||
raise NotImplementedError("tostring() has been removed. " +
|
raise NotImplementedError("tostring() has been removed. "
|
||||||
"Please call tobytes() instead.")
|
"Please call tobytes() instead.")
|
||||||
|
|
||||||
def tobitmap(self, name="image"):
|
def tobitmap(self, name="image"):
|
||||||
"""
|
"""
|
||||||
|
@ -742,8 +744,8 @@ class Image(object):
|
||||||
raise ValueError("cannot decode image data")
|
raise ValueError("cannot decode image data")
|
||||||
|
|
||||||
def fromstring(self, *args, **kw):
|
def fromstring(self, *args, **kw):
|
||||||
raise NotImplementedError("fromstring() has been removed. " +
|
raise NotImplementedError("fromstring() has been removed. "
|
||||||
"Please call frombytes() instead.")
|
"Please call frombytes() instead.")
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
"""
|
"""
|
||||||
|
@ -879,7 +881,7 @@ class Image(object):
|
||||||
try:
|
try:
|
||||||
t = trns_im.palette.getcolor(t)
|
t = trns_im.palette.getcolor(t)
|
||||||
except:
|
except:
|
||||||
raise ValueError("Couldn't allocate a palette " +
|
raise ValueError("Couldn't allocate a palette "
|
||||||
"color for transparency")
|
"color for transparency")
|
||||||
trns_im.putpixel((0, 0), t)
|
trns_im.putpixel((0, 0), t)
|
||||||
|
|
||||||
|
@ -1034,8 +1036,7 @@ class Image(object):
|
||||||
if y1 < y0:
|
if y1 < y0:
|
||||||
y1 = y0
|
y1 = y0
|
||||||
|
|
||||||
return self._new(self.im.crop(( x0, y0, x1, y1)))
|
return self._new(self.im.crop((x0, y0, x1, y1)))
|
||||||
|
|
||||||
|
|
||||||
def draft(self, mode, size):
|
def draft(self, mode, size):
|
||||||
"""
|
"""
|
||||||
|
@ -1254,8 +1255,8 @@ class Image(object):
|
||||||
return self.im.histogram()
|
return self.im.histogram()
|
||||||
|
|
||||||
def offset(self, xoffset, yoffset=None):
|
def offset(self, xoffset, yoffset=None):
|
||||||
raise NotImplementedError("offset() has been removed. " +
|
raise NotImplementedError("offset() has been removed. "
|
||||||
"Please call ImageChops.offset() instead.")
|
"Please call ImageChops.offset() instead.")
|
||||||
|
|
||||||
def paste(self, im, box=None, mask=None):
|
def paste(self, im, box=None, mask=None):
|
||||||
"""
|
"""
|
||||||
|
@ -1553,7 +1554,8 @@ class Image(object):
|
||||||
|
|
||||||
return self._new(self.im.resize(size, resample))
|
return self._new(self.im.resize(size, resample))
|
||||||
|
|
||||||
def rotate(self, angle, resample=NEAREST, expand=0, center=None, translate=None):
|
def rotate(self, angle, resample=NEAREST, expand=0, center=None,
|
||||||
|
translate=None):
|
||||||
"""
|
"""
|
||||||
Returns a rotated copy of this image. This method returns a
|
Returns a rotated copy of this image. This method returns a
|
||||||
copy of this image, rotated the given number of degrees counter
|
copy of this image, rotated the given number of degrees counter
|
||||||
|
@ -1622,10 +1624,13 @@ class Image(object):
|
||||||
round(math.cos(angle), 15), round(math.sin(angle), 15), 0.0,
|
round(math.cos(angle), 15), round(math.sin(angle), 15), 0.0,
|
||||||
round(-math.sin(angle), 15), round(math.cos(angle), 15), 0.0
|
round(-math.sin(angle), 15), round(math.cos(angle), 15), 0.0
|
||||||
]
|
]
|
||||||
|
|
||||||
def transform(x, y, matrix):
|
def transform(x, y, matrix):
|
||||||
(a, b, c, d, e, f) = matrix
|
(a, b, c, d, e, f) = matrix
|
||||||
return a*x + b*y + c, d*x + e*y + f
|
return a*x + b*y + c, d*x + e*y + f
|
||||||
matrix[2], matrix[5] = transform(-center[0] - translate[0], -center[1] - translate[1], matrix)
|
|
||||||
|
matrix[2], matrix[5] = transform(-center[0] - translate[0],
|
||||||
|
-center[1] - translate[1], matrix)
|
||||||
matrix[2] += center[0]
|
matrix[2] += center[0]
|
||||||
matrix[5] += center[1]
|
matrix[5] += center[1]
|
||||||
|
|
||||||
|
@ -1641,9 +1646,11 @@ class Image(object):
|
||||||
nh = int(math.ceil(max(yy)) - math.floor(min(yy)))
|
nh = int(math.ceil(max(yy)) - math.floor(min(yy)))
|
||||||
|
|
||||||
# We multiply a translation matrix from the right. Because of its
|
# We multiply a translation matrix from the right. Because of its
|
||||||
# special form, this is the same as taking the image of the translation vector
|
# special form, this is the same as taking the image of the
|
||||||
# as new translation vector.
|
# translation vector as new translation vector.
|
||||||
matrix[2], matrix[5] = transform(-(nw - w) / 2.0, -(nh - h) / 2.0, matrix)
|
matrix[2], matrix[5] = transform(-(nw - w) / 2.0,
|
||||||
|
-(nh - h) / 2.0,
|
||||||
|
matrix)
|
||||||
w, h = nw, nh
|
w, h = nw, nh
|
||||||
|
|
||||||
return self.transform((w, h), AFFINE, matrix, resample)
|
return self.transform((w, h), AFFINE, matrix, resample)
|
||||||
|
@ -1759,8 +1766,8 @@ class Image(object):
|
||||||
PPM file, and calls either the **xv** utility or the **display**
|
PPM file, and calls either the **xv** utility or the **display**
|
||||||
utility, depending on which one can be found.
|
utility, depending on which one can be found.
|
||||||
|
|
||||||
On macOS, this method saves the image to a temporary BMP file, and opens
|
On macOS, this method saves the image to a temporary BMP file, and
|
||||||
it with the native Preview application.
|
opens it with the native Preview application.
|
||||||
|
|
||||||
On Windows, it saves the image to a temporary BMP file, and uses
|
On Windows, it saves the image to a temporary BMP file, and uses
|
||||||
the standard BMP display utility to show it (usually Paint).
|
the standard BMP display utility to show it (usually Paint).
|
||||||
|
@ -1988,7 +1995,6 @@ class Image(object):
|
||||||
return ImageQt.toqpixmap(self)
|
return ImageQt.toqpixmap(self)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
# Abstract handlers.
|
# Abstract handlers.
|
||||||
|
|
||||||
|
@ -2013,6 +2019,7 @@ def _wedge():
|
||||||
|
|
||||||
return Image()._new(core.wedge("L"))
|
return Image()._new(core.wedge("L"))
|
||||||
|
|
||||||
|
|
||||||
def _check_size(size):
|
def _check_size(size):
|
||||||
"""
|
"""
|
||||||
Common check to enforce type and sanity check on size tuples
|
Common check to enforce type and sanity check on size tuples
|
||||||
|
@ -2030,6 +2037,7 @@ def _check_size(size):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def new(mode, size, color=0):
|
def new(mode, size, color=0):
|
||||||
"""
|
"""
|
||||||
Creates a new image with the given mode and size.
|
Creates a new image with the given mode and size.
|
||||||
|
@ -2101,7 +2109,7 @@ def frombytes(mode, size, data, decoder_name="raw", *args):
|
||||||
|
|
||||||
def fromstring(*args, **kw):
|
def fromstring(*args, **kw):
|
||||||
raise NotImplementedError("fromstring() has been removed. " +
|
raise NotImplementedError("fromstring() has been removed. " +
|
||||||
"Please call frombytes() instead.")
|
"Please call frombytes() instead.")
|
||||||
|
|
||||||
|
|
||||||
def frombuffer(mode, size, data, decoder_name="raw", *args):
|
def frombuffer(mode, size, data, decoder_name="raw", *args):
|
||||||
|
@ -2230,6 +2238,7 @@ def fromqpixmap(im):
|
||||||
raise ImportError("Qt bindings are not installed")
|
raise ImportError("Qt bindings are not installed")
|
||||||
return ImageQt.fromqpixmap(im)
|
return ImageQt.fromqpixmap(im)
|
||||||
|
|
||||||
|
|
||||||
_fromarray_typemap = {
|
_fromarray_typemap = {
|
||||||
# (shape, typestr) => mode, rawmode
|
# (shape, typestr) => mode, rawmode
|
||||||
# first two members of shape are set to one
|
# first two members of shape are set to one
|
||||||
|
@ -2558,3 +2567,21 @@ def effect_noise(size, sigma):
|
||||||
:param sigma: Standard deviation of noise.
|
:param sigma: Standard deviation of noise.
|
||||||
"""
|
"""
|
||||||
return Image()._new(core.effect_noise(size, sigma))
|
return Image()._new(core.effect_noise(size, sigma))
|
||||||
|
|
||||||
|
|
||||||
|
def linear_gradient(mode):
|
||||||
|
"""
|
||||||
|
Generate 256x256 linear gradient from black to white, top to bottom.
|
||||||
|
|
||||||
|
:param mode: Input mode.
|
||||||
|
"""
|
||||||
|
return Image()._new(core.linear_gradient(mode))
|
||||||
|
|
||||||
|
|
||||||
|
def radial_gradient(mode):
|
||||||
|
"""
|
||||||
|
Generate 256x256 radial gradient from black to white, centre to edge.
|
||||||
|
|
||||||
|
:param mode: Input mode.
|
||||||
|
"""
|
||||||
|
return Image()._new(core.radial_gradient(mode))
|
||||||
|
|
BIN
Tests/images/linear_gradient.png
Normal file
BIN
Tests/images/linear_gradient.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 516 B |
BIN
Tests/images/radial_gradient.png
Normal file
BIN
Tests/images/radial_gradient.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.4 KiB |
|
@ -280,21 +280,22 @@ class TestImage(PillowTestCase):
|
||||||
self.assert_image_similar(im2, im3, 110)
|
self.assert_image_similar(im2, im3, 110)
|
||||||
|
|
||||||
def test_check_size(self):
|
def test_check_size(self):
|
||||||
# Checking that the _check_size function throws value errors when we want it to.
|
# Checking that the _check_size function throws value errors
|
||||||
|
# when we want it to.
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
Image.new('RGB', 0) # not a tuple
|
Image.new('RGB', 0) # not a tuple
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
Image.new('RGB', (0,)) # Tuple too short
|
Image.new('RGB', (0,)) # Tuple too short
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
Image.new('RGB', (-1,-1)) # w,h < 0
|
Image.new('RGB', (-1, -1)) # w,h < 0
|
||||||
|
|
||||||
# this should pass with 0 sized images, #2259
|
# this should pass with 0 sized images, #2259
|
||||||
im = Image.new('L', (0, 0))
|
im = Image.new('L', (0, 0))
|
||||||
self.assertEqual(im.size, (0, 0))
|
self.assertEqual(im.size, (0, 0))
|
||||||
|
|
||||||
self.assertTrue(Image.new('RGB', (1,1)))
|
self.assertTrue(Image.new('RGB', (1, 1)))
|
||||||
# Should pass lists too
|
# Should pass lists too
|
||||||
i = Image.new('RGB', [1,1])
|
i = Image.new('RGB', [1, 1])
|
||||||
self.assertIsInstance(i.size, tuple)
|
self.assertIsInstance(i.size, tuple)
|
||||||
|
|
||||||
def test_storage_neg(self):
|
def test_storage_neg(self):
|
||||||
|
@ -304,7 +305,7 @@ class TestImage(PillowTestCase):
|
||||||
# Storage.c, rather than the size check above
|
# Storage.c, rather than the size check above
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
Image.core.fill('RGB', (2,-2), (0,0,0))
|
Image.core.fill('RGB', (2, -2), (0, 0, 0))
|
||||||
|
|
||||||
def test_offset_not_implemented(self):
|
def test_offset_not_implemented(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
|
@ -316,6 +317,58 @@ class TestImage(PillowTestCase):
|
||||||
def test_fromstring(self):
|
def test_fromstring(self):
|
||||||
self.assertRaises(NotImplementedError, Image.fromstring)
|
self.assertRaises(NotImplementedError, Image.fromstring)
|
||||||
|
|
||||||
|
def test_linear_gradient_wrong_mode(self):
|
||||||
|
# Arrange
|
||||||
|
wrong_mode = "RGB"
|
||||||
|
|
||||||
|
# Act / Assert
|
||||||
|
self.assertRaises(ValueError,
|
||||||
|
lambda: Image.linear_gradient(wrong_mode))
|
||||||
|
return
|
||||||
|
|
||||||
|
def test_linear_gradient(self):
|
||||||
|
|
||||||
|
# Arrange
|
||||||
|
target_file = "Tests/images/linear_gradient.png"
|
||||||
|
for mode in ["L", "P"]:
|
||||||
|
|
||||||
|
# Act
|
||||||
|
im = Image.linear_gradient(mode)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertEqual(im.size, (256, 256))
|
||||||
|
self.assertEqual(im.mode, mode)
|
||||||
|
self.assertEqual(im.getpixel((0, 0)), 0)
|
||||||
|
self.assertEqual(im.getpixel((255, 255)), 255)
|
||||||
|
target = Image.open(target_file).convert(mode)
|
||||||
|
self.assert_image_equal(im, target)
|
||||||
|
|
||||||
|
def test_radial_gradient_wrong_mode(self):
|
||||||
|
# Arrange
|
||||||
|
wrong_mode = "RGB"
|
||||||
|
|
||||||
|
# Act / Assert
|
||||||
|
self.assertRaises(ValueError,
|
||||||
|
lambda: Image.radial_gradient(wrong_mode))
|
||||||
|
return
|
||||||
|
|
||||||
|
def test_radial_gradient(self):
|
||||||
|
|
||||||
|
# Arrange
|
||||||
|
target_file = "Tests/images/radial_gradient.png"
|
||||||
|
for mode in ["L", "P"]:
|
||||||
|
|
||||||
|
# Act
|
||||||
|
im = Image.radial_gradient(mode)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertEqual(im.size, (256, 256))
|
||||||
|
self.assertEqual(im.mode, mode)
|
||||||
|
self.assertEqual(im.getpixel((0, 0)), 255)
|
||||||
|
self.assertEqual(im.getpixel((128, 128)), 0)
|
||||||
|
target = Image.open(target_file).convert(mode)
|
||||||
|
self.assert_image_equal(im, target)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -64,15 +64,18 @@ ImagingFillLinearGradient(const char *mode)
|
||||||
Imaging im;
|
Imaging im;
|
||||||
int y;
|
int y;
|
||||||
|
|
||||||
if (strlen(mode) != 1)
|
if (strlen(mode) != 1) {
|
||||||
return (Imaging) ImagingError_ModeError();
|
return (Imaging) ImagingError_ModeError();
|
||||||
|
}
|
||||||
|
|
||||||
im = ImagingNew(mode, 256, 256);
|
im = ImagingNew(mode, 256, 256);
|
||||||
if (!im)
|
if (!im) {
|
||||||
return NULL;
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
for (y = 0; y < 256; y++)
|
for (y = 0; y < 256; y++) {
|
||||||
memset(im->image8[y], (unsigned char) y, 256);
|
memset(im->image8[y], (unsigned char) y, 256);
|
||||||
|
}
|
||||||
|
|
||||||
return im;
|
return im;
|
||||||
}
|
}
|
||||||
|
@ -84,21 +87,25 @@ ImagingFillRadialGradient(const char *mode)
|
||||||
int x, y;
|
int x, y;
|
||||||
int d;
|
int d;
|
||||||
|
|
||||||
if (strlen(mode) != 1)
|
if (strlen(mode) != 1) {
|
||||||
return (Imaging) ImagingError_ModeError();
|
return (Imaging) ImagingError_ModeError();
|
||||||
|
}
|
||||||
|
|
||||||
im = ImagingNew(mode, 256, 256);
|
im = ImagingNew(mode, 256, 256);
|
||||||
if (!im)
|
if (!im) {
|
||||||
return NULL;
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
for (y = 0; y < 256; y++)
|
for (y = 0; y < 256; y++) {
|
||||||
for (x = 0; x < 256; x++) {
|
for (x = 0; x < 256; x++) {
|
||||||
d = (int) sqrt((double) ((x-128)*(x-128) + (y-128)*(y-128)) * 2.0);
|
d = (int) sqrt((double) ((x-128)*(x-128) + (y-128)*(y-128)) * 2.0);
|
||||||
if (d >= 255)
|
if (d >= 255) {
|
||||||
im->image8[y][x] = 255;
|
im->image8[y][x] = 255;
|
||||||
else
|
} else {
|
||||||
im->image8[y][x] = d;
|
im->image8[y][x] = d;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return im;
|
return im;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user