1import itertools 2import re 3 4from django.utils import six 5try: 6 from PIL import Image, ImageChops, ImageFilter 7except ImportError: 8 import Image 9 import ImageChops 10 import ImageFilter 11 12from easy_thumbnails import utils 13 14 15def _compare_entropy(start_slice, end_slice, slice, difference): 16 """ 17 Calculate the entropy of two slices (from the start and end of an axis), 18 returning a tuple containing the amount that should be added to the start 19 and removed from the end of the axis. 20 21 """ 22 start_entropy = utils.image_entropy(start_slice) 23 end_entropy = utils.image_entropy(end_slice) 24 if end_entropy and abs(start_entropy / end_entropy - 1) < 0.01: 25 # Less than 1% difference, remove from both sides. 26 if difference >= slice * 2: 27 return slice, slice 28 half_slice = slice // 2 29 return half_slice, slice - half_slice 30 if start_entropy > end_entropy: 31 return 0, slice 32 else: 33 return slice, 0 34 35 36def _points_table(): 37 """ 38 Iterable to map a 16 bit grayscale image to 8 bits. 39 """ 40 for i in range(256): 41 for j in itertools.repeat(i, 256): 42 yield j 43 44 45def colorspace(im, bw=False, replace_alpha=False, **kwargs): 46 """ 47 Convert images to the correct color space. 48 49 A passive option (i.e. always processed) of this method is that all images 50 (unless grayscale) are converted to RGB colorspace. 51 52 This processor should be listed before :func:`scale_and_crop` so palette is 53 changed before the image is resized. 54 55 bw 56 Make the thumbnail grayscale (not really just black & white). 57 58 replace_alpha 59 Replace any transparency layer with a solid color. For example, 60 ``replace_alpha='#fff'`` would replace the transparency layer with 61 white. 62 63 """ 64 if im.mode == 'I': 65 # PIL (and pillow) have can't convert 16 bit grayscale images to lower 66 # modes, so manually convert them to an 8 bit grayscale. 67 im = im.point(list(_points_table()), 'L') 68 69 is_transparent = utils.is_transparent(im) 70 is_grayscale = im.mode in ('L', 'LA') 71 new_mode = im.mode 72 if is_grayscale or bw: 73 new_mode = 'L' 74 else: 75 new_mode = 'RGB' 76 77 if is_transparent: 78 if replace_alpha: 79 if im.mode != 'RGBA': 80 im = im.convert('RGBA') 81 base = Image.new('RGBA', im.size, replace_alpha) 82 base.paste(im, mask=im) 83 im = base 84 else: 85 new_mode = new_mode + 'A' 86 87 if im.mode != new_mode: 88 im = im.convert(new_mode) 89 90 return im 91 92 93def autocrop(im, autocrop=False, **kwargs): 94 """ 95 Remove any unnecessary whitespace from the edges of the source image. 96 97 This processor should be listed before :func:`scale_and_crop` so the 98 whitespace is removed from the source image before it is resized. 99 100 autocrop 101 Activates the autocrop method for this image. 102 103 """ 104 if autocrop: 105 # If transparent, flatten. 106 if utils.is_transparent(im): 107 no_alpha = Image.new('L', im.size, (255)) 108 no_alpha.paste(im, mask=im.split()[-1]) 109 else: 110 no_alpha = im.convert('L') 111 # Convert to black and white image. 112 bw = no_alpha.convert('L') 113 # bw = bw.filter(ImageFilter.MedianFilter) 114 # White background. 115 bg = Image.new('L', im.size, 255) 116 bbox = ImageChops.difference(bw, bg).getbbox() 117 if bbox: 118 im = im.crop(bbox) 119 return im 120 121 122def scale_and_crop(im, size, crop=False, upscale=False, zoom=None, target=None, 123 **kwargs): 124 """ 125 Handle scaling and cropping the source image. 126 127 Images can be scaled / cropped against a single dimension by using zero 128 as the placeholder in the size. For example, ``size=(100, 0)`` will cause 129 the image to be resized to 100 pixels wide, keeping the aspect ratio of 130 the source image. 131 132 crop 133 Crop the source image height or width to exactly match the requested 134 thumbnail size (the default is to proportionally resize the source 135 image to fit within the requested thumbnail size). 136 137 By default, the image is centered before being cropped. To crop from 138 the edges, pass a comma separated string containing the ``x`` and ``y`` 139 percentage offsets (negative values go from the right/bottom). Some 140 examples follow: 141 142 * ``crop="0,0"`` will crop from the left and top edges. 143 144 * ``crop="-10,-0"`` will crop from the right edge (with a 10% offset) 145 and the bottom edge. 146 147 * ``crop=",0"`` will keep the default behavior for the x axis 148 (horizontally centering the image) and crop from the top edge. 149 150 The image can also be "smart cropped" by using ``crop="smart"``. The 151 image is incrementally cropped down to the requested size by removing 152 slices from edges with the least entropy. 153 154 Finally, you can use ``crop="scale"`` to simply scale the image so that 155 at least one dimension fits within the size dimensions given (you may 156 want to use the upscale option too). 157 158 upscale 159 Allow upscaling of the source image during scaling. 160 161 zoom 162 A percentage to zoom in on the scaled image. For example, a zoom of 163 ``40`` will clip 20% off each side of the source image before 164 thumbnailing. 165 166 target 167 Set the focal point as a percentage for the image if it needs to be 168 cropped (defaults to ``(50, 50)``). 169 170 For example, ``target="10,20"`` will set the focal point as 10% and 20% 171 from the left and top of the image, respectively. If the image needs to 172 be cropped, it will trim off the right and bottom edges until the focal 173 point is centered. 174 175 Can either be set as a two-item tuple such as ``(20, 30)`` or a comma 176 separated string such as ``"20,10"``. 177 178 A null value such as ``(20, None)`` or ``",60"`` will default to 50%. 179 """ 180 source_x, source_y = [float(v) for v in im.size] 181 target_x, target_y = [int(v) for v in size] 182 183 if crop or not target_x or not target_y: 184 scale = max(target_x / source_x, target_y / source_y) 185 else: 186 scale = min(target_x / source_x, target_y / source_y) 187 188 # Handle one-dimensional targets. 189 if not target_x: 190 target_x = round(source_x * scale) 191 elif not target_y: 192 target_y = round(source_y * scale) 193 194 if zoom: 195 if not crop: 196 target_x = round(source_x * scale) 197 target_y = round(source_y * scale) 198 crop = True 199 scale *= (100 + int(zoom)) / 100.0 200 201 if scale < 1.0 or (scale > 1.0 and upscale): 202 # Resize the image to the target size boundary. Round the scaled 203 # boundary sizes to avoid floating point errors. 204 im = im.resize((int(round(source_x * scale)), 205 int(round(source_y * scale))), 206 resample=Image.ANTIALIAS) 207 208 if crop: 209 # Use integer values now. 210 source_x, source_y = im.size 211 # Difference between new image size and requested size. 212 diff_x = int(source_x - min(source_x, target_x)) 213 diff_y = int(source_y - min(source_y, target_y)) 214 if crop != 'scale' and (diff_x or diff_y): 215 if isinstance(target, six.string_types): 216 target = re.match(r'(\d+)?,(\d+)?$', target) 217 if target: 218 target = target.groups() 219 if target: 220 focal_point = [int(n) if (n or n == 0) else 50 for n in target] 221 else: 222 focal_point = 50, 50 223 # Crop around the focal point 224 halftarget_x, halftarget_y = int(target_x / 2), int(target_y / 2) 225 focal_point_x = int(source_x * focal_point[0] / 100) 226 focal_point_y = int(source_y * focal_point[1] / 100) 227 box = [ 228 max(0, min(source_x - target_x, focal_point_x - halftarget_x)), 229 max(0, min(source_y - target_y, focal_point_y - halftarget_y)), 230 ] 231 box.append(int(min(source_x, box[0] + target_x))) 232 box.append(int(min(source_y, box[1] + target_y))) 233 # See if an edge cropping argument was provided. 234 edge_crop = (isinstance(crop, six.string_types) and 235 re.match(r'(?:(-?)(\d+))?,(?:(-?)(\d+))?$', crop)) 236 if edge_crop and filter(None, edge_crop.groups()): 237 x_right, x_crop, y_bottom, y_crop = edge_crop.groups() 238 if x_crop: 239 offset = min(int(target_x) * int(x_crop) // 100, diff_x) 240 if x_right: 241 box[0] = diff_x - offset 242 box[2] = source_x - offset 243 else: 244 box[0] = offset 245 box[2] = source_x - (diff_x - offset) 246 if y_crop: 247 offset = min(int(target_y) * int(y_crop) // 100, diff_y) 248 if y_bottom: 249 box[1] = diff_y - offset 250 box[3] = source_y - offset 251 else: 252 box[1] = offset 253 box[3] = source_y - (diff_y - offset) 254 # See if the image should be "smart cropped". 255 elif crop == 'smart': 256 left = top = 0 257 right, bottom = source_x, source_y 258 while diff_x: 259 slice = min(diff_x, max(diff_x // 5, 10)) 260 start = im.crop((left, 0, left + slice, source_y)) 261 end = im.crop((right - slice, 0, right, source_y)) 262 add, remove = _compare_entropy(start, end, slice, diff_x) 263 left += add 264 right -= remove 265 diff_x = diff_x - add - remove 266 while diff_y: 267 slice = min(diff_y, max(diff_y // 5, 10)) 268 start = im.crop((0, top, source_x, top + slice)) 269 end = im.crop((0, bottom - slice, source_x, bottom)) 270 add, remove = _compare_entropy(start, end, slice, diff_y) 271 top += add 272 bottom -= remove 273 diff_y = diff_y - add - remove 274 box = (left, top, right, bottom) 275 # Finally, crop the image! 276 im = im.crop(box) 277 return im 278 279 280def filters(im, detail=False, sharpen=False, **kwargs): 281 """ 282 Pass the source image through post-processing filters. 283 284 sharpen 285 Sharpen the thumbnail image (using the PIL sharpen filter) 286 287 detail 288 Add detail to the image, like a mild *sharpen* (using the PIL 289 ``detail`` filter). 290 291 """ 292 if detail: 293 im = im.filter(ImageFilter.DETAIL) 294 if sharpen: 295 im = im.filter(ImageFilter.SHARPEN) 296 return im 297 298 299def background(im, size, background=None, **kwargs): 300 """ 301 Add borders of a certain color to make the resized image fit exactly within 302 the dimensions given. 303 304 background 305 Background color to use 306 """ 307 if not background: 308 # Primary option not given, nothing to do. 309 return im 310 if not size[0] or not size[1]: 311 # One of the dimensions aren't specified, can't do anything. 312 return im 313 x, y = im.size 314 if x >= size[0] and y >= size[1]: 315 # The image is already equal to (or larger than) the expected size, so 316 # there's nothing to do. 317 return im 318 im = colorspace(im, replace_alpha=background, **kwargs) 319 new_im = Image.new('RGB', size, background) 320 if new_im.mode != im.mode: 321 new_im = new_im.convert(im.mode) 322 offset = (size[0]-x)//2, (size[1]-y)//2 323 new_im.paste(im, offset) 324 return new_im 325