--- 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"""
--- 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
Binary file src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.mo has changed
--- 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 <tflorac@ulthar.net>\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"
--- 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 <EMAIL@ADDRESS>, 2015.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS\n"
"Language-Team: LANGUAGE <LL@li.org>\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 ""
--- 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
--- 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"""
--- 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,
--- 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"""
--- 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');">
<input type="hidden" name="selection.x1" tal:attributes="value geometry.x1" />
<input type="hidden" name="selection.y1" tal:attributes="value geometry.x2" />
<input type="hidden" name="selection.x2" tal:attributes="value geometry.y1" />
--- /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 @@
+<tal:var define="thumbnails extension:thumbnails(context);
+ image thumbnails.get_thumbnail('800x600', 'jpeg');
+ size context.get_image_size();
+ thumb_size image.get_image_size();
+ geometry thumbnails.get_geometry(view.__parent__.selection_size);">
+ <input type="hidden" name="selection.x1" tal:attributes="value geometry.x1" />
+ <input type="hidden" name="selection.y1" tal:attributes="value geometry.x2" />
+ <input type="hidden" name="selection.x2" tal:attributes="value geometry.y1" />
+ <input type="hidden" name="selection.y2" tal:attributes="value geometry.y2" />
+ <img class="imgareaselect"
+ data-ams-imgareaselect-parent=".modal-dialog"
+ data-ams-imgareaselect-target-field="selection."
+ tal:attributes="src extension:absolute_url(image);
+ width thumb_size[0];
+ height thumb_size[1];
+ data-ams-imgareaselect-image-width size[0];
+ data-ams-imgareaselect-image-height size[1];
+ data-ams-imgareaselect-x1 geometry.x1;
+ data-ams-imgareaselect-y1 geometry.y1;
+ data-ams-imgareaselect-x2 geometry.x2;
+ data-ams-imgareaselect-y2 geometry.y2;" />
+</tal:var>
--- 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');">
<input type="hidden" name="selection.x1" tal:attributes="value geometry.x1" />
<input type="hidden" name="selection.y1" tal:attributes="value geometry.x2" />
<input type="hidden" name="selection.x2" tal:attributes="value geometry.y1" />