# HG changeset patch # User Thierry Florac # Date 1479200102 -3600 # Node ID 58686bb1a7b9c3432fb4a0b69cc6343a467c2e34 # Parent 2f2e70d36b6e417b64e70fa2f4a7c5aa6f007c17 Handle responsive images selection diff -r 2f2e70d36b6e -r 58686bb1a7b9 src/pyams_file/image.py --- a/src/pyams_file/image.py Thu Apr 21 16:40:03 2016 +0200 +++ b/src/pyams_file/image.py Tue Nov 15 09:55:02 2016 +0100 @@ -14,11 +14,13 @@ # import standard library +import re + from io import BytesIO from PIL import Image, ImageFilter # import interfaces -from pyams_file.interfaces import IImage, IThumbnailer, IThumbnailGeometry, IThumbnail +from pyams_file.interfaces import IImage, IThumbnailer, IThumbnailGeometry, IThumbnail, IResponsiveImage # import packages from pyams_utils.adapter import ContextAdapter, adapter_config @@ -27,10 +29,11 @@ WEB_FORMATS = ('JPEG', 'PNG', 'GIF') +THUMB_SIZE = re.compile('^(?:\w+\:)?([0-9]+)x([0-9]+)$') @implementer(IThumbnailGeometry) -class ThumbnailGeometrry(object): +class ThumbnailGeometry(object): """Image thumbnail geometry""" x1 = FieldProperty(IThumbnailGeometry['x1']) @@ -48,7 +51,7 @@ def get_default_geometry(self, options=None): """Default thumbnail geometry""" - geometry = ThumbnailGeometrry() + geometry = ThumbnailGeometry() width, height = self.context.get_image_size() geometry.x1 = 0 geometry.y1 = 0 @@ -56,16 +59,19 @@ geometry.y2 = height return geometry - def create_thumbnail(self, thumbnail_name, format=None): + def create_thumbnail(self, target, format=None): # check thumbnail name - if isinstance(thumbnail_name, str): - width, height = tuple((int(x) for x in thumbnail_name.split('x'))) - elif isinstance(thumbnail_name, tuple): - width, height = thumbnail_name + if isinstance(target, str): + width, height = tuple((int(x) for x in 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 image = Image.open(self.context.get_blob(mode='c')) - # check format if not format: format = image.format format = format.upper() @@ -82,13 +88,59 @@ return new_image, format.lower() +class ImageSelectionThumbnailer(ImageThumbnailer): + """Image thumbnailer based on user selection""" + + def create_thumbnail(self, target, format=None): + # get thumbnail size + if isinstance(target, str): + geometry = IThumbnail(self.context).get_geometry(target) + match = THUMB_SIZE.match(target) + if match: + selection_name, width, height = match.groups() + width, height = int(width), int(height) + 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 + image = Image.open(self.context.get_blob(mode='c')) + if not format: + format = image.format + format = format.upper() + if format not in WEB_FORMATS: + format = 'JPEG' + # check image mode + if image.mode == 'P': + 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.SHARPEN) \ + .save(new_image, format) + return new_image, format.lower() + + def get_thumb_size(self, width, height, geometry): + return width, height + + @adapter_config(name='square', context=IImage, provides=IThumbnailer) -class ImageSquareThumbnailer(ContextAdapter): +class ImageSquareThumbnailer(ImageSelectionThumbnailer): """Image square thumbnail adapter""" def get_default_geometry(self, options=None): """Default square thumbnail geometry""" - geometry = ThumbnailGeometrry() + geometry = ThumbnailGeometry() width, height = self.context.get_image_size() if width >= height: geometry.x1 = round((width / 2) - (height / 2)) @@ -102,45 +154,14 @@ geometry.y2 = round((height / 2) + (width / 2)) return geometry - def create_thumbnail(self, thumbnail_name, format=None): - geometry = IThumbnail(self.context).get_thumbnail_geometry(thumbnail_name) - image = Image.open(self.context.get_blob(mode='c')) - # get thumbnail size - if isinstance(thumbnail_name, str): - if ':' in thumbnail_name: - thumbnailer_name, size = thumbnail_name.split(':', 1) - width, height = tuple((int(x) for x in size.split('x'))) - else: - width, height = tuple((int(x) for x in thumbnail_name.split('x'))) - elif isinstance(thumbnail_name, tuple): - width, height = thumbnail_name - else: - return None - # check format - if not format: - format = image.format - format = format.upper() - if format not in WEB_FORMATS: - format = 'JPEG' - # check image mode - if image.mode == 'P': - image = image.convert('RGBA') - # generate thumbnail - new_image = BytesIO() - image.crop((geometry.x1, geometry.y1, geometry.x2, geometry.y2)) \ - .resize((width, height), Image.ANTIALIAS) \ - .filter(ImageFilter.SHARPEN) \ - .save(new_image, format) - return new_image, format.lower() - @adapter_config(name='pano', context=IImage, provides=IThumbnailer) -class ImagePanoThumbnailer(ContextAdapter): +class ImagePanoThumbnailer(ImageSelectionThumbnailer): """Image panoramic thumbnail adapter""" def get_default_geometry(self, options=None): """Default panoramic thumbnail geometry""" - geometry = ThumbnailGeometrry() + geometry = ThumbnailGeometry() width, height = self.context.get_image_size() pano_max_height = width * 9 / 16 if pano_max_height >= height: @@ -158,38 +179,33 @@ geometry.y2 = round((height / 2) + (pano_height / 2)) return geometry - def create_thumbnail(self, thumbnail_name, format=None): - geometry = IThumbnail(self.context).get_thumbnail_geometry(thumbnail_name) - image = Image.open(self.context.get_blob(mode='c')) - # get thumbnail size - if isinstance(thumbnail_name, str): - if ':' in thumbnail_name: - thumbnailer_name, size = thumbnail_name.split(':', 1) - width, height = tuple((int(x) for x in size.split('x'))) - else: - width, height = tuple((int(x) for x in thumbnail_name.split('x'))) - elif isinstance(thumbnail_name, tuple): - width, height = thumbnail_name - else: - return None - # check aspect ratio + 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) - # check format - if not format: - format = image.format - format = format.upper() - if format not in WEB_FORMATS: - format = 'JPEG' - # check image mode - if image.mode == 'P': - image = image.convert('RGBA') - # generate thumbnail - new_image = BytesIO() - image.crop((geometry.x1, geometry.y1, geometry.x2, geometry.y2)) \ - .resize((round(ratio * thumb_size[0]), round(ratio * thumb_size[1])), Image.ANTIALIAS) \ - .filter(ImageFilter.SHARPEN) \ - .save(new_image, format) - return new_image, format.lower() + return round(ratio * thumb_size[0]), round(ratio * thumb_size[1]) + + +class ResponsiveImageThumbnailer(ImageSelectionThumbnailer): + """Responsive image thumbnailer""" + + +@adapter_config(name='xs', context=IResponsiveImage, provides=IThumbnailer) +class XsImageThumbnailer(ResponsiveImageThumbnailer): + """eXtra-Small responsive image thumbnailer""" + + +@adapter_config(name='sm', context=IResponsiveImage, provides=IThumbnailer) +class SmImageThumbnailer(ResponsiveImageThumbnailer): + """SMall responsive image thumbnailer""" + + +@adapter_config(name='md', context=IResponsiveImage, provides=IThumbnailer) +class MdImageThumbnailer(ResponsiveImageThumbnailer): + """MeDiumresponsive image thumbnailer""" + + +@adapter_config(name='lg', context=IResponsiveImage, provides=IThumbnailer) +class LgImageThumbnailer(ResponsiveImageThumbnailer): + """LarGe responsive image thumbnailer""" diff -r 2f2e70d36b6e -r 58686bb1a7b9 src/pyams_file/interfaces/__init__.py --- a/src/pyams_file/interfaces/__init__.py Thu Apr 21 16:40:03 2016 +0200 +++ b/src/pyams_file/interfaces/__init__.py Tue Nov 15 09:55:02 2016 +0100 @@ -81,6 +81,10 @@ """Crop image to given coordinates""" +class IResponsiveImage(Interface): + """Responsive image marker interface""" + + class IVideo(IMediaFile): """Video object interface""" @@ -160,7 +164,7 @@ """Image field widget""" -class IThumnailImageWidget(IImageWidget): +class IThumbnailImageWidget(IImageWidget): """Image field widget with thumbnail selection""" @@ -198,18 +202,19 @@ def get_default_geometry(self): """Get default thumbnail geometry""" - def create_thumbnail(self, target_size, format=None): + def create_thumbnail(self, target, format=None): """Create thumbnail of the given source object Source can be any file which can provide thumbnails (image, video, - PDF file...) - target_size is the size of the created thumbnail, as an (width, height) tuple. + PDF file...). + Target, which defines thumbnail size, can be defined as a selection name + ('pano', 'square', 'xs'...), as a geometry or as a (width, height) tuple. If the requested image is of a resolution higher than that of the original file, the resulting image resolution will be that of the original file. If format (JPEG, PNG...) is given, this will be the format of the generated - thumbnail; otherwise the selected format + thumbnail; otherwise the format will be those of the source image. """ @@ -230,10 +235,10 @@ source """ - def get_thumbnail_geometry(self, thumbnail_name): + def get_geometry(self, selection_name): """Get geometry of a given thumbnail""" - def set_thumbnail_geometry(self, thumbnail_name, geometry): + def set_geometry(self, selection_name, geometry): """Set geometry for given thumbnail""" def clear_geometries(self): @@ -242,6 +247,9 @@ def get_thumbnail_name(self, thumbnail_name, with_size=None): """Get matching name for the given thumbnail name or size""" + def get_selection(self, selection_name, format=None): + """Get image for given user selection""" + def get_thumbnail(self, thumbnail_name, format=None, watermark=None): """Get requested thumbnail diff -r 2f2e70d36b6e -r 58686bb1a7b9 src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.mo Binary file src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.mo has changed diff -r 2f2e70d36b6e -r 58686bb1a7b9 src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.po --- a/src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.po Thu Apr 21 16:40:03 2016 +0200 +++ b/src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.po Tue Nov 15 09:55:02 2016 +0100 @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE 1.0\n" -"POT-Creation-Date: 2015-02-10 17:59+0100\n" +"POT-Creation-Date: 2016-11-14 17:49+0100\n" "PO-Revision-Date: 2015-02-06 21:39+0100\n" "Last-Translator: Thierry Florac \n" "Language-Team: French\n" @@ -52,23 +52,23 @@ msgid "Delete content" msgstr "Supprimer ce contenu" -#: src/pyams_file/zmi/file.py:41 +#: src/pyams_file/zmi/file.py:42 msgid "Properties..." msgstr "Propriétés..." -#: src/pyams_file/zmi/file.py:52 +#: src/pyams_file/zmi/file.py:53 msgid "Update file properties" msgstr "Mise à jour des propriétés" -#: src/pyams_file/zmi/image.py:56 +#: src/pyams_file/zmi/image.py:61 msgid "Resize image..." msgstr "Redimensionner l'image..." -#: src/pyams_file/zmi/image.py:107 src/pyams_file/zmi/image.py:67 +#: src/pyams_file/zmi/image.py:111 src/pyams_file/zmi/image.py:72 msgid "Resize image" msgstr "Redimensionner l'image" -#: src/pyams_file/zmi/image.py:141 +#: src/pyams_file/zmi/image.py:146 msgid "" "You can use this form to change image dimensions.\n" "\n" @@ -77,51 +77,83 @@ msgstr "" "Vous pouvez utiliser ce formulaire pour changer la taille de l'image.\n" "\n" -"Une nouvelle image ne sera générée que si les dimensions indiquées sont inférieures " -"à la taille du fichier actuel." +"Une nouvelle image ne sera générée que si les dimensions indiquées sont " +"inférieures à la taille du fichier actuel." -#: src/pyams_file/zmi/image.py:152 +#: src/pyams_file/zmi/image.py:161 msgid "Crop image..." msgstr "Recadrer l'image..." -#: src/pyams_file/zmi/image.py:170 +#: src/pyams_file/zmi/image.py:179 msgid "Crop image" msgstr "Recadrer l'image" -#: src/pyams_file/zmi/image.py:231 +#: src/pyams_file/zmi/image.py:245 msgid "Select square thumbnail..." msgstr "Vignette carrée..." -#: src/pyams_file/zmi/image.py:242 +#: src/pyams_file/zmi/image.py:256 msgid "Select square thumbnail" msgstr "Sélection de l'emprise d'une vignette carrée" -#: src/pyams_file/zmi/image.py:290 +#: src/pyams_file/zmi/image.py:309 msgid "Select panoramic thumbnail..." msgstr "Vignette panoramique..." -#: src/pyams_file/zmi/image.py:306 +#: src/pyams_file/zmi/image.py:320 msgid "Select panoramic thumbnail" msgstr "Sélection de l'emprise d'une vignette panoramique" -#: src/pyams_file/zmi/image.py:66 src/pyams_file/zmi/image.py:162 -#: src/pyams_file/zmi/image.py:222 +#: src/pyams_file/zmi/image.py:432 +msgid "Select responsive XS image..." +msgstr "Image adaptative pour très petits terminaux (XS)..." + +#: src/pyams_file/zmi/image.py:444 +msgid "Select image for extra-small (XS) devices" +msgstr "Sélectionner l'image affichée sur les très petits terminaux (taille XS)" + +#: src/pyams_file/zmi/image.py:466 +msgid "Select responsive SM image..." +msgstr "Image adaptative pour petits terminaux (SM)..." + +#: src/pyams_file/zmi/image.py:478 +msgid "Select image for small (SM) devices" +msgstr "Sélectionner l'image affichée sur les petits terminaux (taille SM)" + +#: src/pyams_file/zmi/image.py:500 +msgid "Select responsive MD image..." +msgstr "Image adaptative pour terminaux moyens (MD)..." + +#: src/pyams_file/zmi/image.py:512 +msgid "Select image for medium (MD) devices" +msgstr "Sélectionner l'image affichée sur les terminaux moyens (taille MD)" + +#: src/pyams_file/zmi/image.py:534 +msgid "Select responsive LG image..." +msgstr "Image adaptative pour grands terminaux (LG)..." + +#: src/pyams_file/zmi/image.py:546 +msgid "Select image for large (LG) devices" +msgstr "Sélectionner l'image affichée sur les grands terminaux (taille LG)" + +#: src/pyams_file/zmi/image.py:71 src/pyams_file/zmi/image.py:171 +#: src/pyams_file/zmi/image.py:236 src/pyams_file/zmi/image.py:377 msgid "Close" msgstr "Fermer" -#: src/pyams_file/zmi/image.py:73 +#: src/pyams_file/zmi/image.py:78 msgid "New image width" msgstr "Largeur de l'image" -#: src/pyams_file/zmi/image.py:75 +#: src/pyams_file/zmi/image.py:80 msgid "New image height" msgstr "Hauteur de l'image" -#: src/pyams_file/zmi/image.py:77 +#: src/pyams_file/zmi/image.py:82 msgid "Keep aspect ratio" msgstr "Ne pas déformer l'image" -#: src/pyams_file/zmi/image.py:78 +#: src/pyams_file/zmi/image.py:83 msgid "" "Check to keep original aspect ratio; image will be resized as large as " "possible within given limits" @@ -130,34 +162,38 @@ "L'image sera redimensionnée (sans jamais être agrandie !) pour être aussi " "grande que possible en fonction des contraintes indiquées." -#: src/pyams_file/zmi/image.py:163 +#: src/pyams_file/zmi/image.py:172 msgid "Crop" msgstr "Recadrer l'image" -#: src/pyams_file/zmi/image.py:223 +#: src/pyams_file/zmi/image.py:237 msgid "Select thumbnail" msgstr "Sélectionner cette vignette" -#: src/pyams_file/interfaces/__init__.py:95 +#: src/pyams_file/zmi/image.py:378 +msgid "Select image" +msgstr "Sélectionner l'image" + +#: src/pyams_file/interfaces/__init__.py:99 msgid "Title" msgstr "Titre" -#: src/pyams_file/interfaces/__init__.py:98 +#: src/pyams_file/interfaces/__init__.py:102 msgid "Description" msgstr "Description" -#: src/pyams_file/interfaces/__init__.py:101 +#: src/pyams_file/interfaces/__init__.py:105 msgid "Save file as..." msgstr "Enregistrer sous..." -#: src/pyams_file/interfaces/__init__.py:102 +#: src/pyams_file/interfaces/__init__.py:106 msgid "Name under which the file will be saved" msgstr "Nom proposé automatiquement lors de l'enregistrement du fichier" -#: src/pyams_file/interfaces/__init__.py:105 +#: src/pyams_file/interfaces/__init__.py:109 msgid "Language" msgstr "Langue" -#: src/pyams_file/interfaces/__init__.py:106 +#: src/pyams_file/interfaces/__init__.py:110 msgid "File's content language" msgstr "Langue du contenu du fichier" diff -r 2f2e70d36b6e -r 58686bb1a7b9 src/pyams_file/locales/pyams_file.pot --- a/src/pyams_file/locales/pyams_file.pot Thu Apr 21 16:40:03 2016 +0200 +++ b/src/pyams_file/locales/pyams_file.pot Tue Nov 15 09:55:02 2016 +0100 @@ -1,12 +1,12 @@ # # SOME DESCRIPTIVE TITLE # This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , 2015. +# FIRST AUTHOR , 2016. #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE 1.0\n" -"POT-Creation-Date: 2015-02-10 17:59+0100\n" +"POT-Creation-Date: 2016-11-14 17:49+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" @@ -52,104 +52,140 @@ msgid "Delete content" msgstr "" -#: ./src/pyams_file/zmi/file.py:41 +#: ./src/pyams_file/zmi/file.py:42 msgid "Properties..." msgstr "" -#: ./src/pyams_file/zmi/file.py:52 +#: ./src/pyams_file/zmi/file.py:53 msgid "Update file properties" msgstr "" -#: ./src/pyams_file/zmi/image.py:56 +#: ./src/pyams_file/zmi/image.py:61 msgid "Resize image..." msgstr "" -#: ./src/pyams_file/zmi/image.py:107 ./src/pyams_file/zmi/image.py:67 +#: ./src/pyams_file/zmi/image.py:111 ./src/pyams_file/zmi/image.py:72 msgid "Resize image" msgstr "" -#: ./src/pyams_file/zmi/image.py:141 +#: ./src/pyams_file/zmi/image.py:146 msgid "" "You can use this form to change image dimensions.\n" "\n" "This will generate a new image only if requested size is smaller than the original one." msgstr "" -#: ./src/pyams_file/zmi/image.py:152 +#: ./src/pyams_file/zmi/image.py:161 msgid "Crop image..." msgstr "" -#: ./src/pyams_file/zmi/image.py:170 +#: ./src/pyams_file/zmi/image.py:179 msgid "Crop image" msgstr "" -#: ./src/pyams_file/zmi/image.py:231 +#: ./src/pyams_file/zmi/image.py:245 msgid "Select square thumbnail..." msgstr "" -#: ./src/pyams_file/zmi/image.py:242 +#: ./src/pyams_file/zmi/image.py:256 msgid "Select square thumbnail" msgstr "" -#: ./src/pyams_file/zmi/image.py:290 +#: ./src/pyams_file/zmi/image.py:309 msgid "Select panoramic thumbnail..." msgstr "" -#: ./src/pyams_file/zmi/image.py:306 +#: ./src/pyams_file/zmi/image.py:320 msgid "Select panoramic thumbnail" msgstr "" -#: ./src/pyams_file/zmi/image.py:66 ./src/pyams_file/zmi/image.py:162 -#: ./src/pyams_file/zmi/image.py:222 +#: ./src/pyams_file/zmi/image.py:432 +msgid "Select responsive XS image..." +msgstr "" + +#: ./src/pyams_file/zmi/image.py:444 +msgid "Select image for extra-small (XS) devices" +msgstr "" + +#: ./src/pyams_file/zmi/image.py:466 +msgid "Select responsive SM image..." +msgstr "" + +#: ./src/pyams_file/zmi/image.py:478 +msgid "Select image for small (SM) devices" +msgstr "" + +#: ./src/pyams_file/zmi/image.py:500 +msgid "Select responsive MD image..." +msgstr "" + +#: ./src/pyams_file/zmi/image.py:512 +msgid "Select image for medium (MD) devices" +msgstr "" + +#: ./src/pyams_file/zmi/image.py:534 +msgid "Select responsive LG image..." +msgstr "" + +#: ./src/pyams_file/zmi/image.py:546 +msgid "Select image for large (LG) devices" +msgstr "" + +#: ./src/pyams_file/zmi/image.py:71 ./src/pyams_file/zmi/image.py:171 +#: ./src/pyams_file/zmi/image.py:236 ./src/pyams_file/zmi/image.py:377 msgid "Close" msgstr "" -#: ./src/pyams_file/zmi/image.py:73 +#: ./src/pyams_file/zmi/image.py:78 msgid "New image width" msgstr "" -#: ./src/pyams_file/zmi/image.py:75 +#: ./src/pyams_file/zmi/image.py:80 msgid "New image height" msgstr "" -#: ./src/pyams_file/zmi/image.py:77 +#: ./src/pyams_file/zmi/image.py:82 msgid "Keep aspect ratio" msgstr "" -#: ./src/pyams_file/zmi/image.py:78 +#: ./src/pyams_file/zmi/image.py:83 msgid "" "Check to keep original aspect ratio; image will be resized as large as " "possible within given limits" msgstr "" -#: ./src/pyams_file/zmi/image.py:163 +#: ./src/pyams_file/zmi/image.py:172 msgid "Crop" msgstr "" -#: ./src/pyams_file/zmi/image.py:223 +#: ./src/pyams_file/zmi/image.py:237 msgid "Select thumbnail" msgstr "" -#: ./src/pyams_file/interfaces/__init__.py:95 +#: ./src/pyams_file/zmi/image.py:378 +msgid "Select image" +msgstr "" + +#: ./src/pyams_file/interfaces/__init__.py:99 msgid "Title" msgstr "" -#: ./src/pyams_file/interfaces/__init__.py:98 +#: ./src/pyams_file/interfaces/__init__.py:102 msgid "Description" msgstr "" -#: ./src/pyams_file/interfaces/__init__.py:101 +#: ./src/pyams_file/interfaces/__init__.py:105 msgid "Save file as..." msgstr "" -#: ./src/pyams_file/interfaces/__init__.py:102 +#: ./src/pyams_file/interfaces/__init__.py:106 msgid "Name under which the file will be saved" msgstr "" -#: ./src/pyams_file/interfaces/__init__.py:105 +#: ./src/pyams_file/interfaces/__init__.py:109 msgid "Language" msgstr "" -#: ./src/pyams_file/interfaces/__init__.py:106 +#: ./src/pyams_file/interfaces/__init__.py:110 msgid "File's content language" msgstr "" diff -r 2f2e70d36b6e -r 58686bb1a7b9 src/pyams_file/thumbnail.py --- a/src/pyams_file/thumbnail.py Thu Apr 21 16:40:03 2016 +0200 +++ b/src/pyams_file/thumbnail.py Tue Nov 15 09:55:02 2016 +0100 @@ -39,9 +39,9 @@ THUMBNAIL_ANNOTATIONS_KEY = 'pyams_file.image.thumbnails' THUMBNAIL_GEOMETRY_KEY = 'pyams_file.image.geometry' -THUMB_WIDTH = re.compile('^w([0-9]+)$') -THUMB_HEIGHT = re.compile('^h([0-9]+)$') -THUMB_SIZE = re.compile('^([0-9]+)x([0-9]+)$') +THUMB_WIDTH = re.compile('^(?:\w+\:)?w([0-9]+)$') +THUMB_HEIGHT = re.compile('^(?:\w+\:)?h([0-9]+)$') +THUMB_SIZE = re.compile('^(?:\w+\:)?([0-9]+)x([0-9]+)$') @adapter_config(context=IImage, provides=IThumbnail) @@ -88,30 +88,31 @@ else: return None - def get_thumbnail_geometry(self, thumbnail_name): + def get_geometry(self, selection_name): annotations = IAnnotations(self.image) geometries = annotations.get(THUMBNAIL_GEOMETRY_KEY, {}) # get default geometry for custom thumbnails - if ':' in thumbnail_name: - thumbnailer_name, options = thumbnail_name.split(':', 1) + if ':' in selection_name: + selection_name, options = selection_name.split(':', 1) else: - thumbnailer_name = thumbnail_name + selection_name = selection_name options = None - if thumbnailer_name in geometries: - return geometries[thumbnailer_name] + if selection_name in geometries: + return geometries[selection_name] registry = check_request().registry - thumbnailer = registry.queryAdapter(self.image, IThumbnailer, name=thumbnailer_name) + thumbnailer = registry.queryAdapter(self.image, IThumbnailer, name=selection_name) if thumbnailer is not None: return thumbnailer.get_default_geometry(options) - def set_thumbnail_geometry(self, thumbnail_name, geometry): + def set_geometry(self, selection_name, geometry): annotations = IAnnotations(self.image) geometries = annotations.get(THUMBNAIL_GEOMETRY_KEY) if geometries is None: geometries = annotations[THUMBNAIL_GEOMETRY_KEY] = PersistentDict() - geometries[thumbnail_name] = geometry + geometries[selection_name] = geometry for current_thumbnail_name in self.thumbnails.copy(): - if current_thumbnail_name.startswith(thumbnail_name): + if (current_thumbnail_name == selection_name) or \ + current_thumbnail_name.startswith(selection_name + ':'): self.delete_thumbnail(current_thumbnail_name) def clear_geometries(self): @@ -131,6 +132,32 @@ else: return None, None + def get_selection(self, selection_name, format=None): + if selection_name in self.thumbnails: + return self.thumbnails[selection_name] + geometry = self.get_geometry(selection_name) + registry = get_current_registry() + thumbnailer = registry.queryAdapter(self.image, IThumbnailer, name=selection_name) + if thumbnailer is not None: + selection = thumbnailer.create_thumbnail(geometry, format) + if selection is not None: + if isinstance(selection, tuple): + selection, format = selection + else: + format = 'jpeg' + selection = FileFactory(selection) + alsoProvides(selection, IThumbnailFile) + registry.notify(ObjectCreatedEvent(selection)) + self.thumbnails[selection_name] = selection + selection_size = selection.get_image_size() + locate(selection, self.image, + '++thumb++{0}:{1}x{2}.{3}'.format(selection_name, + selection_size[0], + selection_size[1], + format)) + registry.notify(ObjectAddedEvent(selection)) + return selection + def get_thumbnail(self, thumbnail_name, format=None, watermark=None): # check for existing thumbnail if thumbnail_name in self.thumbnails: @@ -151,7 +178,7 @@ thumbnailer_name, options = thumbnail_name.split(':', 1) else: thumbnailer_name = thumbnail_name - options = name = thumbnail_name + options = name = thumbnail_name # generate and store thumbnail registry = get_current_registry() thumbnailer = registry.queryAdapter(self.image, IThumbnailer, name=thumbnailer_name) @@ -212,6 +239,11 @@ thumbnail_name = name format = None thumbnails = IThumbnail(self.context) + if ':' in thumbnail_name: + selection_name, thumbnail_name = thumbnail_name.split(':', 1) + selection = thumbnails.get_selection(selection_name, format) + transaction.commit() + thumbnails = IThumbnail(selection) result = thumbnails.get_thumbnail(thumbnail_name, format) transaction.commit() return result diff -r 2f2e70d36b6e -r 58686bb1a7b9 src/pyams_file/widget/__init__.py --- a/src/pyams_file/widget/__init__.py Thu Apr 21 16:40:03 2016 +0200 +++ b/src/pyams_file/widget/__init__.py Tue Nov 15 09:55:02 2016 +0100 @@ -18,7 +18,7 @@ # import interfaces from pyams_file.interfaces import IFileField, IFileWidget, IImageField, IImageWidget, \ - IThumnailImageWidget, IThumbnailImageField, DELETED_FILE + IThumbnailImageWidget, IThumbnailImageField, DELETED_FILE from pyams_form.interfaces.form import IFormLayer from z3c.form.interfaces import NOT_CHANGED, IFieldWidget, IDataConverter @@ -101,7 +101,7 @@ return FieldWidget(field, ImageWidget(request)) -@implementer_only(IThumnailImageWidget) +@implementer_only(IThumbnailImageWidget) class ThumbnailImageWidget(ImageWidget): """Image widget with thumbnail images selection""" diff -r 2f2e70d36b6e -r 58686bb1a7b9 src/pyams_file/zmi/file.py --- a/src/pyams_file/zmi/file.py Thu Apr 21 16:40:03 2016 +0200 +++ b/src/pyams_file/zmi/file.py Tue Nov 15 09:55:02 2016 +0100 @@ -62,7 +62,8 @@ def updateWidgets(self, prefix=None): super(FilePropertiesEditForm, self).updateWidgets() - self.widgets['description'].label_css_class = 'textarea' + if 'description' in self.widgets: + self.widgets['description'].widget_css_class = 'textarea' @view_config(name='properties.json', context=IFile, request_type=IPyAMSLayer, diff -r 2f2e70d36b6e -r 58686bb1a7b9 src/pyams_file/zmi/image.py --- a/src/pyams_file/zmi/image.py Thu Apr 21 16:40:03 2016 +0200 +++ b/src/pyams_file/zmi/image.py Tue Nov 15 09:55:02 2016 +0100 @@ -16,7 +16,7 @@ # import standard library # import interfaces -from pyams_file.interfaces import IImage, IThumnailImageWidget, IThumbnail +from pyams_file.interfaces import IImage, IThumbnail, IResponsiveImage, IImageWidget from pyams_form.interfaces.form import IWidgetsPrefixViewletsManager, IFormHelp from pyams_skin.interfaces.viewlet import IContextActions from pyams_skin.layer import IPyAMSLayer @@ -24,7 +24,7 @@ from pyams_zmi.layer import IAdminLayer # import packages -from pyams_file.image import ThumbnailGeometrry +from pyams_file.image import ThumbnailGeometry from pyams_form.form import AJAXEditForm from pyams_form.help import FormHelp from pyams_form.schema import CloseButton @@ -43,7 +43,7 @@ from pyams_file import _ -@viewlet_config(name='image.resize.divider', context=IImage, layer=IPyAMSLayer, view=Interface, +@viewlet_config(name='image.resize.divider', context=IImage, layer=IPyAMSLayer, view=IImageWidget, manager=IContextActions, permission=MANAGE_PERMISSION, weight=19) class ImageDividerAction(ToolbarMenuDivider): """Image divider action""" @@ -53,7 +53,7 @@ # Image resize # -@viewlet_config(name='image.resize.action', context=IImage, layer=IPyAMSLayer, view=Interface, +@viewlet_config(name='image.resize.action', context=IImage, layer=IPyAMSLayer, view=IImageWidget, manager=IContextActions, permission=MANAGE_PERMISSION, weight=20) class ImageResizeAction(ToolbarMenuItem): """Image resize action""" @@ -153,7 +153,7 @@ # Image crop # -@viewlet_config(name='image.crop.action', context=IImage, layer=IPyAMSLayer, view=Interface, +@viewlet_config(name='image.crop.action', context=IImage, layer=IPyAMSLayer, view=IImageWidget, manager=IContextActions, permission=MANAGE_PERMISSION, weight=21) class ImageCropAction(ToolbarMenuItem): """Image crop action""" @@ -224,7 +224,7 @@ # Image square thumbnail selection # -@viewlet_config(name='image.thumb.divider', context=IImage, layer=IPyAMSLayer, view=IThumnailImageWidget, +@viewlet_config(name='image.thumb.divider', context=IImage, layer=IPyAMSLayer, view=IImageWidget, manager=IContextActions, permission=MANAGE_PERMISSION, weight=30) class ImageThumbnailsDividerAction(ToolbarMenuDivider): """Image divider action""" @@ -237,7 +237,7 @@ crop = button.Button(name='crop', title=_("Select thumbnail")) -@viewlet_config(name='image.thumb.square.action', context=IImage, layer=IPyAMSLayer, view=IThumnailImageWidget, +@viewlet_config(name='image.thumb.square.action', context=IImage, layer=IPyAMSLayer, view=IImageWidget, manager=IContextActions, permission=MANAGE_PERMISSION, weight=31) class ImageSquareThumbnailAction(ToolbarMenuItem): """Square thumbnail image selection""" @@ -277,12 +277,12 @@ """Image square thumbnail edit form, AJAX renderer""" def update_content(self, content, data): - geometry = ThumbnailGeometrry() + geometry = ThumbnailGeometry() geometry.x1 = int(self.request.params.get('selection.x1')) geometry.y1 = int(self.request.params.get('selection.y1')) geometry.x2 = int(self.request.params.get('selection.x2')) geometry.y2 = int(self.request.params.get('selection.y2')) - IThumbnail(self.context).set_thumbnail_geometry('square', geometry) + IThumbnail(self.context).set_geometry('square', geometry) def get_ajax_output(self, changes): return {'status': 'success', @@ -301,7 +301,7 @@ # Image panoramic thumbnail selection # -@viewlet_config(name='image.thumb.pano.action', context=IImage, layer=IAdminLayer, view=IThumnailImageWidget, +@viewlet_config(name='image.thumb.pano.action', context=IImage, layer=IAdminLayer, view=IImageWidget, manager=IContextActions, permission=MANAGE_PERMISSION, weight=32) class ImagePanoThumbnailAction(ToolbarMenuItem): """Panoramic thumbnail image selection""" @@ -312,11 +312,6 @@ url = 'pano-thumbnail.html' modal_target = True - def updateActions(self): - super(ImageSquareThumbnailEditForm, self).updateActions() - if 'crop' in self.actions: - self.actions['crop'].addClass('btn-primary') - @pagelet_config(name='pano-thumbnail.html', context=IImage, layer=IPyAMSLayer, permission=MANAGE_PERMISSION) class ImagePanoThumbnailEditForm(AdminDialogEditForm): @@ -346,12 +341,12 @@ """Image panoramic thumbnail edit form, AJAX renderer""" def update_content(self, content, data): - geometry = ThumbnailGeometrry() + geometry = ThumbnailGeometry() geometry.x1 = int(self.request.params.get('selection.x1')) geometry.y1 = int(self.request.params.get('selection.y1')) geometry.x2 = int(self.request.params.get('selection.x2')) geometry.y2 = int(self.request.params.get('selection.y2')) - IThumbnail(self.context).set_thumbnail_geometry('pano', geometry) + IThumbnail(self.context).set_geometry('pano', geometry) def get_ajax_output(self, changes): return {'status': 'success', @@ -364,3 +359,198 @@ @template_config(template='templates/image-pano-thumbnail.pt') class ImagePanoThumbnailViewletsPrefix(Viewlet): """Image panoramic thumbnail viewlets prefix""" + + +# +# Image responsive selections +# + +@viewlet_config(name='responsive-image.selection.divider', context=IResponsiveImage, layer=IPyAMSLayer, + view=IImageWidget, manager=IContextActions, permission=MANAGE_PERMISSION, weight=40) +class ResponsiveImageSelectionDividerAction(ToolbarMenuDivider): + """Image divider action""" + + +class IResponsiveImageSelectionFormButtons(Interface): + """Responsive image selection form buttons""" + + close = CloseButton(name='close', title=_("Close")) + select = button.Button(name='select', title=_("Select image")) + + +class ResponsiveImageSelectionForm(AdminDialogEditForm): + """Base responsive image selection edit form""" + + dialog_class = 'modal-large' + + fields = field.Fields(Interface) + buttons = button.Buttons(IResponsiveImageSelectionFormButtons) + + @property + def title(self): + return self.context.title or self.context.filename + + def updateActions(self): + super(ResponsiveImageSelectionForm, self).updateActions() + if 'select' in self.actions: + self.actions['select'].addClass('btn-primary') + + +class ResponsiveImageSelectionAJAXForm(AJAXEditForm): + """Base responsive image selection edit form, JSON renderer""" + + def update_content(self, content, data): + geometry = ThumbnailGeometry() + geometry.x1 = int(self.request.params.get('selection.x1')) + geometry.y1 = int(self.request.params.get('selection.y1')) + geometry.x2 = int(self.request.params.get('selection.x2')) + geometry.y2 = int(self.request.params.get('selection.y2')) + IThumbnail(self.context).set_geometry(self.selection_size, geometry) + + def get_ajax_output(self, changes): + return {'status': 'success', + 'smallbox': self.request.localizer.translate(self.successMessage), + 'smallbox_status': 'success'} + + +@viewlet_config(name='responsive-image.selection.widgets-prefix', context=IResponsiveImage, layer=IAdminLayer, + view=ResponsiveImageSelectionForm, manager=IWidgetsPrefixViewletsManager) +@template_config(template='templates/image-selection.pt') +class ResponsiveImageSelectionViewletsPrefix(Viewlet): + """Responsive image selection viewlets prefix""" + + +# +# Extra-small devices selection +# + +@viewlet_config(name='responsive-image.selection.xs.action', context=IResponsiveImage, layer=IAdminLayer, + view=IImageWidget, manager=IContextActions, permission=MANAGE_PERMISSION, weight=41) +class ResponsiveImageXsSelectionAction(ToolbarMenuItem): + """Responsive image XS selection""" + + label = _("Select responsive XS image...") + label_css_class = 'fa fa-fw fa-mobile' + + url = 'selection-xs.html' + modal_target = True + + +@pagelet_config(name='selection-xs.html', context=IResponsiveImage, layer=IPyAMSLayer, + permission=MANAGE_PERMISSION) +class ResponsiveImageXsSelectionForm(ResponsiveImageSelectionForm): + """Responsive image XS selection edit form""" + + legend = _("Select image for extra-small (XS) devices") + icon_css_class = 'fa fa-fw fa-mobile' + + selection_size = 'xs' + ajax_handler = 'selection-xs.json' + + +@view_config(name='selection-xs.json', context=IResponsiveImage, request_type=IPyAMSLayer, + permission=MANAGE_PERMISSION, renderer='json', xhr=True) +class ResponsiveImageXsSelectionAJAXEditForm(ResponsiveImageSelectionAJAXForm, ResponsiveImageXsSelectionForm): + """Responsive image XS selection edit form, JSON renderer""" + + +# +# Small devices selection +# + +@viewlet_config(name='responsive-image.selection.sm.action', context=IResponsiveImage, layer=IAdminLayer, + view=IImageWidget, manager=IContextActions, permission=MANAGE_PERMISSION, weight=42) +class ResponsiveImageSmSelectionAction(ToolbarMenuItem): + """Responsive image SM selection""" + + label = _("Select responsive SM image...") + label_css_class = 'fa fa-fw fa-tablet' + + url = 'selection-sm.html' + modal_target = True + + +@pagelet_config(name='selection-sm.html', context=IResponsiveImage, layer=IPyAMSLayer, + permission=MANAGE_PERMISSION) +class ResponsiveImageSmSelectionForm(ResponsiveImageSelectionForm): + """Responsive image SM selection edit form""" + + legend = _("Select image for small (SM) devices") + icon_css_class = 'fa fa-fw fa-tablet' + + selection_size = 'sm' + ajax_handler = 'selection-sm.json' + + +@view_config(name='selection-sm.json', context=IResponsiveImage, request_type=IPyAMSLayer, + permission=MANAGE_PERMISSION, renderer='json', xhr=True) +class ResponsiveImageSmSelectionAJAXEditForm(ResponsiveImageSelectionAJAXForm, ResponsiveImageSmSelectionForm): + """Responsive image SM selection edit form, JSON renderer""" + + +# +# Medium devices selection +# + +@viewlet_config(name='responsive-image.selection.md.action', context=IResponsiveImage, layer=IAdminLayer, + view=IImageWidget, manager=IContextActions, permission=MANAGE_PERMISSION, weight=43) +class ResponsiveImageMdSelectionAction(ToolbarMenuItem): + """Responsive image MD selection""" + + label = _("Select responsive MD image...") + label_css_class = 'fa fa-fw fa-desktop' + + url = 'selection-md.html' + modal_target = True + + +@pagelet_config(name='selection-md.html', context=IResponsiveImage, layer=IPyAMSLayer, + permission=MANAGE_PERMISSION) +class ResponsiveImageMdSelectionForm(ResponsiveImageSelectionForm): + """Responsive image MD selection edit form""" + + legend = _("Select image for medium (MD) devices") + icon_css_class = 'fa fa-fw fa-desktop' + + selection_size = 'md' + ajax_handler = 'selection-md.json' + + +@view_config(name='selection-md.json', context=IResponsiveImage, request_type=IPyAMSLayer, + permission=MANAGE_PERMISSION, renderer='json', xhr=True) +class ResponsiveImageMdSelectionAJAXEditForm(ResponsiveImageSelectionAJAXForm, ResponsiveImageMdSelectionForm): + """Responsive image MD selection edit form, JSON renderer""" + + +# +# Large devices selection +# + +@viewlet_config(name='responsive-image.selection.lg.action', context=IResponsiveImage, layer=IAdminLayer, + view=IImageWidget, manager=IContextActions, permission=MANAGE_PERMISSION, weight=44) +class ResponsiveImageLgSelectionAction(ToolbarMenuItem): + """Responsive image LG selection""" + + label = _("Select responsive LG image...") + label_css_class = 'fa fa-fw fa-television' + + url = 'selection-lg.html' + modal_target = True + + +@pagelet_config(name='selection-lg.html', context=IResponsiveImage, layer=IPyAMSLayer, + permission=MANAGE_PERMISSION) +class ResponsiveImageLgSelectionForm(ResponsiveImageSelectionForm): + """Responsive image LG selection edit form""" + + legend = _("Select image for large (LG) devices") + icon_css_class = 'fa fa-fw fa-television' + + selection_size = 'lg' + ajax_handler = 'selection-lg.json' + + +@view_config(name='selection-lg.json', context=IResponsiveImage, request_type=IPyAMSLayer, + permission=MANAGE_PERMISSION, renderer='json', xhr=True) +class ResponsiveImageLgSelectionAJAXEditForm(ResponsiveImageSelectionAJAXForm, ResponsiveImageLgSelectionForm): + """Responsive image LG selection edit form, JSON renderer""" diff -r 2f2e70d36b6e -r 58686bb1a7b9 src/pyams_file/zmi/templates/image-pano-thumbnail.pt --- a/src/pyams_file/zmi/templates/image-pano-thumbnail.pt Thu Apr 21 16:40:03 2016 +0200 +++ b/src/pyams_file/zmi/templates/image-pano-thumbnail.pt Tue Nov 15 09:55:02 2016 +0100 @@ -2,7 +2,7 @@ image thumbnails.get_thumbnail('800x600', 'jpeg'); size context.get_image_size(); thumb_size image.get_image_size(); - geometry thumbnails.get_thumbnail_geometry('pano');"> + geometry thumbnails.get_geometry('pano');"> diff -r 2f2e70d36b6e -r 58686bb1a7b9 src/pyams_file/zmi/templates/image-selection.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/zmi/templates/image-selection.pt Tue Nov 15 09:55:02 2016 +0100 @@ -0,0 +1,22 @@ + + + + + + + diff -r 2f2e70d36b6e -r 58686bb1a7b9 src/pyams_file/zmi/templates/image-square-thumbnail.pt --- a/src/pyams_file/zmi/templates/image-square-thumbnail.pt Thu Apr 21 16:40:03 2016 +0200 +++ b/src/pyams_file/zmi/templates/image-square-thumbnail.pt Tue Nov 15 09:55:02 2016 +0100 @@ -2,7 +2,7 @@ image thumbnails.get_thumbnail('800x600', 'jpeg'); size context.get_image_size(); thumb_size image.get_image_size(); - geometry thumbnails.get_thumbnail_geometry('square');"> + geometry thumbnails.get_geometry('square');">