Added support to store SVG files as images
authorThierry Florac <thierry.florac@onf.fr>
Tue, 19 Jun 2018 17:09:37 +0200
changeset 104 49e132e15bbc
parent 103 ee6515f0d9ae
child 105 2cbaa43fddec
Added support to store SVG files as images
src/pyams_file/file.py
src/pyams_file/interfaces/__init__.py
src/pyams_file/schema.py
src/pyams_file/thumbnail.py
src/pyams_file/widget/templates/media-input.pt
src/pyams_file/zmi/image.py
src/pyams_file/zmi/templates/svg-render.pt
--- a/src/pyams_file/file.py	Tue Jun 19 17:06:49 2018 +0200
+++ b/src/pyams_file/file.py	Tue Jun 19 17:09:37 2018 +0200
@@ -25,7 +25,7 @@
 from PIL import Image
 
 # import interfaces
-from pyams_file.interfaces import IFile, IImage, IVideo, IAudio, IFileInfo, FileModifiedEvent
+from pyams_file.interfaces import IFile, IImage, ISVGImage, IVideo, IAudio, IFileInfo, FileModifiedEvent
 from zope.copy.interfaces import ICopyHook, ResumeCopy
 from zope.location.interfaces import IContained
 
@@ -255,6 +255,11 @@
         request.registry.notify(FileModifiedEvent(self))
 
 
+@implementer(ISVGImage)
+class SVGImageFile(File):
+    """SVG image file persistent object"""
+
+
 @implementer(IVideo)
 class VideoFile(File):
     """Video file persistent object"""
