Source code for pyams_file.image

#
# 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