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