Added image rotate action and modified timestamps
authorThierry Florac <thierry.florac@onf.fr>
Wed, 27 Jun 2018 12:19:42 +0200
changeset 106 db778bfa5f17
parent 105 2cbaa43fddec
child 107 62ff814702f6
Added image rotate action and modified timestamps
src/pyams_file/file.py
src/pyams_file/interfaces/__init__.py
src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.mo
src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.po
src/pyams_file/locales/pyams_file.pot
src/pyams_file/widget/__init__.py
src/pyams_file/widget/templates/media-display.pt
src/pyams_file/widget/templates/media-input.pt
src/pyams_file/zmi/image.py
src/pyams_file/zmi/templates/image-crop.pt
src/pyams_file/zmi/templates/image-selection.pt
src/pyams_file/zmi/templates/image-thumbnails.pt
--- a/src/pyams_file/file.py	Sat Jun 23 13:02:47 2018 +0200
+++ b/src/pyams_file/file.py	Wed Jun 27 12:19:42 2018 +0200
@@ -254,6 +254,15 @@
         request = check_request()
         request.registry.notify(FileModifiedEvent(self))
 
+    def rotate(self, angle=-90):
+        image = Image.open(self.get_blob(mode='c'))
+        new_image = BytesIO()
+        image.rotate(angle, expand=True) \
+             .save(new_image, image.format, quality=99)
+        self.data = new_image
+        request = check_request()
+        request.registry.notify(FileModifiedEvent(self))
+
 
 @implementer(ISVGImage)
 class SVGImageFile(File):
--- a/src/pyams_file/interfaces/__init__.py	Sat Jun 23 13:02:47 2018 +0200
+++ b/src/pyams_file/interfaces/__init__.py	Wed Jun 27 12:19:42 2018 +0200
@@ -84,6 +84,9 @@
     def crop(self, x1, y1, x2, y2):
         """Crop image to given coordinates"""
 
+    def rotate(self, angle=-90):
+        """Rotate image, default to right"""
+
 
 class ISVGImage(IBaseImage):
     """SVG file interface"""
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	Sat Jun 23 13:02:47 2018 +0200
+++ b/src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.po	Wed Jun 27 12:19:42 2018 +0200
@@ -5,7 +5,7 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE 1.0\n"
-"POT-Creation-Date: 2018-06-08 12:31+0200\n"
+"POT-Creation-Date: 2018-06-27 12:17+0200\n"
 "PO-Revision-Date: 2015-02-06 21:39+0100\n"
 "Last-Translator: Thierry Florac <tflorac@ulthar.net>\n"
 "Language-Team: French\n"
@@ -71,13 +71,9 @@
 msgid "Delete content"
 msgstr "Supprimer ce contenu"
 
-#: src/pyams_file/widget/templates/media-input.pt:30
-#: src/pyams_file/widget/templates/media-display.pt:12
-msgid "Zoom image"
-msgstr "Agrandir l'image"
-
-#: src/pyams_file/widget/templates/media-input.pt:36
-#: src/pyams_file/widget/templates/media-input.pt:68
+#: src/pyams_file/widget/templates/media-input.pt:27
+#: src/pyams_file/widget/templates/media-input.pt:64
+#: src/pyams_file/widget/templates/media-input.pt:96
 #: src/pyams_file/widget/templates/file-input.pt:31
 #: src/pyams_file/widget/templates/file-display.pt:14
 #: src/pyams_file/widget/templates/media-display.pt:20
@@ -85,8 +81,9 @@
 msgid "Current value:"
 msgstr "Contenu actuel :"
 
-#: src/pyams_file/widget/templates/media-input.pt:52
-#: src/pyams_file/widget/templates/media-input.pt:82
+#: src/pyams_file/widget/templates/media-input.pt:41
+#: src/pyams_file/widget/templates/media-input.pt:80
+#: src/pyams_file/widget/templates/media-input.pt:110
 #: src/pyams_file/widget/templates/file-input.pt:45
 #: src/pyams_file/widget/templates/file-display.pt:21
 #: src/pyams_file/widget/templates/media-display.pt:30