@@ -292,7 +297,9 @@
     content-type recognition
     """
     content_type = get_magic_content_type(data)
-    if content_type.startswith('image/'):
+    if content_type == 'image/svg':
+        factory = SVGImageFile
+    elif content_type.startswith('image/'):
         factory = ImageFile
     elif content_type.startswith('video/'):
         factory = VideoFile
--- a/src/pyams_file/interfaces/__init__.py	Tue Jun 19 17:06:49 2018 +0200
+++ b/src/pyams_file/interfaces/__init__.py	Tue Jun 19 17:09:37 2018 +0200
@@ -68,7 +68,11 @@
     """Multimedia file"""
 
 
-class IImage(IMediaFile):
+class IBaseImage(IMediaFile):
+    """Base image interface"""
+
+
+class IImage(IBaseImage):
     """Image object interface"""
 
     def get_image_size(self):
@@ -81,12 +85,16 @@
         """Crop image to given coordinates"""
 
 
+class ISVGImage(IBaseImage):
+    """SVG file interface"""
+
+
 class IResponsiveImage(Interface):
     """Responsive image marker interface"""
 
 
 class IVideo(IMediaFile):
-    """Video object interface"""
+    """Video file interface"""
 
 
 class IAudio(IMediaFile):
--- a/src/pyams_file/schema.py	Tue Jun 19 17:06:49 2018 +0200
+++ b/src/pyams_file/schema.py	Tue Jun 19 17:09:37 2018 +0200
@@ -15,8 +15,8 @@
 # import standard library
 
 # import interfaces
-from pyams_file.interfaces import IFile, IFileField, IMediaFile, IMediaField, IImage, IImageField, IVideo, IVideoField, \
-    IAudio, IAudioField, IThumbnailMediaField, IThumbnailImageField, IThumbnailVideoField, DELETED_FILE
+from pyams_file.interfaces import IFile, IFileField, IMediaFile, IMediaField, IBaseImage, IImageField, IVideo, \
+    IVideoField, IAudio, IAudioField, IThumbnailMediaField, IThumbnailImageField, IThumbnailVideoField, DELETED_FILE
 from z3c.form.interfaces import NOT_CHANGED
 from zope.schema.interfaces import WrongType, RequiredMissing
 
@@ -66,7 +66,7 @@
 class ImageField(MediaField):
     """Custom field used to handle image properties"""
 
-    schema = IImage
+    schema = IBaseImage
 
 
 @implementer(IThumbnailImageField)
--- a/src/pyams_file/thumbnail.py	Tue Jun 19 17:06:49 2018 +0200
+++ b/src/pyams_file/thumbnail.py	Tue Jun 19 17:09:37 2018 +0200
@@ -25,6 +25,7 @@
 # import packages
 from persistent.dict import PersistentDict
 from pyams_file.file import FileFactory
+from pyams_file.zmi.image import render_image
 from pyams_utils.adapter import ContextAdapter, ContextRequestViewAdapter, adapter_config, get_annotation_adapter
 from pyams_utils.registry import query_utility
 from pyams_utils.request import check_request
@@ -243,9 +244,27 @@
 
 @adapter_config(name='thumbnails', context=(Interface, Interface, Interface), provides=ITALESExtension)
 class ThumbnailsExtension(ContextRequestViewAdapter):
-    """extension:thumbnails(image) TALES extension"""
+    """extension:thumbnails(image) TALES extension
+
+    This TALES extension returns the IThumbnails adapter of given image.
+    """
 
     def render(self, context=None):
         if context is None:
             context = self.context
         return IThumbnail(context, None)
+
+
+@adapter_config(name='thumbnail', context=(Interface, Interface, Interface), provides=ITALESExtension)
+class ThumbnailExtension(ContextRequestViewAdapter):
+    """extension:thumbnail(image, width, height) TALES extension
+
+    This TALES extension doesn't return an adapter but HTML code matching given image and dimensions.
+    If image is a classic image, an "img" tag with source to thumbnail of required size is returned.
+    If image in an SVG image, a "div" is returned containing whole SVG data of given image.
+    """
+
+    def render(self, context=None, width=None, height=None, css_class=''):
+        if context is None:
+            context = self.context
+        return render_image(context, width, height, self.request, css_class)
--- a/src/pyams_file/widget/templates/media-input.pt	Tue Jun 19 17:06:49 2018 +0200
+++ b/src/pyams_file/widget/templates/media-input.pt	Tue Jun 19 17:09:37 2018 +0200
@@ -19,7 +19,32 @@
 				<span i18n:translate="">Delete content</span>
 			</label>
 		</div>
-		<tal:if condition="python:value.content_type.startswith('image/')">
+		<tal:if condition="python:value.content_type == 'image/svg'">
+			<div tal:content="structure extension:thumbnail(value, 128, 'auto', 'pull-left margin-5 margin-right-10')">
+				Thumbnail
+			</div>
+			<div class="margin-top-5">
+				<span i18n:translate="">Current value: </span>
+				<span tal:content="value/content_type"></span>
+				&nbsp;&ndash;&nbsp;
+				<span tal:content="python:view.get_human_size(value.get_size())"></span>
+				<br />
+				<div class="btn-group dropup"
+					 tal:define="actions extension:context_actions(value)"
+					 tal:omit-tag="not:actions">
+					<tal:loop repeat="viewlet actions/viewlets"
+							  content="structure viewlet/render">
+					</tal:loop>
+					<a class="btn btn-xs btn-primary" target="download_window"
+					   tal:define="href extension:absolute_url(value)"
+					   tal:attributes="href string:${href}?download=1&_=${view/timestamp}"
+					   i18n:translate="">
+						Download
+					</a>
+				</div>
+			</div>
+		</tal:if>
+		<tal:if condition="python:value.content_type.startswith('image/') and (value.content_type != 'image/svg')">
 			<a class="fancybox hint pull-left margin-5 margin-right-10" data-toggle
 			   data-ams-fancybox-type="image"
 			   data-ams-hint-gravity="e"
--- a/src/pyams_file/zmi/image.py	Tue Jun 19 17:06:49 2018 +0200
+++ b/src/pyams_file/zmi/image.py	Tue Jun 19 17:09:37 2018 +0200
@@ -17,11 +17,12 @@
 import random
 import sys
 import transaction
+
 from collections import OrderedDict
 
 # import interfaces
 from pyams_file.interfaces import IImage, IThumbnail, IResponsiveImage, IFileModifierForm, IThumbnailer, \
-    IFileInfo, IThumbnailForm, IMediaWidget
+    IFileInfo, IThumbnailForm, IMediaWidget, ISVGImage
 from pyams_form.interfaces.form import IWidgetsPrefixViewletsManager, IFormHelp
 from pyams_skin.interfaces.viewlet import IContextActions
 from pyams_skin.layer import IPyAMSLayer
@@ -41,6 +42,7 @@
 from pyams_utils.url import absolute_url
 from pyams_viewlet.viewlet import viewlet_config, Viewlet
 from pyams_zmi.form import AdminDialogEditForm
+from pyramid.renderers import render
 from z3c.form import field, button
 from zope.interface import implementer, Interface
 from zope.schema import Int, Bool
@@ -50,6 +52,46 @@
 
 
 #
+# SVG utilities
+#
+
+def render_svg(image, width=None, height=None, request=None, css_class=''):
+    """Render SVG file"""
+    options = {'svg': image}
+    if width or height:
+        options['style'] = 'width: {0}{1}; height: {2}{3};'.format(width, 'px' if isinstance(width, int) else '',
+                                                                   height, 'px' if isinstance(height, int) else '')
+    options['css_class'] = css_class
+    return render('templates/svg-render.pt', options, request)
+
+
+def render_img(image, width=None, height=None, request=None, css_class='', timestamp=False):
+    """Render image thumbnail"""
+    thumbnails = IThumbnail(image, None)
+    if thumbnails is not None:
+        if width or height:
+            thumbnail = thumbnails.get_thumbnail('{0}x{1}'.format(width, height))
+    if thumbnail is None:
+        thumbnail = image
+    url = absolute_url(thumbnail, request)
+    if timestamp:
+        timestamp = random.randint(0, sys.maxsize)
+        url += '?_={1}'.format(timestamp)
+    result = '<img src="{0}" />'.format(url)
+    if css_class:
+        result = '<div class="{0}">{1}</div>'.format(css_class, result)
+    return result
+
+
+def render_image(image, width=None, height=None, request=None, css_class='', timestamp=False):
+    """Render image"""
+    if ISVGImage.providedBy(image):
+        return render_svg(image, width, height, request, css_class)
+    else:
+        return render_img(image, width, height, request, css_class, timestamp)
+
+
+#
 # Image crop
 #
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_file/zmi/templates/svg-render.pt	Tue Jun 19 17:09:37 2018 +0200
@@ -0,0 +1,4 @@
+<div class="display-inline align-middle svg-container"
+	 tal:attributes="class css_class + ' ' + default; style style;">
+	<svg tal:replace="structure svg.data" />
+</div>