from PIL import Image from django.db.models.fields.files import ImageField, ImageFieldFile from django.core.files.base import ContentFile import os import math import re import colorsys try: from cStringIO import StringIO except ImportError: from StringIO import StringIO class PushPinImageFieldFile(ImageFieldFile): def hex_to_rgb(self, value): value = value.lstrip('#') lv = len(value) return tuple(int(value[i:i+lv/3], 16) for i in range(0, lv, lv/3)) def max_size(self, width, height): ratio = float(width) / height if width > self.field.max_width: width = self.field.max_width height = int(math.floor(width / ratio)) if height > self.field.max_height: height = self.field.max_height width = int(math.floor(height * ratio)) return width, height def crop_to_fixed(self, img, width, height): """ Returns a scaled down and cropped version of the image based on the provided size values. """ ratio = float(img.size[0]) / img.size[1] w = img.size[0] h = img.size[1] if (float(w) / width) >= (float(h) / height): new_h = height new_w = int(round(height * ratio)) else: new_w = width new_h = int(round(width / ratio)) img = img.resize((new_w, new_h), Image.ANTIALIAS) if (new_w, new_h) != (width, height): # crop needed if new_w > width: start_x = int(round(float(new_w - width) / 2)) img = img.crop((start_x, 0, start_x + width, new_h)) elif new_w < width: img = img.resize((width, new_h), Image.ANTIALIAS) if new_h > height: start_y = int(round(float(new_h - height) / 2)) img = img.crop((0, start_y, new_w, start_y + height)) elif new_h < height: img = img.resize((new_w, height), Image.ANTIALIAS) return img def save(self, name, content, save=True): push_pin_im = Image.open(self.field.push_pin_path) new_content = StringIO() content.file.seek(0) in_image = Image.open(content.file) # if fixed size given, max will be ignored if self.field.fixed_width > 0 and self.field.fixed_height > 0: in_width, in_height = self.field.fixed_width, self.field.fixed_height else: # get adjusted height and width values in_width, in_height = self.max_size(in_image.size[0], in_image.size[1]) # adjust further based on borders and stroke new_in_height = in_height - (self.field.border[0] * 2) new_in_width = in_width - (self.field.border[0] * 2) # scale or crop the input image if self.field.fixed_width or self.field.fixed_height: scaled_in_image = self.crop_to_fixed(in_image, new_in_width, new_in_height) else: scaled_in_image = in_image.resize((new_in_width, new_in_height), Image.ANTIALIAS) new_im = Image.new('RGB', (in_width, in_height)) # outer stroke if self.field.outer_border_stroke[0]: new_im.paste(self.hex_to_rgb(self.field.outer_border_stroke[1]), (0, 0, in_width, in_height)) # the main border if self.field.border[0]: new_im.paste(self.hex_to_rgb(self.field.border[1]), (self.field.outer_border_stroke[0], self.field.outer_border_stroke[0], in_width - self.field.outer_border_stroke[0], in_height - self.field.outer_border_stroke[0] )) offset_x = float(in_width - new_in_width) / 2 offset_y = float(in_height - new_in_height) / 2 more_offset_x = 0 if offset_x % 1: more_offset_x = 1 more_offset_y = 0 if offset_y % 1: more_offset_y = 1 offset_x = int(math.floor(offset_x)) offset_y = int(math.floor(offset_y)) # inner stroke if self.field.inner_border_stroke[0]: new_im.paste(self.hex_to_rgb(self.field.inner_border_stroke[1]), ( offset_x - self.field.inner_border_stroke[0], offset_y - self.field.inner_border_stroke[0], in_width - (offset_x + more_offset_x) + self.field.inner_border_stroke[0], in_height - (offset_y + more_offset_y) + self.field.inner_border_stroke[0] )) # paste the scaled-down and centered input image new_im.paste(scaled_in_image, ( offset_x, offset_y, offset_x + new_in_width, offset_y + new_in_height )) # colorize the push pin push_pin_im = push_pin_im.convert('RGBA') pixdata = push_pin_im.load() rgb_push_pin = self.hex_to_rgb(self.field.push_pin_color) push_pin_hsv = colorsys.rgb_to_hsv(float(rgb_push_pin[0])/255, float(rgb_push_pin[1])/255, float(rgb_push_pin[2])/255) push_pin_hue = push_pin_hsv[0] push_pin_s = push_pin_hsv[1] highest_s = list(colorsys.rgb_to_hsv(float(pixdata[16, 20][0])/255, float(pixdata[16, 20][1])/255, float(pixdata[16, 20][2])/255))[1] s_offset = push_pin_s - highest_s for y in xrange(push_pin_im.size[1]): for x in xrange(push_pin_im.size[0]): hsv = list(colorsys.rgb_to_hsv(float(pixdata[x, y][0])/255, float(pixdata[x, y][1])/255, float(pixdata[x, y][2])/255)) if hsv[0]>0: hsv[0] = push_pin_hue hsv[1] += s_offset new_rgb = colorsys.hsv_to_rgb(hsv[0], hsv[1], hsv[2]) pixdata[x, y] = (int(round(new_rgb[0] * 255)), int(round(new_rgb[1] * 255)), int(round(new_rgb[2] * 255)), pixdata[x, y][3]) # paste in the push pin pin_x = int(round(float(in_width - push_pin_im.size[1]) / 2)) + 8 new_im.paste(push_pin_im, ( pin_x, 1, pin_x + push_pin_im.size[0], 1 + push_pin_im.size[1] ), push_pin_im) new_im.save(new_content, format=self.field.format, quality=100) new_content = ContentFile(new_content.getvalue()) super(PushPinImageFieldFile, self).save(name, new_content, save) class PushPinImageField(ImageField): attr_class = PushPinImageFieldFile @staticmethod def is_valid_hex_color(value): if not value.startswith('#') or len(value) != 7 or not re.match('^[A-F0-9]*$', value[1:7].upper()): return False else: return True def check_color(self, value, field): if not PushPinImageField.is_valid_hex_color(value): raise ValueError('The color value for the %s field must contain a valid 6 digit hex color value' \ ' and must start with a hash(#) character. Short-hand notation (e.g. #FFF) is' \ ' not acceptable.' % field) def __init__(self, push_pin_path, max_width=150, max_height=100, fixed_width=0, fixed_height=0, push_pin_color='#ffff33', format='PNG', border=(0, '#ffffff'), inner_border_stroke=(0,'#000000'), outer_border_stroke=(0, '#000000'), *args, **kwargs): self.check_color(push_pin_color, 'push_pin_color') self.check_color(border[1], 'border') self.check_color(inner_border_stroke[1], 'inner_border_stroke') self.check_color(outer_border_stroke[1], 'outer_border_stroke') if inner_border_stroke[0] + outer_border_stroke[0] > border[0]: raise ValueError('The sum of the sizes of the innner_border_stroke and outer_border_stroke' \ ' cannot exceed the size of the border.') self.push_pin_path = push_pin_path self.max_width = max_width self.max_height = max_height self.fixed_width = fixed_width self.fixed_height = fixed_height self.push_pin_color = push_pin_color self.format = format self.border = border self.inner_border_stroke = inner_border_stroke self.outer_border_stroke = outer_border_stroke super(PushPinImageField, self).__init__(*args, **kwargs)