@@ -94,6 +91,11 @@
 msgid "Download"
 msgstr "Enregistrer sous..."
 
+#: src/pyams_file/widget/templates/media-input.pt:55
+#: src/pyams_file/widget/templates/media-display.pt:12
+msgid "Zoom image"
+msgstr "Agrandir l'image"
+
 #: src/pyams_file/zmi/file.py:42
 msgid "Properties..."
 msgstr "Propriétés"
@@ -102,15 +104,19 @@
 msgid "Update file properties"
 msgstr "Mise à jour des propriétés"
 
-#: src/pyams_file/zmi/image.py:61
+#: src/pyams_file/zmi/image.py:107
+msgid "Rotate image to right..."
+msgstr "Tourner l'image vers la droite"
+
+#: src/pyams_file/zmi/image.py:156
 msgid "Crop image..."
 msgstr "Recadrer l'image"
 
-#: src/pyams_file/zmi/image.py:83
+#: src/pyams_file/zmi/image.py:178
 msgid "Crop image"
 msgstr "Recadrer l'image"
 
-#: src/pyams_file/zmi/image.py:126
+#: src/pyams_file/zmi/image.py:226
 msgid ""
 "You can use this form to crop an image.\n"
 "\n"
@@ -128,7 +134,7 @@
 "ou les vignettes déjà sélectionnées sur la base de l'ancienne image sont "
 "réinitialisées !"
 
-#: src/pyams_file/zmi/image.py:140
+#: src/pyams_file/zmi/image.py:240
 msgid "You can use this form to make a selection on an image."
 msgstr ""
 "Par défaut, l'image est affichée dans son intégralité quel que soit le type "
@@ -141,39 +147,39 @@
 "**ATTENTION** : si l'image d'origine est recadrée ou rechargée, la fonction "
 "est réinitialisée et il faut procéder à un nouveau choix."
 
-#: src/pyams_file/zmi/image.py:160
+#: src/pyams_file/zmi/image.py:308
 msgid "Select portrait thumbnail..."
 msgstr "Vignette portrait"
 
-#: src/pyams_file/zmi/image.py:182
+#: src/pyams_file/zmi/image.py:330
 msgid "Select portrait thumbnail"
 msgstr "Emprise de la vignette en mode portrait"
 
-#: src/pyams_file/zmi/image.py:225
+#: src/pyams_file/zmi/image.py:341
 msgid ""
 "You can use this form to select a portrait thumbnail of this image.\n"
 "\n"
 "**WARNING**: cropping or resizing an image will reset all selected "
 "thumbnails and adaptive images!!"
 msgstr ""
-"L'utilisation d'une vignette en mode portrait n'est pas systématique, elle dépend "
-"du modèle de présentation qui peut ou non y faire appel. Par défaut, la "
-"vignette est positionnée au centre de l'image, la sélection de "
-"son emprise ne modifie pas l'image d'origine.\n"
+"L'utilisation d'une vignette en mode portrait n'est pas systématique, elle "
+"dépend du modèle de présentation qui peut ou non y faire appel. Par défaut, "
+"la vignette est positionnée au centre de l'image, la sélection de son "
+"emprise ne modifie pas l'image d'origine.\n"
 "\n"
 "**ATTENTION** : lorsqu'une image est recadrée, redimentionnée ou rechargée, "
 "la sélection est réinitialisée sur sa position par défaut (centrée) ; s'il y "
 "a lieu, vous devez procéder à une nouvelle sélection personnalisée."
 
-#: src/pyams_file/zmi/image.py:240
+#: src/pyams_file/zmi/image.py:356
 msgid "Select square thumbnail..."
 msgstr "Vignette carrée"
 
-#: src/pyams_file/zmi/image.py:262
+#: src/pyams_file/zmi/image.py:378
 msgid "Select square thumbnail"
 msgstr "Emprise de la vignette carrée"
 
-#: src/pyams_file/zmi/image.py:305
+#: src/pyams_file/zmi/image.py:389
 msgid ""
 "You can use this form to select a square thumbnail of this image.\n"
 "\n"
