From cfd503a2eb801bca868d0fee56ccd2b529dab24c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 15 Nov 2011 23:42:43 -0500 Subject: [PATCH 1/5] TrimColor processor --- imagekit/lib.py | 5 +++- imagekit/processors/crop.py | 60 +++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 imagekit/processors/crop.py diff --git a/imagekit/lib.py b/imagekit/lib.py index d8b2aed..efacb79 100644 --- a/imagekit/lib.py +++ b/imagekit/lib.py @@ -1,7 +1,8 @@ # Required PIL classes may or may not be available from the root namespace # depending on the installation method used. try: - from PIL import Image, ImageColor, ImageChops, ImageEnhance, ImageFile, ImageFilter + from PIL import Image, ImageColor, ImageChops, ImageEnhance, ImageFile, \ + ImageFilter, ImageDraw, ImageStat except ImportError: try: import Image @@ -10,5 +11,7 @@ except ImportError: import ImageEnhance import ImageFile import ImageFilter + import ImageDraw + import ImageStat except ImportError: raise ImportError('ImageKit was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path.') diff --git a/imagekit/processors/crop.py b/imagekit/processors/crop.py new file mode 100644 index 0000000..ff2ea14 --- /dev/null +++ b/imagekit/processors/crop.py @@ -0,0 +1,60 @@ +from ..lib import Image, ImageChops, ImageDraw, ImageStat + + +class Side(object): + TOP = 't' + RIGHT = 'r' + BOTTOM = 'b' + LEFT = 'l' + ALL = (TOP, RIGHT, BOTTOM, LEFT) + + +def crop(img, bbox, sides=Side.ALL): + bbox = ( + bbox[0] if Side.LEFT in sides else 0, + bbox[1] if Side.TOP in sides else 0, + bbox[2] if Side.RIGHT in sides else img.size[0], + bbox[3] if Side.BOTTOM in sides else img.size[1], + ) + return img.crop(bbox) + + +def detect_border_color(img): + mask = Image.new('1', img.size, 1) + w, h = img.size[0] - 2, img.size[1] - 2 + if w > 0 and h > 0: + draw = ImageDraw.Draw(mask) + draw.rectangle([1, 1, w, h], 0) + return ImageStat.Stat(img.convert('RGBA').histogram(mask)).median + + +class TrimColor(object): + """Trims a color from the sides of an image. + + """ + def __init__(self, color=None, tolerance=0.3, sides=Side.ALL): + """ + :param color: The color to trim from the image, in a 4-tuple RGBA value, + where each component is an integer between 0 and 255, inclusive. If + no color is provided, the processor will attempt to detect the + border color automatically. + :param tolerance: A number between 0 and 1 where 0. Zero is the least + tolerant and one is the most. + :param sides: A list of sides that should be trimmed. Possible values + are provided by the :class:`Side` enum class. + + """ + self.color = color + self.sides = sides + self.tolerance = tolerance + + def process(self, img): + source = img.convert('RGBA') + border_color = self.color or tuple(detect_border_color(source)) + bg = Image.new('RGBA', img.size, border_color) + offset = -int(self.tolerance * 255) + bbox = ImageChops.subtract(bg, source, offset=offset).getbbox() + if bbox: + img = crop(img, bbox, self.sides) + + return img From ed6403276297ed56fa7215b8cc48a02a1eff5e06 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 16 Nov 2011 15:16:24 -0500 Subject: [PATCH 2/5] Renames processor to `TrimBorderColor` --- imagekit/processors/crop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/processors/crop.py b/imagekit/processors/crop.py index ff2ea14..ebf1475 100644 --- a/imagekit/processors/crop.py +++ b/imagekit/processors/crop.py @@ -28,7 +28,7 @@ def detect_border_color(img): return ImageStat.Stat(img.convert('RGBA').histogram(mask)).median -class TrimColor(object): +class TrimBorderColor(object): """Trims a color from the sides of an image. """ From 25be1f66ca9b6cee5ca7aa567360de3712873a5f Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 16 Nov 2011 15:17:11 -0500 Subject: [PATCH 3/5] Explicitly import crop module So you can `import crop from imagekit.processors` --- imagekit/processors/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imagekit/processors/__init__.py b/imagekit/processors/__init__.py index 51b6282..95d2282 100644 --- a/imagekit/processors/__init__.py +++ b/imagekit/processors/__init__.py @@ -8,7 +8,7 @@ from both the filesystem and the ORM. """ from imagekit.lib import Image, ImageColor, ImageEnhance -from imagekit.processors import resize +from imagekit.processors import resize, crop RGBA_TRANSPARENCY_FORMATS = ['PNG'] From 5579c8db3c90cfedfcbbd44b6bcd3feb9385438e Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Tue, 22 Nov 2011 23:12:54 -0500 Subject: [PATCH 4/5] Adds crop module to docs --- docs/apireference.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/apireference.rst b/docs/apireference.rst index c997026..cbaca49 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -18,6 +18,9 @@ API Reference .. automodule:: imagekit.processors.resize :members: +.. automodule:: imagekit.processors.crop +:members: + :mod:`admin` Module -------------------- From 358bb1f6b0aaea7c24235f7962822c08b340d18d Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Mon, 5 Dec 2011 17:44:25 -0500 Subject: [PATCH 5/5] Using difference instead of subtract This corrects the border removal, however, I'm not certain whether the interpretation of "tolerance" gels with expectations. --- imagekit/processors/crop.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/imagekit/processors/crop.py b/imagekit/processors/crop.py index ebf1475..aca43d3 100644 --- a/imagekit/processors/crop.py +++ b/imagekit/processors/crop.py @@ -52,8 +52,19 @@ class TrimBorderColor(object): source = img.convert('RGBA') border_color = self.color or tuple(detect_border_color(source)) bg = Image.new('RGBA', img.size, border_color) - offset = -int(self.tolerance * 255) - bbox = ImageChops.subtract(bg, source, offset=offset).getbbox() + diff = ImageChops.difference(source, bg) + if self.tolerance not in (0, 1): + # If tolerance is zero, we've already done the job. A tolerance of + # one would mean to trim EVERY color, and since that would result + # in a zero-sized image, we just ignore it. + if not 0 <= self.tolerance <= 1: + raise ValueError('%s is an invalid tolerance. Acceptable values' + ' are between 0 and 1 (inclusive).' % self.tolerance) + tmp = ImageChops.constant(diff, int(self.tolerance * 255)) \ + .convert('RGBA') + diff = ImageChops.subtract(diff, tmp) + + bbox = diff.getbbox() if bbox: img = crop(img, bbox, self.sides)