#
# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
__docformat__ = 'restructuredtext'
import re
from io import BytesIO
from PIL import Image, ImageFilter
from zope.interface import implementer
from zope.schema.fieldproperty import FieldProperty
from pyams_file import _
from pyams_file.interfaces import IImage, IResponsiveImage, IThumbnailGeometry, IThumbnailer, IThumbnails
from pyams_utils.adapter import ContextAdapter, adapter_config
WEB_FORMATS = ('JPEG', 'PNG', 'GIF')
THUMB_SIZE = re.compile('^(?:\w+\:)?([0-9]+)x([0-9]+)$')
[docs]@implementer(IThumbnailGeometry)
class ThumbnailGeometry(object):
"""Image thumbnail geometry"""
x1 = FieldProperty(IThumbnailGeometry['x1'])
y1 = FieldProperty(IThumbnailGeometry['y1'])
x2 = FieldProperty(IThumbnailGeometry['x2'])
y2 = FieldProperty(IThumbnailGeometry['y2'])
def __repr__(self):
return '<ThumbnailGeometry: x1,y1={0.x1},{0.y1} - x2,y2={0.x2},{0.y2}>'.format(self)
def __eq__(self, other):
if IThumbnailGeometry.providedBy(other):
return (self.x1 == other.x1) and \
(self.x2 == other.x2) and \
(self.y1 == other.y1) and \
(self.y2 == other.y2)
else:
return False
[docs] def is_empty(self):
return (self.x2 <= self.x1) or (self.y2 <= self.y1)
[docs]@adapter_config(context=IImage, provides=IThumbnailer)
class ImageThumbnailer(ContextAdapter):
"""Image thumbnailer adapter"""
label = _("Default thumbnail")
section = _("Default thumbnail")
weight = 1
[docs] def get_default_geometry(self, options=None):
"""Default thumbnail geometry"""
geometry = ThumbnailGeometry()
width, height = self.context.get_image_size()
geometry.x1 = 0
geometry.y1 = 0
geometry.x2 = width
geometry.y2 = height
return geometry
[docs] def create_thumbnail(self, target, format=None):
# check thumbnail name
if isinstance(target, str):
width, height = tuple(map(int, target.split('x')))
elif IThumbnailGeometry.providedBy(target):
width = target.x2 - target.x1
height = target.y2 - target.y1
elif isinstance(target, tuple):
width, height = target
else:
return None
# check format
blob = self.context.get_blob(mode='r')
if blob is None:
return None
image = Image.open(blob)
if not format:
format = image.format
format = format.upper()
if format not in WEB_FORMATS:
format = 'JPEG'
# check image mode
if image.mode == 'P':
if format == 'JPEG':
image = image.convert('RGB')
else:
image = image.convert('RGBA')
# generate thumbnail
new_image = BytesIO()
image.resize((width, height), Image.ANTIALIAS) \
.filter(ImageFilter.UnsharpMask(radius=0.5, percent=100, threshold=0)) \
.save(new_image, format)
return new_image, format.lower()
[docs]class ImageSelectionThumbnailer(ImageThumbnailer):
"""Image thumbnailer based on user selection"""
section = _("Custom selections")
[docs] def create_thumbnail(self, target, format=None):
# get thumbnail size
if isinstance(target, str):
geometry = IThumbnails(self.context).get_geometry(target)
match = THUMB_SIZE.match(target)
if match:
width, height = tuple(map(int, match.groups()))
else:
width = abs(geometry.x2 - geometry.x1)
height = abs(geometry.y2 - geometry.y1)
elif IThumbnailGeometry.providedBy(target):
geometry = target
width = abs(geometry.x2 - geometry.x1)
height = abs(geometry.y2 - geometry.y1)
elif isinstance(target, tuple):
width, height = target
geometry = self.get_default_geometry()
else:
return None
# check format
blob = self.context.get_blob(mode='r')
if blob is None:
return None
image = Image.open(blob)
if not format:
format = image.format
format = format.upper()
if format not in WEB_FORMATS:
format = 'JPEG'
# check image mode
if image.mode == 'P':
if format == 'JPEG':
image = image.convert('RGB')
else:
image = image.convert('RGBA')
# generate thumbnail
new_image = BytesIO()
thumb_size = self.get_thumb_size(width, height, geometry)
image.crop((geometry.x1, geometry.y1, geometry.x2, geometry.y2)) \
.resize(thumb_size, Image.ANTIALIAS) \
.filter(ImageFilter.UnsharpMask(radius=0.5, percent=100, threshold=0)) \
.save(new_image, format)
return new_image, format.lower()
[docs] def get_thumb_size(self, width, height, geometry):
return width, height
[docs]class ImageRatioThumbnailer(ImageSelectionThumbnailer):
"""Image thumbnailer with specific ratio"""
ratio = (None, None) # (width, height) ratio tuple
[docs] def get_default_geometry(self, options=None):
geometry = ThumbnailGeometry()
width, height = self.context.get_image_size()
thumb_max_height = width * self.ratio[1] / self.ratio[0]
if thumb_max_height >= height:
# image wider
thumb_width = height * self.ratio[0] / self.ratio[1]
geometry.x1 = round((width / 2) - (thumb_width / 2))
geometry.y1 = 0
geometry.x2 = round((width / 2) + (thumb_width / 2))
geometry.y2 = height
else:
thumb_height = thumb_max_height
geometry.x1 = 0
geometry.y1 = round((height / 2) - (thumb_height / 2))
geometry.x2 = width
geometry.y2 = round((height / 2) + (thumb_height / 2))
return geometry
[docs]@adapter_config(name='portrait', context=IImage, provides=IThumbnailer)
class ImagePortraitThumbnailer(ImageRatioThumbnailer):
"""Image portrait thumbnail adapter"""
label = _("Portrait thumbnail")
weight = 5
ratio = (3, 4)
[docs]@adapter_config(name='square', context=IImage, provides=IThumbnailer)
class ImageSquareThumbnailer(ImageRatioThumbnailer):
"""Image square thumbnail adapter"""
label = _("Square thumbnail")
weight = 6
ratio = (1, 1)
[docs]@adapter_config(name='pano', context=IImage, provides=IThumbnailer)
class ImagePanoThumbnailer(ImageRatioThumbnailer):
"""Image panoramic thumbnail adapter"""
label = _("Panoramic thumbnail")
weight = 7
ratio = (16, 9)
[docs] def get_thumb_size(self, width, height, geometry):
thumb_size = abs(geometry.x2 - geometry.x1), abs(geometry.y2 - geometry.y1)
w_ratio = 1. * width / thumb_size[0]
h_ratio = 1. * height / thumb_size[1]
ratio = min(w_ratio, h_ratio)
return round(ratio * thumb_size[0]), round(ratio * thumb_size[1])
[docs]@adapter_config(name='banner', context=IImage, provides=IThumbnailer)
class ImageBannerThumbnailer(ImageRatioThumbnailer):
"""Image banner thumbnail adapter"""
label = _("Banner thumbnail")
weight = 8
ratio = (5, 1)
[docs]class ResponsiveImageThumbnailer(ImageSelectionThumbnailer):
"""Responsive image thumbnailer"""
section = _("Responsive selections")
[docs]@adapter_config(name='xs', context=IResponsiveImage, provides=IThumbnailer)
class XsImageThumbnailer(ResponsiveImageThumbnailer):
"""eXtra-Small responsive image thumbnailer"""
label = _("Smartphone thumbnail")
weight = 10
[docs]@adapter_config(name='sm', context=IResponsiveImage, provides=IThumbnailer)
class SmImageThumbnailer(ResponsiveImageThumbnailer):
"""SMall responsive image thumbnailer"""
label = _("Tablet thumbnail")
weight = 11
[docs]@adapter_config(name='md', context=IResponsiveImage, provides=IThumbnailer)
class MdImageThumbnailer(ResponsiveImageThumbnailer):
"""MeDium responsive image thumbnailer"""
label = _("Medium screen thumbnail")
weight = 12
[docs]@adapter_config(name='lg', context=IResponsiveImage, provides=IThumbnailer)
class LgImageThumbnailer(ResponsiveImageThumbnailer):
"""LarGe responsive image thumbnailer"""
label = _("Large screen thumbnail")
weight = 13