Login

A Django form field that accepts an uploaded image and creates a resized image attached with a push-pin

Author:
rmisio
Posted:
May 30, 2011
Language:
Python
Version:
1.2
Tags:
django image thumbnail form field custom-field
Score:
0 (after 0 ratings)

PushPinImageField is a Django form field that is a sub-class of an ImageField. The field accepts an image to upload and based on certain settings, notably the size of the resulting image, the sizes and colors of 3 different borders, as well as the color of the push pin, a re-sized image is created with a colorized push pin in the top-center.

A live demo of the field as well as more detailed usage instructions are available as a blog entry.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
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)

More like this

  1. Handling Image uploading and Delete Old Image in Django by mulianto 3 years ago
  2. Amazon S3 browser-based upload form by simon 5 years, 4 months ago
  3. Custom FileField with content type and size validation by nemesis 4 years, 8 months ago
  4. Image gradients on the fly by jensbreit 6 years, 11 months ago
  5. HTML Hex Color Field by claudelacey 5 years, 12 months ago

Comments

Please login first before commenting.