@@ -189,15 +195,15 @@
 "la sélection est réinitialisée sur sa position par défaut (centrée) ; s'il y "
 "a lieu, vous devez procéder à une nouvelle sélection personnalisée."
 
-#: src/pyams_file/zmi/image.py:320
+#: src/pyams_file/zmi/image.py:404
 msgid "Select panoramic thumbnail..."
 msgstr "Vignette panoramique"
 
-#: src/pyams_file/zmi/image.py:342
+#: src/pyams_file/zmi/image.py:426
 msgid "Select panoramic thumbnail"
 msgstr "Emprise de la vignette panoramique"
 
-#: src/pyams_file/zmi/image.py:385
+#: src/pyams_file/zmi/image.py:437
 msgid ""
 "You can use this form to select a panoramic thumbnail of this image.\n"
 "\n"
@@ -213,55 +219,55 @@
 "la sélection est réinitialisée sur sa position par défaut (centrée) ; s'il y "
 "a lieu, vous devez procéder à une nouvelle sélection personnalisée."
 
-#: src/pyams_file/zmi/image.py:458
+#: src/pyams_file/zmi/image.py:452
 msgid "Select responsive XS image..."
 msgstr "Image adaptative pour smartphones"
 
-#: src/pyams_file/zmi/image.py:479
+#: src/pyams_file/zmi/image.py:473
 msgid "Select image for extra-small (XS) devices"
 msgstr "Portion de l'image affichée sur les smartphones (taille XS)"
 
-#: src/pyams_file/zmi/image.py:494
+#: src/pyams_file/zmi/image.py:488
 msgid "Select responsive SM image..."
 msgstr "Image adaptative pour tablettes"
 
-#: src/pyams_file/zmi/image.py:515
+#: src/pyams_file/zmi/image.py:509
 msgid "Select image for small (SM) devices"
 msgstr "Portion de l'image affichée sur les tablettes (taille SM)"
 
-#: src/pyams_file/zmi/image.py:530
+#: src/pyams_file/zmi/image.py:524
 msgid "Select responsive MD image..."
 msgstr "Image adaptative pour terminaux moyens"
 
-#: src/pyams_file/zmi/image.py:551
+#: src/pyams_file/zmi/image.py:545
 msgid "Select image for medium (MD) devices"
 msgstr "Portion de l'image affichée sur les terminaux moyens (taille MD)"
 
-#: src/pyams_file/zmi/image.py:566
+#: src/pyams_file/zmi/image.py:560
 msgid "Select responsive LG image..."
 msgstr "Image adaptative pour grands terminaux"
 
-#: src/pyams_file/zmi/image.py:587
+#: src/pyams_file/zmi/image.py:581
 msgid "Select image for large (LG) devices"
 msgstr "Portion de l'image affichée sur les grands terminaux (taille LG)"
 
-#: src/pyams_file/zmi/image.py:602
+#: src/pyams_file/zmi/image.py:596
 msgid "Display all thumbnails"
 msgstr "Voir toutes les vignettes"
 
-#: src/pyams_file/zmi/image.py:619
+#: src/pyams_file/zmi/image.py:613
 msgid "Display all image thumbnails"
 msgstr "Récapitulatif des vignettes associées"
 
-#: src/pyams_file/zmi/image.py:664
+#: src/pyams_file/zmi/image.py:654
 msgid "Resize image..."
 msgstr "Redimensionner l'image"
 
-#: src/pyams_file/zmi/image.py:718 src/pyams_file/zmi/image.py:675
+#: src/pyams_file/zmi/image.py:708 src/pyams_file/zmi/image.py:665
 msgid "Resize image"
 msgstr "Redimensionner l'image"
 
-#: src/pyams_file/zmi/image.py:749
+#: src/pyams_file/zmi/image.py:739
 msgid ""
 "You can use this form to change image dimensions.\n"
 "\n"
@@ -273,32 +279,32 @@
 "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:71 src/pyams_file/zmi/image.py:147
-#: src/pyams_file/zmi/image.py:398 src/pyams_file/zmi/image.py:674
+#: src/pyams_file/zmi/image.py:166 src/pyams_file/zmi/image.py:247
+#: src/pyams_file/zmi/image.py:664
 msgid "Close"
 msgstr "Fermer"
 
