From 09ecbae1437ca9f394eb236ca7198d4842682720 Mon Sep 17 00:00:00 2001 From: Jan Sagemueller Date: Mon, 13 Feb 2012 15:06:28 +0100 Subject: [PATCH 01/12] Resize processor: Externalized transparency padding into new class Mat Conflicts: imagekit/processors/resize.py --- imagekit/processors/resize.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index 923877a..08c903a 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -116,6 +116,20 @@ class Crop(Fill): super(Crop, self).__init__(*args, **kwargs) +class Mat(object): + def __init__(self, width, height, color=(0, 0, 0, 0), anchor=(0.5, 0.5)): + """Anchor behaves like Crop's anchor argument""" + self.width = width + self.height = height + self.color = color + self.anchor = anchor + + def process(self, img): + new_img = Image.new('RGBA', (self.width, self.height), self.color) + new_img.paste(img, ((self.width - img.size[0]) / 2, (self.height - img.size[1]) / 2)) + return new_img + + class Fit(object): """ Resizes an image to fit within the specified dimensions. @@ -154,9 +168,7 @@ class Fit(object): img = BasicResize(new_dimensions[0], new_dimensions[1]).process(img) if self.mat_color: - new_img = Image.new('RGBA', (self.width, self.height), self.mat_color) - new_img.paste(img, ((self.width - img.size[0]) / 2, (self.height - img.size[1]) / 2)) - img = new_img + img = Mat(self.width, self.height, self.mat_color).process(img) return img From b073868bb740f0b9e84da045c412cac0cad4b9d2 Mon Sep 17 00:00:00 2001 From: Jan Sagemueller Date: Thu, 16 Feb 2012 02:46:31 +0100 Subject: [PATCH 02/12] AddBorder, Anchor, and ResizeCanvas processors [NEW] Processors: AddBorder [NEW] Processors: Anchor has now its own class, taken from Crop [CHG] Processors: Renamed Mat => ResizeCanvas, and will now use either an anchor from Anchor or a user defined pixel offset --- imagekit/processors/__init__.py | 32 ++++++++++++- imagekit/processors/crop.py | 28 ++--------- imagekit/processors/resize.py | 83 +++++++++++++++++++++------------ 3 files changed, 87 insertions(+), 56 deletions(-) diff --git a/imagekit/processors/__init__.py b/imagekit/processors/__init__.py index c3be66c..d6d2aad 100644 --- a/imagekit/processors/__init__.py +++ b/imagekit/processors/__init__.py @@ -8,7 +8,6 @@ from both the filesystem and the ORM. """ from imagekit.lib import Image, ImageColor, ImageEnhance -from imagekit.processors import resize, crop RGBA_TRANSPARENCY_FORMATS = ['PNG'] @@ -198,7 +197,8 @@ class AutoConvert(object): def process(self, img): matte = False self.save_kwargs = {} - if img.mode == 'RGBA': + self.rgba_ = img.mode == 'RGBA' + if self.rgba_: if self.format in RGBA_TRANSPARENCY_FORMATS: pass elif self.format in PALETTE_TRANSPARENCY_FORMATS: @@ -264,3 +264,31 @@ class AutoConvert(object): self.save_kwargs['optimize'] = True return img + + +class Anchor(object): + """ + Defines all the anchor points needed by the various processor classes. + + """ + TOP_LEFT = 'tl' + TOP = 't' + TOP_RIGHT = 'tr' + BOTTOM_LEFT = 'bl' + BOTTOM = 'b' + BOTTOM_RIGHT = 'br' + CENTER = 'c' + LEFT = 'l' + RIGHT = 'r' + + _ANCHOR_PTS = { + TOP_LEFT: (0, 0), + TOP: (0.5, 0), + TOP_RIGHT: (1, 0), + LEFT: (0, 0.5), + CENTER: (0.5, 0.5), + RIGHT: (1, 0.5), + BOTTOM_LEFT: (0, 1), + BOTTOM: (0.5, 1), + BOTTOM_RIGHT: (1, 1), + } \ No newline at end of file diff --git a/imagekit/processors/crop.py b/imagekit/processors/crop.py index 304297d..3476b0f 100644 --- a/imagekit/processors/crop.py +++ b/imagekit/processors/crop.py @@ -1,5 +1,6 @@ from ..lib import Image, ImageChops, ImageDraw, ImageStat from .utils import histogram_entropy +from . import Anchor class Side(object): @@ -99,27 +100,6 @@ class Crop(object): relative to the anchor. """ - TOP_LEFT = 'tl' - TOP = 't' - TOP_RIGHT = 'tr' - BOTTOM_LEFT = 'bl' - BOTTOM = 'b' - BOTTOM_RIGHT = 'br' - CENTER = 'c' - LEFT = 'l' - RIGHT = 'r' - - _ANCHOR_PTS = { - TOP_LEFT: (0, 0), - TOP: (0.5, 0), - TOP_RIGHT: (1, 0), - LEFT: (0, 0.5), - CENTER: (0.5, 0.5), - RIGHT: (1, 0.5), - BOTTOM_LEFT: (0, 1), - BOTTOM: (0.5, 1), - BOTTOM_RIGHT: (1, 1), - } def __init__(self, width=None, height=None, anchor=None): """ @@ -158,9 +138,9 @@ class Crop(object): # If the user passed in one of the string values, convert it to a # percentage tuple. - anchor = self.anchor or Crop.CENTER - if anchor in Crop._ANCHOR_PTS.keys(): - anchor = Crop._ANCHOR_PTS[anchor] + anchor = self.anchor or Anchor.CENTER + if anchor in Anchor._ANCHOR_PTS.keys(): + anchor = Anchor._ANCHOR_PTS[anchor] x = int(float(trim_x) * float(anchor[0])) y = int(float(trim_y) * float(anchor[1])) diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index 08c903a..7706d9c 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -1,6 +1,7 @@ from imagekit.lib import Image from . import crop import warnings +from . import Anchor class BasicResize(object): @@ -47,36 +48,16 @@ class Cover(object): class Fill(object): """ - Resizes an image , cropping it to the exact specified width and height. + Resizes an image, cropping it to the exact specified width and height. """ - TOP_LEFT = crop.Crop.TOP_LEFT - TOP = crop.Crop.TOP - TOP_RIGHT = crop.Crop.TOP_RIGHT - BOTTOM_LEFT = crop.Crop.BOTTOM_LEFT - BOTTOM = crop.Crop.BOTTOM - BOTTOM_RIGHT = crop.Crop.BOTTOM_RIGHT - CENTER = crop.Crop.CENTER - LEFT = crop.Crop.LEFT - RIGHT = crop.Crop.RIGHT def __init__(self, width=None, height=None, anchor=None): """ :param width: The target width, in pixels. :param height: The target height, in pixels. :param anchor: Specifies which part of the image should be retained - when cropping. Valid values are: - - - Fill.TOP_LEFT - - Fill.TOP - - Fill.TOP_RIGHT - - Fill.LEFT - - Fill.CENTER - - Fill.RIGHT - - Fill.BOTTOM_LEFT - - Fill.BOTTOM - - Fill.BOTTOM_RIGHT - + when cropping. """ self.width = width self.height = height @@ -94,7 +75,6 @@ class SmartFill(object): entropy to crop the image instead of a user-specified anchor point. Internally, it simply runs the ``resize.Cover`` and ``crop.SmartCrop`` processors in series. - """ def __init__(self, width, height): """ @@ -116,26 +96,68 @@ class Crop(Fill): super(Crop, self).__init__(*args, **kwargs) -class Mat(object): - def __init__(self, width, height, color=(0, 0, 0, 0), anchor=(0.5, 0.5)): - """Anchor behaves like Crop's anchor argument""" +class ResizeCanvas(object): + """ + Takes an image an resizes the canvas, using a specific background color + if the new size is larger than the current image. + + """ + def __init__(self, width, height, color=None, top=None, left=None, anchor=None): + """ + :param width: The target width, in pixels. + :param height: The target height, in pixels. + :param color: The background color to use for padding. + :param anchor: Specifies the relative position of the original image. + + """ + if (anchor and not (top is None and left is None)) \ + or (anchor is None and top is None and left is None): + raise Exception('You provide either an anchor or x and y position, but not both or none.') self.width = width self.height = height self.color = color + self.top = top + self.left = left self.anchor = anchor def process(self, img): - new_img = Image.new('RGBA', (self.width, self.height), self.color) - new_img.paste(img, ((self.width - img.size[0]) / 2, (self.height - img.size[1]) / 2)) + new_img = Image.new('RGBA', (self.width, self.height), self.color) + if self.anchor: + self.top = int(abs(self.width - img.size[0]) * Anchor._ANCHOR_PTS[self.anchor][0]) + self.left = int(abs(self.height - img.size[1]) * Anchor._ANCHOR_PTS[self.anchor][1]) + new_img.paste(img, (self.top, self.left)) return new_img +class AddBorder(object): + """ + Add a border of specific color and size to an image. + + """ + def __init__(self, color, thickness): + """ + :param color: Color to use for the border + :param thickness: Thickness of the border which is either an int or a 4-tuple of ints. + """ + self.color = color + if isinstance(thickness, int): + self.top = self.right = self.bottom = self.left = thickness + else: + self.top, self.right, self.bottom, self.left = thickness + + def process(self, img): + new_width = img.size[0] + self.left + self.right + new_height = img.size[1] + self.top + self.bottom + return ResizeCanvas(new_width, new_height, self.color, self.top, self.left).process(img) + + class Fit(object): """ Resizes an image to fit within the specified dimensions. """ - def __init__(self, width=None, height=None, upscale=None, mat_color=None): + + def __init__(self, width=None, height=None, upscale=None, mat_color=None, anchor=Anchor.CENTER): """ :param width: The maximum width of the desired image. :param height: The maximum height of the desired image. @@ -150,6 +172,7 @@ class Fit(object): self.height = height self.upscale = upscale self.mat_color = mat_color + self.anchor = anchor def process(self, img): cur_width, cur_height = img.size @@ -168,7 +191,7 @@ class Fit(object): img = BasicResize(new_dimensions[0], new_dimensions[1]).process(img) if self.mat_color: - img = Mat(self.width, self.height, self.mat_color).process(img) + img = ResizeCanvas(self.width, self.height, self.mat_color, anchor=self.anchor).process(img) return img From 441266a6d78f180e1701fe29b3a3129ca6498894 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 17 Feb 2012 22:14:30 -0500 Subject: [PATCH 03/12] Small ResizeCanvas fixes --- imagekit/processors/resize.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index 7706d9c..5ce1c39 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -98,11 +98,11 @@ class Crop(Fill): class ResizeCanvas(object): """ - Takes an image an resizes the canvas, using a specific background color - if the new size is larger than the current image. + Resizes the canvas, using the provided background color if the new size is + larger than the current image. """ - def __init__(self, width, height, color=None, top=None, left=None, anchor=None): + def __init__(self, width, height, color=None, anchor=None, x=None, y=None): """ :param width: The target width, in pixels. :param height: The target height, in pixels. @@ -110,22 +110,29 @@ class ResizeCanvas(object): :param anchor: Specifies the relative position of the original image. """ - if (anchor and not (top is None and left is None)) \ - or (anchor is None and top is None and left is None): - raise Exception('You provide either an anchor or x and y position, but not both or none.') + if x is not None or y is not None: + if anchor: + raise Exception('You may provide either an anchor or x and y' + ' coordinate, but not both.') + else: + self.x, self.y = x or 0, y or 0 + self.anchor = None + else: + self.anchor = anchor or Anchor.CENTER + self.x = self.y = None + self.width = width self.height = height self.color = color - self.top = top - self.left = left - self.anchor = anchor def process(self, img): new_img = Image.new('RGBA', (self.width, self.height), self.color) if self.anchor: - self.top = int(abs(self.width - img.size[0]) * Anchor._ANCHOR_PTS[self.anchor][0]) - self.left = int(abs(self.height - img.size[1]) * Anchor._ANCHOR_PTS[self.anchor][1]) - new_img.paste(img, (self.top, self.left)) + x = int(abs(self.width - img.size[0]) * Anchor._ANCHOR_PTS[self.anchor][0]) + y = int(abs(self.height - img.size[1]) * Anchor._ANCHOR_PTS[self.anchor][1]) + else: + x, y = self.x, self.y + new_img.paste(img, (x, y)) return new_img @@ -148,7 +155,8 @@ class AddBorder(object): def process(self, img): new_width = img.size[0] + self.left + self.right new_height = img.size[1] + self.top + self.bottom - return ResizeCanvas(new_width, new_height, self.color, self.top, self.left).process(img) + return ResizeCanvas(new_width, new_height, color=self.color, + x=self.left, y=self.top).process(img) class Fit(object): From dd5efac0ebcc9b2be571daaf8bcbc72c3c0b6b57 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 17 Feb 2012 22:26:10 -0500 Subject: [PATCH 04/12] A little AddBorer cleanup --- imagekit/processors/resize.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index 5ce1c39..c8e457f 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -141,10 +141,11 @@ class AddBorder(object): Add a border of specific color and size to an image. """ - def __init__(self, color, thickness): + def __init__(self, thickness, color=None): """ :param color: Color to use for the border - :param thickness: Thickness of the border which is either an int or a 4-tuple of ints. + :param thickness: Thickness of the border. Can be either an int or + a 4-tuple of ints of the form (top, right, bottom, left). """ self.color = color if isinstance(thickness, int): From 6cca16ef990c5dd901b1601f935dc45d13b7ae28 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 17 Feb 2012 22:29:41 -0500 Subject: [PATCH 05/12] Allow negative coordinates when using an anchor --- imagekit/processors/resize.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index c8e457f..9501806 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -128,8 +128,8 @@ class ResizeCanvas(object): def process(self, img): new_img = Image.new('RGBA', (self.width, self.height), self.color) if self.anchor: - x = int(abs(self.width - img.size[0]) * Anchor._ANCHOR_PTS[self.anchor][0]) - y = int(abs(self.height - img.size[1]) * Anchor._ANCHOR_PTS[self.anchor][1]) + x = int((self.width - img.size[0]) * Anchor._ANCHOR_PTS[self.anchor][0]) + y = int((self.height - img.size[1]) * Anchor._ANCHOR_PTS[self.anchor][1]) else: x, y = self.x, self.y new_img.paste(img, (x, y)) From 568c3d29a1e2a8adcb78eb34502569f1894525ad Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 17 Feb 2012 22:39:43 -0500 Subject: [PATCH 06/12] Default ResizeCanvas color --- imagekit/processors/resize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index 9501806..f22f93c 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -123,7 +123,7 @@ class ResizeCanvas(object): self.width = width self.height = height - self.color = color + self.color = color or (255, 255, 255, 0) def process(self, img): new_img = Image.new('RGBA', (self.width, self.height), self.color) From a164427074b09a19377ac8f94f71c8c5aa269e06 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 17 Feb 2012 23:04:45 -0500 Subject: [PATCH 07/12] Don't use Anchor internals; allow any anchor tuple ResizeCanvas now uses the anchor behavior of the Crop processor --- imagekit/processors/__init__.py | 13 ++++++++++++- imagekit/processors/resize.py | 12 +++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/imagekit/processors/__init__.py b/imagekit/processors/__init__.py index d6d2aad..c7002ef 100644 --- a/imagekit/processors/__init__.py +++ b/imagekit/processors/__init__.py @@ -291,4 +291,15 @@ class Anchor(object): BOTTOM_LEFT: (0, 1), BOTTOM: (0.5, 1), BOTTOM_RIGHT: (1, 1), - } \ No newline at end of file + } + + @staticmethod + def get_tuple(anchor): + """Normalizes anchor values (strings or tuples) to tuples. + + """ + # If the user passed in one of the string values, convert it to a + # percentage tuple. + if anchor in Anchor._ANCHOR_PTS.keys(): + anchor = Anchor._ANCHOR_PTS[anchor] + return anchor diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index f22f93c..a18a533 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -126,12 +126,18 @@ class ResizeCanvas(object): self.color = color or (255, 255, 255, 0) def process(self, img): - new_img = Image.new('RGBA', (self.width, self.height), self.color) + original_width, original_height = img.size + if self.anchor: - x = int((self.width - img.size[0]) * Anchor._ANCHOR_PTS[self.anchor][0]) - y = int((self.height - img.size[1]) * Anchor._ANCHOR_PTS[self.anchor][1]) + anchor = Anchor.get_tuple(self.anchor) + trim_x, trim_y = self.width - original_width, \ + self.height - original_height + x = int(float(trim_x) * float(anchor[0])) + y = int(float(trim_y) * float(anchor[1])) else: x, y = self.x, self.y + + new_img = Image.new('RGBA', (self.width, self.height), self.color) new_img.paste(img, (x, y)) return new_img From 3e2bd2f21f631542e0cb480e44d3bfdc878b84ac Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 17 Feb 2012 23:20:54 -0500 Subject: [PATCH 08/12] Crop processor consolidation BasicCrop is absorbed into Crop and Crop uses ResizeCanvas --- imagekit/processors/crop.py | 75 ++++++----------------------------- imagekit/processors/resize.py | 18 ++++++++- 2 files changed, 30 insertions(+), 63 deletions(-) diff --git a/imagekit/processors/crop.py b/imagekit/processors/crop.py index 3476b0f..5f7af38 100644 --- a/imagekit/processors/crop.py +++ b/imagekit/processors/crop.py @@ -1,6 +1,6 @@ -from ..lib import Image, ImageChops, ImageDraw, ImageStat -from .utils import histogram_entropy from . import Anchor +from .utils import histogram_entropy +from ..lib import Image, ImageChops, ImageDraw, ImageStat class Side(object): @@ -72,80 +72,31 @@ class TrimBorderColor(object): return img -class BasicCrop(object): - """Crops an image to the specified rectangular region. - - """ - def __init__(self, x, y, width, height): - """ - :param x: The x position of the clipping box, in pixels. - :param y: The y position of the clipping box, in pixels. - :param width: The width position of the clipping box, in pixels. - :param height: The height position of the clipping box, in pixels. - - """ - self.x = x - self.y = y - self.width = width - self.height = height - - def process(self, img): - box = (self.x, self.y, self.x + self.width, self.y + self.height) - return img.crop(box) - - class Crop(object): """ - Crops an image , cropping it to the specified width and height - relative to the anchor. + Crops an image, cropping it to the specified width and height. You may + optionally provide either an anchor or x and y coordinates. This processor + functions exactly the same as ``ResizeCanvas`` except that it will never + enlarge the image. """ - def __init__(self, width=None, height=None, anchor=None): - """ - :param width: The target width, in pixels. - :param height: The target height, in pixels. - :param anchor: Specifies which part of the image should be retained - when cropping. Valid values are: - - - Crop.TOP_LEFT - - Crop.TOP - - Crop.TOP_RIGHT - - Crop.LEFT - - Crop.CENTER - - Crop.RIGHT - - Crop.BOTTOM_LEFT - - Crop.BOTTOM - - Crop.BOTTOM_RIGHT - - You may also pass a tuple that indicates the percentages of excess - to be trimmed from each dimension. For example, ``(0, 0)`` - corresponds to "top left", ``(0.5, 0.5)`` to "center" and ``(1, 1)`` - to "bottom right". This is basically the same as using percentages - in CSS background positions. - - """ + def __init__(self, width=None, height=None, anchor=None, x=None, y=None): self.width = width self.height = height self.anchor = anchor + self.x = x + self.y = y def process(self, img): + from .resize import ResizeCanvas + original_width, original_height = img.size new_width, new_height = min(original_width, self.width), \ min(original_height, self.height) - trim_x, trim_y = original_width - new_width, \ - original_height - new_height - # If the user passed in one of the string values, convert it to a - # percentage tuple. - anchor = self.anchor or Anchor.CENTER - if anchor in Anchor._ANCHOR_PTS.keys(): - anchor = Anchor._ANCHOR_PTS[anchor] - - x = int(float(trim_x) * float(anchor[0])) - y = int(float(trim_y) * float(anchor[1])) - - return BasicCrop(x, y, new_width, new_height).process(img) + return ResizeCanvas(new_width, new_height, anchor=self.anchor, + x=self.x, y=self.y).process(img) class SmartCrop(object): diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index a18a533..65677ab 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -107,7 +107,23 @@ class ResizeCanvas(object): :param width: The target width, in pixels. :param height: The target height, in pixels. :param color: The background color to use for padding. - :param anchor: Specifies the relative position of the original image. + :param anchor: Specifies the position of the original image on the new + canvas. Valid values are: + + - Anchor.TOP_LEFT + - Anchor.TOP + - Anchor.TOP_RIGHT + - Anchor.LEFT + - Anchor.CENTER + - Anchor.RIGHT + - Anchor.BOTTOM_LEFT + - Anchor.BOTTOM + - Anchor.BOTTOM_RIGHT + + You may also pass a tuple that indicates the position in + percentages. For example, ``(0, 0)`` corresponds to "top left", + ``(0.5, 0.5)`` to "center" and ``(1, 1)`` to "bottom right". This is + basically the same as using percentages in CSS background positions. """ if x is not None or y is not None: From 3fad906305e77fa4eee7f361dc40c510e9c8bc54 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 17 Feb 2012 23:54:39 -0500 Subject: [PATCH 09/12] Remove crop.Crop to avoid confusion with resize.Crop --- imagekit/processors/resize.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index 65677ab..b50ae51 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -89,13 +89,6 @@ class SmartFill(object): return crop.SmartCrop(self.width, self.height).process(img) -class Crop(Fill): - def __init__(self, *args, **kwargs): - warnings.warn('`imagekit.processors.resize.Crop` has been renamed to' - '`imagekit.processors.resize.Fill`.', DeprecationWarning) - super(Crop, self).__init__(*args, **kwargs) - - class ResizeCanvas(object): """ Resizes the canvas, using the provided background color if the new size is From 5a8564d03970be849ee6a61c66fb4714a684739d Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Fri, 17 Feb 2012 23:55:59 -0500 Subject: [PATCH 10/12] Rename BasicResize to Resize --- imagekit/processors/resize.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index b50ae51..cc19027 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -4,7 +4,7 @@ import warnings from . import Anchor -class BasicResize(object): +class Resize(object): """ Resizes an image to the specified width and height. @@ -43,7 +43,7 @@ class Cover(object): float(self.height) / original_height) new_width, new_height = (int(original_width * ratio), int(original_height * ratio)) - return BasicResize(new_width, new_height).process(img) + return Resize(new_width, new_height).process(img) class Fill(object): @@ -212,7 +212,7 @@ class Fit(object): int(round(cur_height * ratio))) if (cur_width > new_dimensions[0] or cur_height > new_dimensions[1]) or \ self.upscale: - img = BasicResize(new_dimensions[0], + img = Resize(new_dimensions[0], new_dimensions[1]).process(img) if self.mat_color: img = ResizeCanvas(self.width, self.height, self.mat_color, anchor=self.anchor).process(img) From 3912003f02ff8a4ff981379059103f1817856376 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 18 Feb 2012 00:02:37 -0500 Subject: [PATCH 11/12] Rename Fit and Fill to ResizeToFit and ResizeToFill --- imagekit/processors/resize.py | 2 +- tests/core/tests.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index cc19027..c18fc47 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -175,7 +175,7 @@ class AddBorder(object): x=self.left, y=self.top).process(img) -class Fit(object): +class ResizeToFit(object): """ Resizes an image to fit within the specified dimensions. diff --git a/tests/core/tests.py b/tests/core/tests.py index c11d89b..396aab5 100644 --- a/tests/core/tests.py +++ b/tests/core/tests.py @@ -13,8 +13,8 @@ from imagekit import utils from imagekit.lib import Image from imagekit.models.fields import ImageSpecField from imagekit.processors import Adjust -from imagekit.processors.resize import Fill -from imagekit.processors.crop import SmartCrop +from imagekit.processors import ResizeToFill +from imagekit.processors import SmartCrop def generate_lenna(): @@ -52,7 +52,7 @@ class Photo(models.Model): original_image = models.ImageField(upload_to='photos') thumbnail = ImageSpecField([Adjust(contrast=1.2, sharpness=1.1), - Fill(50, 50)], image_field='original_image', format='JPEG', + ResizeToFill(50, 50)], image_field='original_image', format='JPEG', options={'quality': 90}) smartcropped_thumbnail = ImageSpecField([Adjust(contrast=1.2, From e67934852dacde2bfe4ff7e6ca59d46281cabfbd Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 18 Feb 2012 00:16:41 -0500 Subject: [PATCH 12/12] Rename processors and clean up packages --- README.rst | 6 +- imagekit/processors/__init__.py | 298 +------------------------------- imagekit/processors/base.py | 296 +++++++++++++++++++++++++++++++ imagekit/processors/crop.py | 2 +- imagekit/processors/resize.py | 29 ++-- 5 files changed, 314 insertions(+), 317 deletions(-) create mode 100644 imagekit/processors/base.py diff --git a/README.rst b/README.rst index d60119d..800efa4 100644 --- a/README.rst +++ b/README.rst @@ -61,12 +61,12 @@ your spec, you can expose different versions of the original image:: from django.db import models from imagekit.models.fields import ImageSpecField - from imagekit.processors import resize, Adjust + from imagekit.processors import ResizeToFill, Adjust class Photo(models.Model): original_image = models.ImageField(upload_to='photos') thumbnail = ImageSpecField([Adjust(contrast=1.2, sharpness=1.1), - resize.Fill(50, 50)], image_field='original_image', + ResizeToFill(50, 50)], image_field='original_image', format='JPEG', options={'quality': 90}) The ``thumbnail`` property will now return a cropped image:: @@ -77,7 +77,7 @@ The ``thumbnail`` property will now return a cropped image:: photo.original_image.width # > 1000 The original image is not modified; ``thumbnail`` is a new file that is the -result of running the ``imagekit.processors.resize.Fill`` processor on the +result of running the ``imagekit.processors.ResizeToFill`` processor on the original. The ``imagekit.processors`` module contains processors for many common diff --git a/imagekit/processors/__init__.py b/imagekit/processors/__init__.py index c7002ef..c2c9320 100644 --- a/imagekit/processors/__init__.py +++ b/imagekit/processors/__init__.py @@ -7,299 +7,7 @@ should be limited to image manipulations--they should be completely decoupled from both the filesystem and the ORM. """ -from imagekit.lib import Image, ImageColor, ImageEnhance - -RGBA_TRANSPARENCY_FORMATS = ['PNG'] -PALETTE_TRANSPARENCY_FORMATS = ['PNG', 'GIF'] - - -class ProcessorPipeline(list): - """ - A :class:`list` of other processors. This class allows any object that - knows how to deal with a single processor to deal with a list of them. - For example:: - - processed_image = ProcessorPipeline([ProcessorA(), ProcessorB()]).process(image) - - """ - def process(self, img): - for proc in self: - img = proc.process(img) - return img - - -class Adjust(object): - """ - Performs color, brightness, contrast, and sharpness enhancements on the - image. See :mod:`PIL.ImageEnhance` for more imformation. - - """ - def __init__(self, color=1.0, brightness=1.0, contrast=1.0, sharpness=1.0): - """ - :param color: A number between 0 and 1 that specifies the saturation - of the image. 0 corresponds to a completely desaturated image - (black and white) and 1 to the original color. - See :class:`PIL.ImageEnhance.Color` - :param brightness: A number representing the brightness; 0 results in - a completely black image whereas 1 corresponds to the brightness - of the original. See :class:`PIL.ImageEnhance.Brightness` - :param contrast: A number representing the contrast; 0 results in a - completely gray image whereas 1 corresponds to the contrast of - the original. See :class:`PIL.ImageEnhance.Contrast` - :param sharpness: A number representing the sharpness; 0 results in a - blurred image; 1 corresponds to the original sharpness; 2 - results in a sharpened image. See - :class:`PIL.ImageEnhance.Sharpness` - - """ - self.color = color - self.brightness = brightness - self.contrast = contrast - self.sharpness = sharpness - - def process(self, img): - original = img = img.convert('RGBA') - for name in ['Color', 'Brightness', 'Contrast', 'Sharpness']: - factor = getattr(self, name.lower()) - if factor != 1.0: - try: - img = getattr(ImageEnhance, name)(img).enhance(factor) - except ValueError: - pass - else: - # PIL's Color and Contrast filters both convert the image - # to L mode, losing transparency info, so we put it back. - # See https://github.com/jdriscoll/django-imagekit/issues/64 - if name in ('Color', 'Contrast'): - img = Image.merge('RGBA', img.split()[:3] + - original.split()[3:4]) - return img - - -class Reflection(object): - """ - Creates an image with a reflection. - - """ - background_color = '#FFFFFF' - size = 0.0 - opacity = 0.6 - - def process(self, img): - # Convert bgcolor string to RGB value. - background_color = ImageColor.getrgb(self.background_color) - # Handle palleted images. - img = img.convert('RGB') - # Copy orignial image and flip the orientation. - reflection = img.copy().transpose(Image.FLIP_TOP_BOTTOM) - # Create a new image filled with the bgcolor the same size. - background = Image.new("RGB", img.size, background_color) - # Calculate our alpha mask. - start = int(255 - (255 * self.opacity)) # The start of our gradient. - steps = int(255 * self.size) # The number of intermedite values. - increment = (255 - start) / float(steps) - mask = Image.new('L', (1, 255)) - for y in range(255): - if y < steps: - val = int(y * increment + start) - else: - val = 255 - mask.putpixel((0, y), val) - alpha_mask = mask.resize(img.size) - # Merge the reflection onto our background color using the alpha mask. - reflection = Image.composite(background, reflection, alpha_mask) - # Crop the reflection. - reflection_height = int(img.size[1] * self.size) - reflection = reflection.crop((0, 0, img.size[0], reflection_height)) - # Create new image sized to hold both the original image and - # the reflection. - composite = Image.new("RGB", (img.size[0], img.size[1] + reflection_height), background_color) - # Paste the orignal image and the reflection into the composite image. - composite.paste(img, (0, 0)) - composite.paste(reflection, (0, img.size[1])) - # Return the image complete with reflection effect. - return composite - - -class Transpose(object): - """ - Rotates or flips the image. - - """ - AUTO = 'auto' - FLIP_HORIZONTAL = Image.FLIP_LEFT_RIGHT - FLIP_VERTICAL = Image.FLIP_TOP_BOTTOM - ROTATE_90 = Image.ROTATE_90 - ROTATE_180 = Image.ROTATE_180 - ROTATE_270 = Image.ROTATE_270 - - methods = [AUTO] - _EXIF_ORIENTATION_STEPS = { - 1: [], - 2: [FLIP_HORIZONTAL], - 3: [ROTATE_180], - 4: [FLIP_VERTICAL], - 5: [ROTATE_270, FLIP_HORIZONTAL], - 6: [ROTATE_270], - 7: [ROTATE_90, FLIP_HORIZONTAL], - 8: [ROTATE_90], - } - - def __init__(self, *args): - """ - Possible arguments: - - Transpose.AUTO - - Transpose.FLIP_HORIZONTAL - - Transpose.FLIP_VERTICAL - - Transpose.ROTATE_90 - - Transpose.ROTATE_180 - - Transpose.ROTATE_270 - - The order of the arguments dictates the order in which the - Transposition steps are taken. - - If Transpose.AUTO is present, all other arguments are ignored, and - the processor will attempt to rotate the image according to the - EXIF Orientation data. - - """ - super(Transpose, self).__init__() - if args: - self.methods = args - - def process(self, img): - if self.AUTO in self.methods: - try: - orientation = img._getexif()[0x0112] - ops = self._EXIF_ORIENTATION_STEPS[orientation] - except (KeyError, TypeError, AttributeError): - ops = [] - else: - ops = self.methods - for method in ops: - img = img.transpose(method) - return img - - -class AutoConvert(object): - """A processor that does some common-sense conversions based on the target - format. This includes things like preserving transparency and quantizing. - This processors is used automatically by ``ImageSpecField`` and - ``ProcessedImageField`` immediately before saving the image unless you - specify ``autoconvert=False``. - - """ - - def __init__(self, format): - self.format = format - - def process(self, img): - matte = False - self.save_kwargs = {} - self.rgba_ = img.mode == 'RGBA' - if self.rgba_: - if self.format in RGBA_TRANSPARENCY_FORMATS: - pass - elif self.format in PALETTE_TRANSPARENCY_FORMATS: - # If you're going from a format with alpha transparency to one - # with palette transparency, transparency values will be - # snapped: pixels that are more opaque than not will become - # fully opaque; pixels that are more transparent than not will - # become fully transparent. This will not produce a good-looking - # result if your image contains varying levels of opacity; in - # that case, you'll probably want to use a processor to matte - # the image on a solid color. The reason we don't matte by - # default is because not doing so allows processors to treat - # RGBA-format images as a super-type of P-format images: if you - # have an RGBA-format image with only a single transparent - # color, and save it as a GIF, it will retain its transparency. - # In other words, a P-format image converted to an - # RGBA-formatted image by a processor and then saved as a - # P-format image will give the expected results. - alpha = img.split()[-1] - mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) - img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE, - colors=255) - img.paste(255, mask) - self.save_kwargs['transparency'] = 255 - else: - # Simply converting an RGBA-format image to an RGB one creates a - # gross result, so we matte the image on a white background. If - # that's not what you want, that's fine: use a processor to deal - # with the transparency however you want. This is simply a - # sensible default that will always produce something that looks - # good. Or at least, it will look better than just a straight - # conversion. - matte = True - elif img.mode == 'P': - if self.format in PALETTE_TRANSPARENCY_FORMATS: - try: - self.save_kwargs['transparency'] = img.info['transparency'] - except KeyError: - pass - elif self.format in RGBA_TRANSPARENCY_FORMATS: - # Currently PIL doesn't support any RGBA-mode formats that - # aren't also P-mode formats, so this will never happen. - img = img.convert('RGBA') - else: - matte = True - else: - img = img.convert('RGB') - - # GIFs are always going to be in palette mode, so we can do a little - # optimization. Note that the RGBA sources also use adaptive - # quantization (above). Images that are already in P mode don't need - # any quantization because their colors are already limited. - if self.format == 'GIF': - img = img.convert('P', palette=Image.ADAPTIVE) - - if matte: - img = img.convert('RGBA') - bg = Image.new('RGBA', img.size, (255, 255, 255)) - bg.paste(img, img) - img = bg.convert('RGB') - - if self.format == 'JPEG': - self.save_kwargs['optimize'] = True - - return img - - -class Anchor(object): - """ - Defines all the anchor points needed by the various processor classes. - - """ - TOP_LEFT = 'tl' - TOP = 't' - TOP_RIGHT = 'tr' - BOTTOM_LEFT = 'bl' - BOTTOM = 'b' - BOTTOM_RIGHT = 'br' - CENTER = 'c' - LEFT = 'l' - RIGHT = 'r' - - _ANCHOR_PTS = { - TOP_LEFT: (0, 0), - TOP: (0.5, 0), - TOP_RIGHT: (1, 0), - LEFT: (0, 0.5), - CENTER: (0.5, 0.5), - RIGHT: (1, 0.5), - BOTTOM_LEFT: (0, 1), - BOTTOM: (0.5, 1), - BOTTOM_RIGHT: (1, 1), - } - - @staticmethod - def get_tuple(anchor): - """Normalizes anchor values (strings or tuples) to tuples. - - """ - # If the user passed in one of the string values, convert it to a - # percentage tuple. - if anchor in Anchor._ANCHOR_PTS.keys(): - anchor = Anchor._ANCHOR_PTS[anchor] - return anchor +from .base import * +from .crop import * +from .resize import * diff --git a/imagekit/processors/base.py b/imagekit/processors/base.py new file mode 100644 index 0000000..afc0f4f --- /dev/null +++ b/imagekit/processors/base.py @@ -0,0 +1,296 @@ +from imagekit.lib import Image, ImageColor, ImageEnhance + + +RGBA_TRANSPARENCY_FORMATS = ['PNG'] +PALETTE_TRANSPARENCY_FORMATS = ['PNG', 'GIF'] + + +class ProcessorPipeline(list): + """ + A :class:`list` of other processors. This class allows any object that + knows how to deal with a single processor to deal with a list of them. + For example:: + + processed_image = ProcessorPipeline([ProcessorA(), ProcessorB()]).process(image) + + """ + def process(self, img): + for proc in self: + img = proc.process(img) + return img + + +class Adjust(object): + """ + Performs color, brightness, contrast, and sharpness enhancements on the + image. See :mod:`PIL.ImageEnhance` for more imformation. + + """ + def __init__(self, color=1.0, brightness=1.0, contrast=1.0, sharpness=1.0): + """ + :param color: A number between 0 and 1 that specifies the saturation + of the image. 0 corresponds to a completely desaturated image + (black and white) and 1 to the original color. + See :class:`PIL.ImageEnhance.Color` + :param brightness: A number representing the brightness; 0 results in + a completely black image whereas 1 corresponds to the brightness + of the original. See :class:`PIL.ImageEnhance.Brightness` + :param contrast: A number representing the contrast; 0 results in a + completely gray image whereas 1 corresponds to the contrast of + the original. See :class:`PIL.ImageEnhance.Contrast` + :param sharpness: A number representing the sharpness; 0 results in a + blurred image; 1 corresponds to the original sharpness; 2 + results in a sharpened image. See + :class:`PIL.ImageEnhance.Sharpness` + + """ + self.color = color + self.brightness = brightness + self.contrast = contrast + self.sharpness = sharpness + + def process(self, img): + original = img = img.convert('RGBA') + for name in ['Color', 'Brightness', 'Contrast', 'Sharpness']: + factor = getattr(self, name.lower()) + if factor != 1.0: + try: + img = getattr(ImageEnhance, name)(img).enhance(factor) + except ValueError: + pass + else: + # PIL's Color and Contrast filters both convert the image + # to L mode, losing transparency info, so we put it back. + # See https://github.com/jdriscoll/django-imagekit/issues/64 + if name in ('Color', 'Contrast'): + img = Image.merge('RGBA', img.split()[:3] + + original.split()[3:4]) + return img + + +class Reflection(object): + """ + Creates an image with a reflection. + + """ + background_color = '#FFFFFF' + size = 0.0 + opacity = 0.6 + + def process(self, img): + # Convert bgcolor string to RGB value. + background_color = ImageColor.getrgb(self.background_color) + # Handle palleted images. + img = img.convert('RGB') + # Copy orignial image and flip the orientation. + reflection = img.copy().transpose(Image.FLIP_TOP_BOTTOM) + # Create a new image filled with the bgcolor the same size. + background = Image.new("RGB", img.size, background_color) + # Calculate our alpha mask. + start = int(255 - (255 * self.opacity)) # The start of our gradient. + steps = int(255 * self.size) # The number of intermedite values. + increment = (255 - start) / float(steps) + mask = Image.new('L', (1, 255)) + for y in range(255): + if y < steps: + val = int(y * increment + start) + else: + val = 255 + mask.putpixel((0, y), val) + alpha_mask = mask.resize(img.size) + # Merge the reflection onto our background color using the alpha mask. + reflection = Image.composite(background, reflection, alpha_mask) + # Crop the reflection. + reflection_height = int(img.size[1] * self.size) + reflection = reflection.crop((0, 0, img.size[0], reflection_height)) + # Create new image sized to hold both the original image and + # the reflection. + composite = Image.new("RGB", (img.size[0], img.size[1] + reflection_height), background_color) + # Paste the orignal image and the reflection into the composite image. + composite.paste(img, (0, 0)) + composite.paste(reflection, (0, img.size[1])) + # Return the image complete with reflection effect. + return composite + + +class Transpose(object): + """ + Rotates or flips the image. + + """ + AUTO = 'auto' + FLIP_HORIZONTAL = Image.FLIP_LEFT_RIGHT + FLIP_VERTICAL = Image.FLIP_TOP_BOTTOM + ROTATE_90 = Image.ROTATE_90 + ROTATE_180 = Image.ROTATE_180 + ROTATE_270 = Image.ROTATE_270 + + methods = [AUTO] + _EXIF_ORIENTATION_STEPS = { + 1: [], + 2: [FLIP_HORIZONTAL], + 3: [ROTATE_180], + 4: [FLIP_VERTICAL], + 5: [ROTATE_270, FLIP_HORIZONTAL], + 6: [ROTATE_270], + 7: [ROTATE_90, FLIP_HORIZONTAL], + 8: [ROTATE_90], + } + + def __init__(self, *args): + """ + Possible arguments: + - Transpose.AUTO + - Transpose.FLIP_HORIZONTAL + - Transpose.FLIP_VERTICAL + - Transpose.ROTATE_90 + - Transpose.ROTATE_180 + - Transpose.ROTATE_270 + + The order of the arguments dictates the order in which the + Transposition steps are taken. + + If Transpose.AUTO is present, all other arguments are ignored, and + the processor will attempt to rotate the image according to the + EXIF Orientation data. + + """ + super(Transpose, self).__init__() + if args: + self.methods = args + + def process(self, img): + if self.AUTO in self.methods: + try: + orientation = img._getexif()[0x0112] + ops = self._EXIF_ORIENTATION_STEPS[orientation] + except (KeyError, TypeError, AttributeError): + ops = [] + else: + ops = self.methods + for method in ops: + img = img.transpose(method) + return img + + +class AutoConvert(object): + """A processor that does some common-sense conversions based on the target + format. This includes things like preserving transparency and quantizing. + This processors is used automatically by ``ImageSpecField`` and + ``ProcessedImageField`` immediately before saving the image unless you + specify ``autoconvert=False``. + + """ + + def __init__(self, format): + self.format = format + + def process(self, img): + matte = False + self.save_kwargs = {} + self.rgba_ = img.mode == 'RGBA' + if self.rgba_: + if self.format in RGBA_TRANSPARENCY_FORMATS: + pass + elif self.format in PALETTE_TRANSPARENCY_FORMATS: + # If you're going from a format with alpha transparency to one + # with palette transparency, transparency values will be + # snapped: pixels that are more opaque than not will become + # fully opaque; pixels that are more transparent than not will + # become fully transparent. This will not produce a good-looking + # result if your image contains varying levels of opacity; in + # that case, you'll probably want to use a processor to matte + # the image on a solid color. The reason we don't matte by + # default is because not doing so allows processors to treat + # RGBA-format images as a super-type of P-format images: if you + # have an RGBA-format image with only a single transparent + # color, and save it as a GIF, it will retain its transparency. + # In other words, a P-format image converted to an + # RGBA-formatted image by a processor and then saved as a + # P-format image will give the expected results. + alpha = img.split()[-1] + mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) + img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE, + colors=255) + img.paste(255, mask) + self.save_kwargs['transparency'] = 255 + else: + # Simply converting an RGBA-format image to an RGB one creates a + # gross result, so we matte the image on a white background. If + # that's not what you want, that's fine: use a processor to deal + # with the transparency however you want. This is simply a + # sensible default that will always produce something that looks + # good. Or at least, it will look better than just a straight + # conversion. + matte = True + elif img.mode == 'P': + if self.format in PALETTE_TRANSPARENCY_FORMATS: + try: + self.save_kwargs['transparency'] = img.info['transparency'] + except KeyError: + pass + elif self.format in RGBA_TRANSPARENCY_FORMATS: + # Currently PIL doesn't support any RGBA-mode formats that + # aren't also P-mode formats, so this will never happen. + img = img.convert('RGBA') + else: + matte = True + else: + img = img.convert('RGB') + + # GIFs are always going to be in palette mode, so we can do a little + # optimization. Note that the RGBA sources also use adaptive + # quantization (above). Images that are already in P mode don't need + # any quantization because their colors are already limited. + if self.format == 'GIF': + img = img.convert('P', palette=Image.ADAPTIVE) + + if matte: + img = img.convert('RGBA') + bg = Image.new('RGBA', img.size, (255, 255, 255)) + bg.paste(img, img) + img = bg.convert('RGB') + + if self.format == 'JPEG': + self.save_kwargs['optimize'] = True + + return img + + +class Anchor(object): + """ + Defines all the anchor points needed by the various processor classes. + + """ + TOP_LEFT = 'tl' + TOP = 't' + TOP_RIGHT = 'tr' + BOTTOM_LEFT = 'bl' + BOTTOM = 'b' + BOTTOM_RIGHT = 'br' + CENTER = 'c' + LEFT = 'l' + RIGHT = 'r' + + _ANCHOR_PTS = { + TOP_LEFT: (0, 0), + TOP: (0.5, 0), + TOP_RIGHT: (1, 0), + LEFT: (0, 0.5), + CENTER: (0.5, 0.5), + RIGHT: (1, 0.5), + BOTTOM_LEFT: (0, 1), + BOTTOM: (0.5, 1), + BOTTOM_RIGHT: (1, 1), + } + + @staticmethod + def get_tuple(anchor): + """Normalizes anchor values (strings or tuples) to tuples. + + """ + # If the user passed in one of the string values, convert it to a + # percentage tuple. + if anchor in Anchor._ANCHOR_PTS.keys(): + anchor = Anchor._ANCHOR_PTS[anchor] + return anchor diff --git a/imagekit/processors/crop.py b/imagekit/processors/crop.py index 5f7af38..da5c0fb 100644 --- a/imagekit/processors/crop.py +++ b/imagekit/processors/crop.py @@ -1,4 +1,4 @@ -from . import Anchor +from .base import Anchor from .utils import histogram_entropy from ..lib import Image, ImageChops, ImageDraw, ImageStat diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index c18fc47..721768a 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -1,7 +1,6 @@ from imagekit.lib import Image -from . import crop import warnings -from . import Anchor +from .base import Anchor class Resize(object): @@ -26,7 +25,7 @@ class Cover(object): """ Resizes the image to the smallest possible size that will entirely cover the provided dimensions. You probably won't be using this processor directly, - but it's used internally by ``Fill`` and ``SmartFill``. + but it's used internally by ``ResizeToFill`` and ``SmartResize``. """ def __init__(self, width, height): @@ -46,7 +45,7 @@ class Cover(object): return Resize(new_width, new_height).process(img) -class Fill(object): +class ResizeToFill(object): """ Resizes an image, cropping it to the exact specified width and height. @@ -64,16 +63,17 @@ class Fill(object): self.anchor = anchor def process(self, img): + from .crop import Crop img = Cover(self.width, self.height).process(img) - return crop.Crop(self.width, self.height, + return Crop(self.width, self.height, anchor=self.anchor).process(img) -class SmartFill(object): +class SmartResize(object): """ - The ``SmartFill`` processor is identical to ``Fill``, except that it uses - entropy to crop the image instead of a user-specified anchor point. - Internally, it simply runs the ``resize.Cover`` and ``crop.SmartCrop`` + The ``SmartResize`` processor is identical to ``ResizeToFill``, except that + it uses entropy to crop the image instead of a user-specified anchor point. + Internally, it simply runs the ``ResizeToCover`` and ``SmartCrop`` processors in series. """ def __init__(self, width, height): @@ -85,8 +85,9 @@ class SmartFill(object): self.width, self.height = width, height def process(self, img): + from .crop import SmartCrop img = Cover(self.width, self.height).process(img) - return crop.SmartCrop(self.width, self.height).process(img) + return SmartCrop(self.width, self.height).process(img) class ResizeCanvas(object): @@ -217,11 +218,3 @@ class ResizeToFit(object): if self.mat_color: img = ResizeCanvas(self.width, self.height, self.mat_color, anchor=self.anchor).process(img) return img - - -class SmartCrop(crop.SmartCrop): - def __init__(self, *args, **kwargs): - warnings.warn('The SmartCrop processor has been moved to' - ' `imagekit.processors.crop.SmartCrop`, where it belongs.', - DeprecationWarning) - super(SmartCrop, self).__init__(*args, **kwargs)