mirror of
https://github.com/Hopiu/django-imagekit.git
synced 2026-03-17 05:40:25 +00:00
225 lines
7.2 KiB
Python
225 lines
7.2 KiB
Python
from ..lib import Image, ImageChops, ImageDraw, ImageStat
|
|
from .utils import histogram_entropy
|
|
|
|
|
|
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 TrimBorderColor(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)
|
|
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)
|
|
return img
|
|
|
|
|
|
class BasicCrop(object):
|
|
"""Crops an image to the specified rectangular region.
|
|
|
|
"""
|
|
def __init__(self, left, top, right, bottom):
|
|
self.left = left
|
|
self.top = top
|
|
self.right = right
|
|
self.bottom = bottom
|
|
|
|
def process(self, img):
|
|
box = (self.left, self.top, self.right, self.bottom)
|
|
img = img.crop(box)
|
|
return img
|
|
|
|
|
|
class Crop(object):
|
|
"""
|
|
Crops an image , cropping it to the specified width and height
|
|
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):
|
|
"""
|
|
: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
|
|
|
|
"""
|
|
self.width = width
|
|
self.height = height
|
|
self.anchor = anchor
|
|
|
|
def process(self, img):
|
|
cur_width, cur_height = img.size
|
|
horizontal_anchor, vertical_anchor = Crop._ANCHOR_PTS[self.anchor or \
|
|
Crop.CENTER]
|
|
print horizontal_anchor, self.width, cur_width
|
|
crop_x, crop_y = (abs(self.width - cur_width), abs(self.height - cur_height))
|
|
x_diff, y_diff = (int(crop_x / 2), int(crop_y / 2))
|
|
box_left, box_right = {
|
|
0: (0, self.width),
|
|
0.5: (int(x_diff), int(x_diff + self.width)),
|
|
1: (int(crop_x), int(cur_width)),
|
|
}[horizontal_anchor]
|
|
box_upper, box_lower = {
|
|
0: (0, self.height),
|
|
0.5: (int(y_diff), int(y_diff + self.height)),
|
|
1: (int(crop_y), int(cur_height)),
|
|
}[vertical_anchor]
|
|
box = (box_left, box_upper, box_right, box_lower)
|
|
img = img.crop(box)
|
|
return img
|
|
|
|
|
|
class SmartCrop(object):
|
|
"""
|
|
Crop an image 'smartly' -- based on smart crop implementation from easy-thumbnails:
|
|
|
|
https://github.com/SmileyChris/easy-thumbnails/blob/master/easy_thumbnails/processors.py#L193
|
|
|
|
Smart cropping whittles away the parts of the image with the least entropy.
|
|
|
|
"""
|
|
|
|
def __init__(self, width=None, height=None):
|
|
self.width = width
|
|
self.height = height
|
|
|
|
def compare_entropy(self, start_slice, end_slice, slice, difference):
|
|
"""
|
|
Calculate the entropy of two slices (from the start and end of an axis),
|
|
returning a tuple containing the amount that should be added to the start
|
|
and removed from the end of the axis.
|
|
|
|
"""
|
|
start_entropy = histogram_entropy(start_slice)
|
|
end_entropy = histogram_entropy(end_slice)
|
|
|
|
if end_entropy and abs(start_entropy / end_entropy - 1) < 0.01:
|
|
# Less than 1% difference, remove from both sides.
|
|
if difference >= slice * 2:
|
|
return slice, slice
|
|
half_slice = slice // 2
|
|
return half_slice, slice - half_slice
|
|
|
|
if start_entropy > end_entropy:
|
|
return 0, slice
|
|
else:
|
|
return slice, 0
|
|
|
|
def process(self, img):
|
|
source_x, source_y = img.size
|
|
diff_x = int(source_x - min(source_x, self.width))
|
|
diff_y = int(source_y - min(source_y, self.height))
|
|
left = top = 0
|
|
right, bottom = source_x, source_y
|
|
|
|
while diff_x:
|
|
slice = min(diff_x, max(diff_x // 5, 10))
|
|
start = img.crop((left, 0, left + slice, source_y))
|
|
end = img.crop((right - slice, 0, right, source_y))
|
|
add, remove = self.compare_entropy(start, end, slice, diff_x)
|
|
left += add
|
|
right -= remove
|
|
diff_x = diff_x - add - remove
|
|
|
|
while diff_y:
|
|
slice = min(diff_y, max(diff_y // 5, 10))
|
|
start = img.crop((0, top, source_x, top + slice))
|
|
end = img.crop((0, bottom - slice, source_x, bottom))
|
|
add, remove = self.compare_entropy(start, end, slice, diff_y)
|
|
top += add
|
|
bottom -= remove
|
|
diff_y = diff_y - add - remove
|
|
|
|
box = (left, top, right, bottom)
|
|
img = img.crop(box)
|
|
return img
|