-#: src/pyams_file/zmi/image.py:72
+#: src/pyams_file/zmi/image.py:167
 msgid "Crop"
 msgstr "Recadrer l'image"
 
-#: src/pyams_file/zmi/image.py:148 src/pyams_file/zmi/image.py:399
+#: src/pyams_file/zmi/image.py:248
 msgid "Select thumbnail"
 msgstr "Sélectionner cette portion d'image"
 
-#: src/pyams_file/zmi/image.py:681
+#: src/pyams_file/zmi/image.py:671
 msgid "New image width"
 msgstr "Largeur de l'image"
 
-#: src/pyams_file/zmi/image.py:683
+#: src/pyams_file/zmi/image.py:673
 msgid "New image height"
 msgstr "Hauteur de l'image"
 
-#: src/pyams_file/zmi/image.py:685
+#: src/pyams_file/zmi/image.py:675
 msgid "Keep aspect ratio"
 msgstr "Ne pas déformer l'image"
 
-#: src/pyams_file/zmi/image.py:686
+#: src/pyams_file/zmi/image.py:676
 msgid ""
 "Check to keep original aspect ratio; image will be resized as large as "
 "possible within given limits"
@@ -307,27 +313,27 @@
 "L'image sera redimensionnée (sans jamais être agrandie !) pour être aussi "
 "grande que possible en fonction des contraintes indiquées."
 
-#: src/pyams_file/interfaces/__init__.py:99
+#: src/pyams_file/interfaces/__init__.py:110
 msgid "Title"
 msgstr "Titre"
 
-#: src/pyams_file/interfaces/__init__.py:102
+#: src/pyams_file/interfaces/__init__.py:113
 msgid "Description"
 msgstr "Description"
 
-#: src/pyams_file/interfaces/__init__.py:105
+#: src/pyams_file/interfaces/__init__.py:116
 msgid "Save file as..."
 msgstr "Enregistrer sous..."
 
-#: src/pyams_file/interfaces/__init__.py:106
+#: src/pyams_file/interfaces/__init__.py:117
 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:109
+#: src/pyams_file/interfaces/__init__.py:120
 msgid "Language"
 msgstr "Langue"
 
-#: src/pyams_file/interfaces/__init__.py:110
+#: src/pyams_file/interfaces/__init__.py:121
 msgid "File's content language"
 msgstr "Langue du contenu du fichier"
 
--- a/src/pyams_file/locales/pyams_file.pot	Sat Jun 23 13:02:47 2018 +0200
+++ b/src/pyams_file/locales/pyams_file.pot	Wed Jun 27 12:19:42 2018 +0200
@@ -6,7 +6,7 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE 1.0\n"
-"POT-Creation-Date: 2018-06-08 12:31+0200\n"
+"POT-Creation-Date: 2018-06-27 12:17+0200\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"
@@ -71,13 +71,9 @@
 msgid "Delete content"
 msgstr ""
 
-#: ./src/pyams_file/widget/templates/media-input.pt:30
-#: ./src/pyams_file/widget/templates/media-display.pt:12
-msgid "Zoom image"
-msgstr ""
-
-#: ./src/pyams_file/widget/templates/media-input.pt:36
-#: ./src/pyams_file/widget/templates/media-input.pt:68
+#: ./src/pyams_file/widget/templates/media-input.pt:27
+#: ./src/pyams_file/widget/templates/media-input.pt:64
+#: ./src/pyams_file/widget/templates/media-input.pt:96
 #: ./src/pyams_file/widget/templates/file-input.pt:31
 #: ./src/pyams_file/widget/templates/file-display.pt:14
 #: ./src/pyams_file/widget/templates/media-display.pt:20
@@ -85,8 +81,9 @@
 msgid "Current value:"
 msgstr ""
 
-#: ./src/pyams_file/widget/templates/media-input.pt:52
-#: ./src/pyams_file/widget/templates/media-input.pt:82
+#: ./src/pyams_file/widget/templates/media-input.pt:41
+#: ./src/pyams_file/widget/templates/media-input.pt:80
+#: ./src/pyams_file/widget/templates/media-input.pt:110
 #: ./src/pyams_file/widget/templates/file-input.pt:45
 #: ./src/pyams_file/widget/templates/file-display.pt:21
 #: ./src/pyams_file/widget/templates/media-display.pt:30
