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)
|
Comments