from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals import hashlib import os from PIL import Image as pil from functools import lru_cache import six from six import StringIO, BytesIO from image import settings from image.settings import IMAGE_DEFAULT_QUALITY, IMAGE_DEFAULT_FORMAT from image.storage import MEDIA_STORAGE, STATIC_STORAGE, IMAGE_CACHE_STORAGE INTERNAL_CACHE_ROOT = "%s/_internal/" % settings.IMAGE_CACHE_ROOT ALPHA_FORMATS = ["PNG"] def power_to_rgb(value): if value <= 0.0031308: value *= 12.92 else: value = 1.055 * pow(value, 0.416666666667) - 0.055 return round(value * 255.0) def rgb_to_power(value): value = float(value) / 255.0 if value <= 0.04045: value /= 12.92 else: value = pow((value + 0.055) / 1.055, 2.4) return value def add_rgba_to_pixel(pixel, rgba, x_ammount, x_displacement): a = rgba[3] pa = pixel[3] if a == 1.0 and pa == 1.0: total_ammount = x_ammount + x_displacement rgba_ammount = x_ammount / total_ammount pixel_ammount = x_displacement / total_ammount return ( pixel[0] * pixel_ammount + rgba[0] * rgba_ammount, pixel[1] * pixel_ammount + rgba[1] * rgba_ammount, pixel[2] * pixel_ammount + rgba[2] * rgba_ammount, pa * pixel_ammount + a * rgba_ammount, ) else: total_ammount = x_ammount + x_displacement rgba_ammount = x_ammount / total_ammount # rgba_ammount_alpha = rgba_ammount * a pixel_ammount = x_displacement / total_ammount # pixel_ammount_alpha = pixel_ammount * pa return ( pixel[0] * pixel_ammount + rgba[0] * rgba_ammount, pixel[1] * pixel_ammount + rgba[1] * rgba_ammount, pixel[2] * pixel_ammount + rgba[2] * rgba_ammount, pa * pixel_ammount + a * rgba_ammount, ) def resizeScale(img, width, height, force): src_width, src_height = img.size src_ratio = float(src_width) / float(src_height) if force: max_width = width max_height = height else: max_width = min(width, src_width) max_height = min(height, src_height) dst_width = max_width dst_height = dst_width / src_ratio if dst_height > max_height: dst_height = max_height dst_width = dst_height * src_ratio # img_width, img_height = img.size img = img.resize((int(dst_width), int(dst_height)), pil.ANTIALIAS) return img def resizeCrop(img, width, height, center, force): """ # Esto no hace nada perceptible RATIO = 2 while img.size[0] / RATIO > width or img.size[1] / RATIO > height: img = img.resize((int(img.size[0]/RATIO), int(img.size[1]/RATIO)), pil.ANTIALIAS) """ max_width = width max_height = height if not force: img.thumbnail((max_width, max_height), pil.ANTIALIAS) else: src_width, src_height = img.size src_ratio = float(src_width) / float(src_height) dst_width, dst_height = max_width, max_height dst_ratio = float(dst_width) / float(dst_height) if dst_ratio < src_ratio: crop_height = src_height crop_width = crop_height * dst_ratio x_offset = float(src_width - crop_width) / 2 y_offset = 0 else: crop_width = src_width crop_height = crop_width / dst_ratio x_offset = 0 y_offset = float(src_height - crop_height) / 2 center_x, center_y = center.split(',') center_x = float(center_x) center_y = float(center_y) x_offset = min( max(0, center_x * src_width - crop_width / 2), src_width - crop_width ) y_offset = min( max(0, center_y * src_height - crop_height / 2), src_height - crop_height ) img = img.crop( (int(x_offset), int(y_offset), int(x_offset) + int(crop_width), int(y_offset) + int(crop_height))) img = img.resize((int(dst_width), int(dst_height)), pil.ANTIALIAS) return img def do_tint(img, tint): if not tint or tint == 'None': return if img.mode != "RGBA": img = img.convert("RGBA") try: tint_red = float(int("0x%s" % tint[0:2], 16)) / 255.0 except ValueError: tint_red = 1.0 try: tint_green = float(int("0x%s" % tint[2:4], 16)) / 255.0 except ValueError: tint_green = 1.0 try: tint_blue = float(int("0x%s" % tint[4:6], 16)) / 255.0 except ValueError: tint_blue = 1.0 try: tint_alpha = float(int("0x%s" % tint[6:8], 16)) / 255.0 except ValueError: tint_alpha = 1.0 try: intensity = float(int("0x%s" % tint[8:10], 16)) except ValueError: intensity = 255.0 if intensity > 0.0 and (tint_red != 1.0 or tint_green != 1.0 or tint_blue != 1.0 or tint_alpha != 1.0): # Only tint if the color provided is not ffffffff, because that equals no tint pixels = img.load() if intensity == 255.0: for y in range(img.size[1]): for x in range(img.size[0]): data = pixels[x, y] pixels[x, y] = ( int(float(data[0]) * tint_red), int(float(data[1]) * tint_green), int(float(data[2]) * tint_blue), int(float(data[3]) * tint_alpha), ) else: intensity = intensity / 255.0 intensity_inv = 1 - intensity tint_red *= intensity tint_green *= intensity tint_blue *= intensity tint_alpha *= intensity for y in range(img.size[1]): for x in range(img.size[0]): data = pixels[x, y] pixels[x, y] = ( int(float(data[0]) * intensity_inv + float(data[0]) * tint_red), int(float(data[1]) * intensity_inv + float(data[1]) * tint_green), int(float(data[2]) * intensity_inv + float(data[2]) * tint_blue), int(float(data[3]) * intensity_inv + float(data[3]) * tint_alpha), ) def do_grayscale(img): return img.convert('LA').convert('RGBA') def do_paste(img, overlay, position): if overlay.mode != 'RGBA': overlay = overlay.convert('RGBA') # img.paste(overlay, position, overlay) # return img overlay_full = pil.new('RGBA', img.size, color=(255, 255, 255, 0)) overlay_full.paste(overlay, position) # img.paste(overlay_full, (0,0), overlay_full) # return img return pil.alpha_composite(img, overlay_full) # overlay_pixels = overlay.load() # img_pixels = img.load() # overlay_width, overlay_height = overlay.size # x_offset, y_offset = position # # for y in range(min(overlay_height, img.size[1] - y_offset)): # for x in range(min(overlay_width, img.size[0] - x_offset)): # img_pixel = img_pixels[x + x_offset, y + y_offset] # overlay_pixel = overlay_pixels[x, y] # ia = img_pixel[3] # oa = overlay_pixel[3] # if oa == 0: # # overlay is transparent, nothing to do # continue # elif oa == 255: # # overlay is opaque, ignore img pixel # new_pixel = overlay_pixel # elif ia == 0: # # image pixel is 100% transparent, only overlay matters # new_pixel = overlay_pixel # elif ia == 255: # # simpler math # oa = float(oa) / 255.0 # oa1 = 1.0 - oa # new_pixel = ( # int(power_to_rgb( rgb_to_power(img_pixel[0]) * oa1 + rgb_to_power(overlay_pixel[0]) * oa )), # int(power_to_rgb( rgb_to_power(img_pixel[1]) * oa1 + rgb_to_power(overlay_pixel[1]) * oa )), # int(power_to_rgb( rgb_to_power(img_pixel[2]) * oa1 + rgb_to_power(overlay_pixel[2]) * oa )), # 255, # ) # else: # # complex math # oa = float(oa) / 255.0 # ia = float(ia) / 255.0 # oa1 = 1 - oa # #total_alpha_percent = oa + ia # overlay_percent = oa # image_percent = ia * oa1 # # new_pixel = ( # int(power_to_rgb( rgb_to_power(img_pixel[0]) * image_percent + rgb_to_power(overlay_pixel[0]) * overlay_percent )), # int(power_to_rgb( rgb_to_power(img_pixel[1]) * image_percent + rgb_to_power(overlay_pixel[1]) * overlay_percent )), # int(power_to_rgb( rgb_to_power(img_pixel[2]) * image_percent + rgb_to_power(overlay_pixel[2]) * overlay_percent )), # int((oa + ia * oa1) * 255.0), # ) # # img_pixels[x + x_offset, y + y_offset] = new_pixel def do_overlay(img, overlay_path, overlay_source=None, overlay_tint=None, overlay_size=None, overlay_position=None): if not overlay_path: return img if overlay_source == 'media': overlay = pil.open(MEDIA_STORAGE.open(overlay_path)) else: overlay = pil.open(STATIC_STORAGE.open(overlay_path)) # We want the overlay to fit in the image iw, ih = img.size ow, oh = overlay.size overlay_ratio = float(ow) / float(oh) if overlay_size: tw, th = overlay_size.split(',') ow = int(round(float(tw.strip()) * iw)) oh = int(round(float(th.strip()) * ih)) if ow < 0: ow = oh * overlay_ratio elif oh < 0: oh = ow / overlay_ratio overlay = resizeScale(overlay, ow, oh, overlay_source + "/" + overlay_path) ow, oh = overlay.size else: have_to_scale = False if ow > iw: ow = iw oh = int(float(iw) / overlay_ratio) have_to_scale = True if oh > ih: ow = int(float(ih) * overlay_ratio) oh = ih have_to_scale = True if have_to_scale: overlay = resizeScale(overlay, ow, oh, overlay_source + "/" + overlay_path) ow, oh = overlay.size if overlay_tint: do_tint(overlay, overlay_tint) if not overlay_position: target_x = int((iw - ow) / 2) target_y = int((ih - oh) / 2) else: tx, ty = overlay_position.split(',') tx = tx.strip() ty = ty.strip() if tx == "": # Center horizontally. target_x = int((iw - ow) / 2) else: if "!" in tx: # X origin on the right side. x_percent = 1.0 - float(tx.replace("!", "").strip()) target_x = int(round(x_percent * iw) - ow) else: # X origin on the left side. x_percent = float(tx) target_x = int(round(x_percent * iw)) if ty == "": # Center vertically. target_y = int((ih - oh) / 2) else: if "!" in ty: # Y origin on the bottom side. y_percent = 1.0 - float(ty.replace("!", "").strip()) target_y = int(round(y_percent * ih) - oh) else: # Y origin on the top side. y_percent = float(ty) target_y = int(round(y_percent * ih)) """ TODO: paste seems to be buggy, because pasting over opaque background returns a non opaque image (the parts that are not 100% opaque or 100% transparent become partially transparent. the putalpha workareound doesn't seem to look nice enough """ img = do_paste(img, overlay, (target_x, target_y)) return img def do_overlays(img, overlays, overlay_tints, overlay_sources, overlay_sizes, overlay_positions): overlay_index = 0 for overlay in overlays: try: overlay_tint = overlay_tints[overlay_index] except (IndexError, TypeError): overlay_tint = None if overlay_tint == "None": overlay_tint = None try: overlay_source = overlay_sources[overlay_index] except (IndexError, TypeError): overlay_source = 'static' try: overlay_size = overlay_sizes[overlay_index] except (IndexError, TypeError): overlay_size = None if overlay_size == "None": overlay_size = None try: overlay_position = overlay_positions[overlay_index] except (IndexError, TypeError): overlay_position = None if overlay_position == "None": overlay_position = None img = do_overlay(img, overlay, overlay_source, overlay_tint, overlay_size, overlay_position) overlay_index += 1 return img def do_mask(img, mask_path, mask_source, mask_mode=None): if not mask_path: return img if mask_source == 'media': mask = pil.open(MEDIA_STORAGE.open(mask_path)).convert("RGBA") else: mask = pil.open(STATIC_STORAGE.open(mask_path)).convert("RGBA") # We want the mask to have the same size than the image if mask_mode == 'distort': iw, ih = img.size mw, mh = mask.size if mw != iw or mh != ih: mask = mask.resize((iw, ih), pil.ANTIALIAS) else: # We want the overlay to fit in the image iw, ih = img.size ow, oh = mask.size overlay_ratio = float(ow) / float(oh) have_to_scale = False if ow > iw: ow = iw oh = int(float(iw) / overlay_ratio) if oh > ih: ow = int(float(ih) * overlay_ratio) oh = ih if ow != iw or oh != ih: have_to_scale = True if have_to_scale: nmask = mask.resize((ow, oh), pil.ANTIALIAS) mask = pil.new('RGBA', (iw, ih)) # mask.paste(nmask, (int((iw - ow) / 2), int((ih - oh) / 2)), nmask) mask = do_paste(mask, nmask, (int((iw - ow) / 2), int((ih - oh) / 2))) ow, oh = mask.size r, g, b, a = mask.split() img.putalpha(a) def do_fill(img, fill, width, height): if not fill: return img overlay = img fill_color = ( int("0x%s" % fill[0:2], 16), int("0x%s" % fill[2:4], 16), int("0x%s" % fill[4:6], 16), int("0x%s" % fill[6:8], 16), ) img = pil.new("RGBA", (width, height), fill_color) iw, ih = img.size ow, oh = overlay.size # img.paste(overlay, (int((iw - ow) / 2), int((ih - oh) / 2)), overlay) img = do_paste(img, overlay, (int((iw - ow) / 2), int((ih - oh) / 2))) return img def do_padding(img, padding): if not padding: return img try: padding = float(padding) * 2.0 if padding > .9: padding = .9 if padding <= 0.0: return img except ValueError: return iw, ih = img.size img.thumbnail( ( int(round(float(img.size[0]) * (1.0 - padding))), int(round(float(img.size[1]) * (1.0 - padding))) ), pil.ANTIALIAS ) img = do_fill(img, "ffffff00", iw, ih) return img def do_background(img, background): if not background: return img overlay = img fill_color = ( int("0x%s" % background[0:2], 16), int("0x%s" % background[2:4], 16), int("0x%s" % background[4:6], 16), int("0x%s" % background[6:8], 16), ) img = pil.new("RGBA", overlay.size, fill_color) iw, ih = img.size ow, oh = overlay.size # img.paste(overlay, (int((iw - ow) / 2), int((ih - oh) / 2)), overlay) img = do_paste(img, overlay, (int((iw - ow) / 2), int((ih - oh) / 2))) return img def do_rotate(img, rotation): if not rotation: return img try: rotation = float(rotation) except ValueError: rotation = 0 if rotation % 90 == 0: img = img.rotate(rotation, pil.NEAREST, expand=True) else: img = img.rotate(rotation, pil.BICUBIC, expand=True) return img def render(data, width, height, force=True, padding=None, overlays=(), overlay_sources=(), overlay_tints=(), overlay_sizes=None, overlay_positions=None, mask=None, mask_source=None, center=".5,.5", format=IMAGE_DEFAULT_FORMAT, quality=IMAGE_DEFAULT_QUALITY, fill=None, background=None, tint=None, pre_rotation=None, post_rotation=None, crop=True, grayscale=False): """ Rescale the given image, optionally cropping it to make sure the result image has the specified width and height. """ if not isinstance(data, six.string_types): input_file = BytesIO(data) else: input_file = StringIO(data) img = pil.open(input_file) if img.mode != "RGBA": img = img.convert("RGBA") if width is None: width = img.size[0] if height is None: height = img.size[1] img = do_rotate(img, pre_rotation) if crop: img = resizeCrop(img, width, height, center, force) else: img = resizeScale(img, width, height, force) if grayscale: img = do_grayscale(img) do_tint(img, tint) img = do_fill(img, fill, width, height) img = do_background(img, background) do_mask(img, mask, mask_source) img = do_overlays(img, overlays, overlay_tints, overlay_sources, overlay_sizes, overlay_positions) img = do_padding(img, padding) img = do_rotate(img, post_rotation) tmp = BytesIO() if not format.upper() in ALPHA_FORMATS: img = img.convert("RGB") img.save(tmp, format, quality=quality) tmp.seek(0) output_data = tmp.getvalue() input_file.close() tmp.close() return output_data @lru_cache(maxsize=128) def image_create_token(parameters): return "image_token_%s" % hashlib.sha1(parameters.encode("utf8")).hexdigest() def image_tokenize(session, parameters): if session: token = None for k, v in session.items(): if v == parameters: token = k break if token is None: token = image_create_token(parameters) session[token] = parameters else: token = image_create_token(parameters) return token def image_url(request, parameters, image_field, generate=False): if generate: from image import views as image_views autogen = 'autogen=true' in parameters image_views.image(request=request, path=str(image_field), token=parameters, autogen=autogen) image_path = os.path.join(image_tokenize(request.session, parameters), six.text_type(image_field)) return IMAGE_CACHE_STORAGE.url(image_path)