@@ -94,6 +91,11 @@
 msgid "Download"
 msgstr ""
 
+#: ./src/pyams_file/widget/templates/media-input.pt:55
+#: ./src/pyams_file/widget/templates/media-display.pt:12
+msgid "Zoom image"
+msgstr ""
+
 #: ./src/pyams_file/zmi/file.py:42
 msgid "Properties..."
 msgstr ""
@@ -102,176 +104,180 @@
 msgid "Update file properties"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:61
+#: ./src/pyams_file/zmi/image.py:107
+msgid "Rotate image to right..."
+msgstr ""
+
+#: ./src/pyams_file/zmi/image.py:156
 msgid "Crop image..."
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:83
+#: ./src/pyams_file/zmi/image.py:178
 msgid "Crop image"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:126
+#: ./src/pyams_file/zmi/image.py:226
 msgid ""
 "You can use this form to crop an image.\n"
 "\n"
 "**WARNING**: cropping an image will reset all selected thumbnails and adaptive images!!"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:140
+#: ./src/pyams_file/zmi/image.py:240
 msgid "You can use this form to make a selection on an image."
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:160
+#: ./src/pyams_file/zmi/image.py:308
 msgid "Select portrait thumbnail..."
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:182
+#: ./src/pyams_file/zmi/image.py:330
 msgid "Select portrait thumbnail"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:225
+#: ./src/pyams_file/zmi/image.py:341
 msgid ""
 "You can use this form to select a portrait thumbnail of this image.\n"
 "\n"
 "**WARNING**: cropping or resizing an image will reset all selected thumbnails and adaptive images!!"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:240
+#: ./src/pyams_file/zmi/image.py:356
 msgid "Select square thumbnail..."
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:262
+#: ./src/pyams_file/zmi/image.py:378
 msgid "Select square thumbnail"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:305
+#: ./src/pyams_file/zmi/image.py:389
 msgid ""
 "You can use this form to select a square thumbnail of this image.\n"
 "\n"
 "**WARNING**: cropping or resizing an image will reset all selected thumbnails and adaptive images!!"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:320
+#: ./src/pyams_file/zmi/image.py:404
 msgid "Select panoramic thumbnail..."
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:342
+#: ./src/pyams_file/zmi/image.py:426
 msgid "Select panoramic thumbnail"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:385
+#: ./src/pyams_file/zmi/image.py:437
 msgid ""
 "You can use this form to select a panoramic thumbnail of this image.\n"
 "\n"
 "**WARNING**: cropping or resizing an image will reset all selected thumbnails and adaptive images!!"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:458
+#: ./src/pyams_file/zmi/image.py:452
 msgid "Select responsive XS image..."
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:479
+#: ./src/pyams_file/zmi/image.py:473
 msgid "Select image for extra-small (XS) devices"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:494
+#: ./src/pyams_file/zmi/image.py:488
 msgid "Select responsive SM image..."
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:515
+#: ./src/pyams_file/zmi/image.py:509
 msgid "Select image for small (SM) devices"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:530
+#: ./src/pyams_file/zmi/image.py:524
 msgid "Select responsive MD image..."
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:551
+#: ./src/pyams_file/zmi/image.py:545
 msgid "Select image for medium (MD) devices"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:566
+#: ./src/pyams_file/zmi/image.py:560
 msgid "Select responsive LG image..."
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:587
+#: ./src/pyams_file/zmi/image.py:581
 msgid "Select image for large (LG) devices"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:602
+#: ./src/pyams_file/zmi/image.py:596
 msgid "Display all thumbnails"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:619
+#: ./src/pyams_file/zmi/image.py:613
 msgid "Display all image thumbnails"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:664
+#: ./src/pyams_file/zmi/image.py:654
 msgid "Resize image..."
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:718 ./src/pyams_file/zmi/image.py:675
+#: ./src/pyams_file/zmi/image.py:708 ./src/pyams_file/zmi/image.py:665
 msgid "Resize image"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:749
