mirror of
https://github.com/Hopiu/django-imagekit.git
synced 2026-03-17 05:40:25 +00:00
In the old IK API, processors (like `Transpose`) were able to access the file by inspecting the model instance (which carried an options object that specified the attribute name of the ImageField from which the file could be extracted). Since the new API allows for multiple ImageFields (and because IKOptions have been removed), it became necessary to provide more information. Initially, this was accomplished by passing the spec to `process()`, however with the addition of ProcessedImageField, it became clear the a cleaner solution was to pass only the field file (ImageSpecFile or ProcessedImageFieldFile). This keeps the ORM stuff (fields, etc.) out of the `ImageProcessor` API but (because field files, not just regular files, are passed) the average hacker can still have their processor make use of model information by accessing the model through the file's `instance` property.
210 lines
6.9 KiB
Python
210 lines
6.9 KiB
Python
""" Imagekit Image "ImageProcessors"
|
|
|
|
A processor defines a set of class variables (optional) and a
|
|
class method named "process" which processes the supplied image using
|
|
the class properties as settings. The process method can be overridden as well allowing user to define their
|
|
own effects/processes entirely.
|
|
|
|
"""
|
|
from imagekit.lib import *
|
|
|
|
class ImageProcessor(object):
|
|
""" Base image processor class """
|
|
|
|
def process(self, img, fmt, file):
|
|
return img, fmt
|
|
|
|
|
|
class Adjust(ImageProcessor):
|
|
|
|
def __init__(self, color=1.0, brightness=1.0, contrast=1.0, sharpness=1.0):
|
|
self.color = color
|
|
self.brightness = brightness
|
|
self.contrast = contrast
|
|
self.sharpness = sharpness
|
|
|
|
def process(self, img, fmt, file):
|
|
img = img.convert('RGB')
|
|
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
|
|
return img, fmt
|
|
|
|
|
|
class Reflection(ImageProcessor):
|
|
background_color = '#FFFFFF'
|
|
size = 0.0
|
|
opacity = 0.6
|
|
|
|
def process(self, img, fmt, file):
|
|
# 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]))
|
|
# Save the file as a JPEG
|
|
fmt = 'JPEG'
|
|
# return the image complete with reflection effect
|
|
return composite, fmt
|
|
|
|
|
|
class _Resize(ImageProcessor):
|
|
|
|
width = None
|
|
height = None
|
|
|
|
def __init__(self, width=None, height=None):
|
|
if width is not None:
|
|
self.width = width
|
|
if height is not None:
|
|
self.height = height
|
|
|
|
def process(self, img, fmt, file):
|
|
raise NotImplementedError('process must be overridden by subclasses.')
|
|
|
|
|
|
class Crop(_Resize):
|
|
|
|
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):
|
|
super(Crop, self).__init__(width, height)
|
|
self.anchor = anchor
|
|
|
|
def process(self, img, fmt, file):
|
|
cur_width, cur_height = img.size
|
|
horizontal_anchor, vertical_anchor = Crop._ANCHOR_PTS[self.anchor or \
|
|
Crop.CENTER]
|
|
ratio = max(float(self.width)/cur_width, float(self.height)/cur_height)
|
|
resize_x, resize_y = ((cur_width * ratio), (cur_height * ratio))
|
|
crop_x, crop_y = (abs(self.width - resize_x), abs(self.height - resize_y))
|
|
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(resize_x)),
|
|
}[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(resize_y)),
|
|
}[vertical_anchor]
|
|
box = (box_left, box_upper, box_right, box_lower)
|
|
img = img.resize((int(resize_x), int(resize_y)), Image.ANTIALIAS).crop(box)
|
|
return img, fmt
|
|
|
|
|
|
class Fit(_Resize):
|
|
def __init__(self, width=None, height=None, upscale=None):
|
|
super(Fit, self).__init__(width, height)
|
|
self.upscale = upscale
|
|
|
|
def process(self, img, fmt, file):
|
|
cur_width, cur_height = img.size
|
|
if not self.width is None and not self.height is None:
|
|
ratio = min(float(self.width)/cur_width,
|
|
float(self.height)/cur_height)
|
|
else:
|
|
if self.width is None:
|
|
ratio = float(self.height)/cur_height
|
|
else:
|
|
ratio = float(self.width)/cur_width
|
|
new_dimensions = (int(round(cur_width*ratio)),
|
|
int(round(cur_height*ratio)))
|
|
if new_dimensions[0] > cur_width or \
|
|
new_dimensions[1] > cur_height:
|
|
if not self.upscale:
|
|
return img, fmt
|
|
img = img.resize(new_dimensions, Image.ANTIALIAS)
|
|
return img, fmt
|
|
|
|
|
|
class Transpose(ImageProcessor):
|
|
""" Rotates or flips the image
|
|
|
|
Method should be one of the following strings:
|
|
- FLIP_LEFT RIGHT
|
|
- FLIP_TOP_BOTTOM
|
|
- ROTATE_90
|
|
- ROTATE_270
|
|
- ROTATE_180
|
|
- auto
|
|
|
|
If method is set to 'auto' the processor will attempt to rotate the image
|
|
according to the EXIF Orientation data.
|
|
|
|
"""
|
|
EXIF_ORIENTATION_STEPS = {
|
|
1: [],
|
|
2: ['FLIP_LEFT_RIGHT'],
|
|
3: ['ROTATE_180'],
|
|
4: ['FLIP_TOP_BOTTOM'],
|
|
5: ['ROTATE_270', 'FLIP_LEFT_RIGHT'],
|
|
6: ['ROTATE_270'],
|
|
7: ['ROTATE_90', 'FLIP_LEFT_RIGHT'],
|
|
8: ['ROTATE_90'],
|
|
}
|
|
|
|
method = 'auto'
|
|
|
|
def process(self, img, fmt, file):
|
|
if self.method == 'auto':
|
|
try:
|
|
orientation = Image.open(file.file)._getexif()[0x0112]
|
|
ops = self.EXIF_ORIENTATION_STEPS[orientation]
|
|
except:
|
|
ops = []
|
|
else:
|
|
ops = [self.method]
|
|
for method in ops:
|
|
img = img.transpose(getattr(Image, method))
|
|
return img, fmt
|