Merge pull request #2382 from hugovk/test-effects

Test linear and radial gradient effects
This commit is contained in:
wiredfool 2017-02-06 21:58:41 +00:00 committed by GitHub
commit 9c4eafc188
5 changed files with 128 additions and 41 deletions

View File

@ -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))

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -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()

View File

@ -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;
} }