+#: ./src/pyams_file/zmi/image.py:739
 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:71 ./src/pyams_file/zmi/image.py:147
-#: ./src/pyams_file/zmi/image.py:398 ./src/pyams_file/zmi/image.py:674
+#: ./src/pyams_file/zmi/image.py:166 ./src/pyams_file/zmi/image.py:247
+#: ./src/pyams_file/zmi/image.py:664
 msgid "Close"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:72
+#: ./src/pyams_file/zmi/image.py:167
 msgid "Crop"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:148 ./src/pyams_file/zmi/image.py:399
+#: ./src/pyams_file/zmi/image.py:248
 msgid "Select thumbnail"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:681
+#: ./src/pyams_file/zmi/image.py:671
 msgid "New image width"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:683
+#: ./src/pyams_file/zmi/image.py:673
 msgid "New image height"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:685
+#: ./src/pyams_file/zmi/image.py:675
 msgid "Keep aspect ratio"
 msgstr ""
 
-#: ./src/pyams_file/zmi/image.py:686
+#: ./src/pyams_file/zmi/image.py:676
 msgid ""
 "Check to keep original aspect ratio; image will be resized as large as "
 "possible within given limits"
 msgstr ""
 
-#: ./src/pyams_file/interfaces/__init__.py:99
+#: ./src/pyams_file/interfaces/__init__.py:110
 msgid "Title"
 msgstr ""
 
-#: ./src/pyams_file/interfaces/__init__.py:102
+#: ./src/pyams_file/interfaces/__init__.py:113
 msgid "Description"
 msgstr ""
 
-#: ./src/pyams_file/interfaces/__init__.py:105
+#: ./src/pyams_file/interfaces/__init__.py:116
 msgid "Save file as..."
 msgstr ""
 
-#: ./src/pyams_file/interfaces/__init__.py:106
+#: ./src/pyams_file/interfaces/__init__.py:117
 msgid "Name under which the file will be saved"
 msgstr ""
 
-#: ./src/pyams_file/interfaces/__init__.py:109
+#: ./src/pyams_file/interfaces/__init__.py:120
 msgid "Language"
 msgstr ""
 
-#: ./src/pyams_file/interfaces/__init__.py:110
+#: ./src/pyams_file/interfaces/__init__.py:121
 msgid "File's content language"
 msgstr ""
--- a/src/pyams_file/widget/__init__.py	Sat Jun 23 13:02:47 2018 +0200
+++ b/src/pyams_file/widget/__init__.py	Wed Jun 27 12:19:42 2018 +0200
@@ -24,6 +24,7 @@
 from pyams_form.interfaces.form import IFormLayer
 from pyramid.interfaces import IView
 from z3c.form.interfaces import NOT_CHANGED, IFieldWidget, IDataConverter
+from zope.dublincore.interfaces import IZopeDublinCore
 
 # import packages
 from pyams_file.file import EXTENSIONS_THUMBNAILS
@@ -72,7 +73,11 @@
 
     @property
     def timestamp(self):
-        return datetime.utcnow().timestamp()
+        dc = IZopeDublinCore(self.current_value, None)
+        if dc is None:
+            return datetime.utcnow().timestamp()
+        else:
+            return dc.modified.timestamp()
 
     @property
     def current_value(self):
--- a/src/pyams_file/widget/templates/media-display.pt	Sat Jun 23 13:02:47 2018 +0200
+++ b/src/pyams_file/widget/templates/media-display.pt	Wed Jun 27 12:19:42 2018 +0200
@@ -10,10 +10,10 @@
 					   target python:thumbnails.get_thumbnail('800x600', 'jpeg');"
 		   tal:attributes="href extension:absolute_url(target);"
 		   title="Zoom image" i18n:attributes="title">
-			<img class="thumbnail"
+			<img class="thumbnail" title="" src="" alt=""
 				 tal:define="thumbnail python:thumbnails.get_thumbnail('128x128');"
