--- 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>
+ –
+ <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>