-				 tal:attributes="src extension:absolute_url(thumbnail);
-								 title i18n:value.title;" title="" src="" alt="" />
+				 tal:attributes="src string:${extension:absolute_url(thumbnail)}?_=${extension:timestamp(thumbnail)};
+								 title i18n:value.title;" />
 		</a>
 		<tal:if condition="python:value.content_type.startswith('image/')">
 			<div class="margin-top-5">
--- a/src/pyams_file/widget/templates/media-input.pt	Sat Jun 23 13:02:47 2018 +0200
+++ b/src/pyams_file/widget/templates/media-input.pt	Wed Jun 27 12:19:42 2018 +0200
@@ -53,9 +53,12 @@
 						   target python:view.get_thumbnail('800x600');"
 			   tal:attributes="href extension:absolute_url(target);"
 			   title="Zoom image" i18n:attributes="title">
-				<img class="thumbnail"
-					 tal:attributes="src extension:absolute_url(thumbnail);
-									 title i18n:value.title;" title="" src="" alt="" />
+				<img class="thumbnail" title="" src="" alt=""
+					 tal:define="url extension:absolute_url(thumbnail);
+								 timestamp extension:timestamp(thumbnail);"
+					 tal:attributes="id string:thumbnail_${extension:cache_key(value)};
+									 src string:${url}?_=${timestamp};
+									 title i18n:value.title;" />
 			</a>
 			<div class="margin-top-5">
 				<span i18n:translate="">Current value: </span>
--- a/src/pyams_file/zmi/image.py	Sat Jun 23 13:02:47 2018 +0200
+++ b/src/pyams_file/zmi/image.py	Wed Jun 27 12:19:42 2018 +0200
@@ -9,6 +9,7 @@
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE.
 #
+from transaction.interfaces import ITransactionManager
 
 __docformat__ = 'restructuredtext'
 
@@ -24,10 +25,12 @@
 from pyams_file.interfaces import IImage, IThumbnail, IResponsiveImage, IFileModifierForm, IThumbnailer, \
     IFileInfo, IThumbnailForm, IMediaWidget, ISVGImage
 from pyams_form.interfaces.form import IWidgetsPrefixViewletsManager, IFormHelp
+from pyams_i18n.interfaces import II18n
 from pyams_skin.interfaces.viewlet import IContextActions
 from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import VIEW_PERMISSION
+from pyams_utils.interfaces import VIEW_PERMISSION, ICacheKeyValue
 from pyams_zmi.layer import IAdminLayer
+from zope.dublincore.interfaces import IZopeDublinCore
 
 # import packages
 from pyams_file.image import ThumbnailGeometry
@@ -43,6 +46,7 @@
 from pyams_viewlet.viewlet import viewlet_config, Viewlet
 from pyams_zmi.form import AdminDialogEditForm
 from pyramid.renderers import render
+from pyramid.view import view_config
 from z3c.form import field, button
 from zope.interface import implementer, Interface
 from zope.schema import Int, Bool
@@ -92,6 +96,55 @@
 
 
 #
+# Image rotate
+#
+
+@viewlet_config(name='image-rotate.action', context=IImage, layer=IPyAMSLayer, view=IMediaWidget,
+                manager=IContextActions, weight=20)
+class ImageRotateAction(FileModifierAction):
+    """Image rotate action"""
+
+    label = _("Rotate image to right...")
+    label_css_class = 'fa fa-fw-md fa-rotate-right'
+
+    @property
+    def url(self):
+        return 'MyAMS.ajax.getJSON?url={0}'.format(absolute_url(self.context, self.request, 'rotate.json'))
+
+    def get_url(self):
+        return self.url
+
+
+@view_config(name='rotate.json', context=IImage, request_type=IPyAMSLayer,
+             permission=VIEW_PERMISSION, renderer='json', xhr=True)
+def rotate_image(request):
+    """Rotate given image to right"""
+    image = request.context
+    IImage(image).rotate(-90)
+    # Commit to save blobs!!
+    ITransactionManager(image).commit()
+    cache_key = ICacheKeyValue(image)
+    thumbnail = IThumbnail(image).get_thumbnail('128x128')
+    dc = IZopeDublinCore(thumbnail)
+    return {
+        'status': 'success',
+        'message': request.localizer.translate(AdminDialogEditForm.successMessage),
+        'callbacks': [{
+            'callback': 'MyAMS.skin.refreshContent',
+            'options': {
+                'object_id': 'thumbnail_{0}'.format(cache_key),
+                'content': '<img class="thumbnail" id="thumbnail_{key}" src="{src}" title="{title}" />'.format(
+                    key=cache_key,
+                    src='{0}?_={1}'.format(absolute_url(thumbnail, request), dc.modified.timestamp()),
+                    title=II18n(image).query_attribute('title', request=request)
+                )
+            }
+        }],
+        'close_form': False
+    }
+
+
+#
 # Image crop
 #
 
@@ -134,6 +187,11 @@
     def title(self):
         return self.context.title or self.context.filename
 
+    @property
+    def timestamp(self):
+        dc = IZopeDublinCore(self.context)
+        return dc.modified.timestamp()
+
     def updateActions(self):
         super(ImageCropForm, self).updateActions()
         if 'crop' in self.actions:
@@ -232,6 +290,11 @@
 class ImageSelectionThumbnailViewletsPrefix(Viewlet):
     """Image square thumbnail viewlets prefix"""
 
+    @property
+    def timestamp(self):
+        dc = IZopeDublinCore(self.context)
+        return dc.modified.timestamp()
+
 
 #
 # Image portrait thumbnail selection
@@ -560,10 +623,6 @@
 class ImageThumbnailsViewletsPrefix(Viewlet):
     """Image thumbnails viewlets prefix"""
 
-    @property
-    def random(self):
-        return random.randint(0, sys.maxsize)
-
     def get_thumbnails(self):
         registry = self.request.registry
         translate = self.request.localizer.translate
--- a/src/pyams_file/zmi/templates/image-crop.pt	Sat Jun 23 13:02:47 2018 +0200
+++ b/src/pyams_file/zmi/templates/image-crop.pt	Wed Jun 27 12:19:42 2018 +0200
@@ -9,7 +9,9 @@
  	<img class="imgareaselect"
 		 data-ams-imgareaselect-parent=".modal-dialog"
 		 data-ams-imgareaselect-target-field="selection."
-		 tal:attributes="src extension:absolute_url(image);
+		 tal:define="url extension:absolute_url(image);
+					 timestamp extension:timestamp(image);"
+		 tal:attributes="src string:${url}?_=${timestamp};
 						 width thumb_size[0];
 						 height thumb_size[1];
 						 data-ams-imgareaselect-image-width size[0];
--- a/src/pyams_file/zmi/templates/image-selection.pt	Sat Jun 23 13:02:47 2018 +0200
+++ b/src/pyams_file/zmi/templates/image-selection.pt	Wed Jun 27 12:19:42 2018 +0200
@@ -10,7 +10,9 @@
  	<img class="imgareaselect"
 		 data-ams-imgareaselect-parent=".modal-dialog"
 		 data-ams-imgareaselect-target-field="selection."
-		 tal:attributes="src extension:absolute_url(image);
+		 tal:define="url extension:absolute_url(image);
+					 timestamp extension:timestamp(image);"
+		 tal:attributes="src string:${url}?_=${timestamp};
 						 width thumb_size[0];
 						 height thumb_size[1];
 						 data-ams-imgareaselect-ratio view.__parent__.selection_ratio;
--- a/src/pyams_file/zmi/templates/image-thumbnails.pt	Sat Jun 23 13:02:47 2018 +0200
+++ b/src/pyams_file/zmi/templates/image-thumbnails.pt	Wed Jun 27 12:19:42 2018 +0200
@@ -11,7 +11,7 @@
 						<img tal:define="base extension:absolute_url(context);
 										 name adapter['name'];
 										 thname '{0}:'.format(name) if name else '';"
-							 tal:attributes="src string:${base}/++thumb++${thname}200x128.jpeg?_=${view.random}" />
+							 tal:attributes="src string:${base}/++thumb++${thname}200x128.jpeg?_=${extension:timestamp(context)}" />
 					</div>
 				</div>
 			</tal:loop>