Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Implement ``tag`` method in the scaling view directly to make use of …

…image metadata. This makes the call relatively cheap compared to going through the image scale object (which for legacy reasons loads the image data into memory). The documentation has been updated to reflect this change (i.e. it's now recommended to use the ``tag`` method directly off the scaling view).
  • Loading branch information...
commit 06b44d5be0aac4c9412d3ab7db118f2447db1c79 1 parent 45f8ea5
@malthe malthe authored
View
65 README.txt
@@ -43,7 +43,41 @@ New-style image scales
templates. There are several variants you can pick from depending on how
much flexibility/convenience you need:
-1. for full control you may do the tag generation explicitly::
+1. The ``tag`` method generates a complete image tag::
+
+ <img tal:define="scales context/@@images"
+ tal:replace="structure python: scales.tag('image',
+ width=1200, height=800, direction='down')"
+ />
+
+ While the first call requires the storage to load the image data
+ and extract information for scaling, consecutive calls are cheap
+ because the metadata is stored for each call signature.
+
+ The ``direction`` keyword-argument can be used to specify the
+ scaling direction. Additional parameters are rendered as element
+ attributes (typically: "title" and "alt").
+
+2. For tag generation using predefined scale names this would look like::
+
+ <img tal:define="scales context/@@images"
+ tal:replace="structure python: scales.tag('image', scale='mini')"
+ />
+
+ This would use the predefined scale size "mini" to determine the desired
+ image dimensions, but still allow to pass in extra parameters.
+
+3. The following traversal syntax is a short-cut for tag generation
+ for predefined image scales::
+
+ <img tal:replace="structure context/@@images/image/mini" />
+
+4. The same syntax may be used for the original image::
+
+ <img tal:replace="structure context/@@images/image" />
+
+5. The ``scale`` method returns an image scale object useful for
+ explicit tag generation::
<img tal:define="scales context/@@images;
thumbnail python: scales.scale('image', width=64, height=64);"
@@ -57,27 +91,16 @@ much flexibility/convenience you need:
parameters support by `plone.scale`_'s ``scaleImage`` function, e.g.
``direction`` or ``quality``.
- .. _`plone.scale`: http://pypi.python.org/pypi/plone.scale
-
-2. for automatic tag generation with extra parameters you would use::
-
- <img tal:define="scale context/@@images"
- tal:replace="structure python: scale.scale('image',
- width=1200, height=800, direction='down').tag()" />
+ Note that the ``scale`` method loads the actual image data into
+ memory on each invocation.
-3. for tag generation using predefined scale names this would look like::
-
- <img tal:define="scale context/@@images"
- tal:replace="structure python: scale.scale('image',
- scale='mini').tag()" />
-
- This would use the predefined scale size "mini" to determine the desired
- image dimensions, but still allow to pass in extra parameters.
-
-4. a convenience short-cut for option 3 can be used::
+ .. _`plone.scale`: http://pypi.python.org/pypi/plone.scale
- <img tal:replace="structure context/@@images/image/mini" />
+6. The image scale object also implements a ``tag`` method::
-5. and lastly, the short-cut can also be used to render the unscaled image::
+ <img tal:define="scales context/@@images;
+ scale python: scale.scale('image', width=1200, height=800)"
+ tal:replace="structure scale/tag" />
- <img tal:replace="structure context/@@images/image" />
+ However, it's recommended to use the ``tag`` method of the image
+ scales view directly because it avoids loading the image into memory.
View
4 docs/HISTORY.txt
@@ -4,6 +4,10 @@ Changelog
1.0.6 - unreleased
------------------
+* Avoid loading an image scale object in order to generate a tag. It's
+ expensive because it loads the image data into memory. The
+ documentation has been updated to reflect that this is the most
+ efficient usage of the API.
1.0.5 - 2011-04-03
------------------
View
3  src/plone/app/imaging/interfaces.py
@@ -59,6 +59,9 @@ def getAvailableSizes(fieldname=None):
def getImageSize(fieldname=None):
""" returns the original image size, a tuple of (width, height) """
+ def getInfo(fieldname=None, scalename=None, **parameters):
+ """ returns metadata for the requested scale from the storage """
+
class IImageScaleHandler(Interface):
""" handler for retrieving scaled versions of an image """
View
2  src/plone/app/imaging/scale.py
@@ -13,6 +13,8 @@ def __init__(self, id, data, content_type, **kw):
self.__name__ = id
self.__dict__.update(kw)
self.precondition = ''
+
+
# `OFS.Image` has no proper support for file objects or iterators,
# so we'll require `data` to be a string or a file-like object...
if not isinstance(data, str):
View
83 src/plone/app/imaging/scaling.py
@@ -1,3 +1,4 @@
+from cgi import escape
from logging import getLogger
from logging import exception
from Acquisition import aq_base
@@ -110,24 +111,86 @@ def modified(self):
items, so stored image scales can be invalidated """
return self.context.modified().millis()
- def scale(self, fieldname=None, scale=None, height=None, width=None, **parameters):
+ def scale(self, fieldname=None, scale=None, height=None, width=None,
+ **parameters):
if scale is not None:
available = self.getAvailableSizes(fieldname)
if not scale in available:
return None
width, height = available[scale]
+
if width is None and height is None:
field = self.field(fieldname)
return field.get(self.context)
- storage = AnnotationStorage(self.context, self.modified)
- info = storage.scale(factory=self.create,
- fieldname=fieldname, height=height, width=width, **parameters)
+
+ info = self.getInfo(
+ fieldname=fieldname, scale=scale, height=height, width=width,
+ **parameters
+ )
+
if info is not None:
return self.make(info).__of__(self.context)
- def tag(self, fieldname=None, scale=None, **kwargs):
- scale = self.scale(fieldname, scale)
- return scale.tag(**kwargs)
+ def tag(self, fieldname=None, scale=None, height=None, width=None,
+ css_class=None, direction='keep', **args):
+ """
+ Generate an HTML IMG tag for this image, with customization.
+ Arguments to self.tag() can be any valid attributes of an IMG
+ tag. 'src' will always be an absolute pathname, to prevent
+ redundant downloading of images. Defaults are applied
+ intelligently for 'height' and 'width'. If specified, the
+ 'scale' argument will be used to automatically adjust the
+ output height and width values of the image tag.
+
+ Since 'class' is a Python reserved word, it cannot be passed in
+ directly in keyword arguments which is a problem if you are
+ trying to use 'tag()' to include a CSS class. The tag() method
+ will accept a 'css_class' argument that will be converted to
+ 'class' in the output tag to work around this.
+ """
+
+ if scale is not None:
+ available = self.getAvailableSizes(fieldname)
+ if not scale in available:
+ return None
+ width, height = available[scale]
+
+ if width is None and height is None:
+ field = self.field(fieldname)
+ return field.tag(
+ self.context, css_class=css_class, **args
+ )
+
+ info = self.getInfo(
+ fieldname=fieldname, scale=scale,
+ height=height, width=width,
+ direction=direction,
+ )
+
+ width = info['width']
+ height = info['height']
+ mimetype = info['mimetype']
+ extension = mimetype.split('/')[-1]
+
+ url = self.context.absolute_url()
+ src = '%s/@@images/%s.%s' % (url, info['uid'], extension)
+ result = '<img src="%s"' % src
+
+ if height:
+ result = '%s height="%s"' % (result, height)
+
+ if width:
+ result = '%s width="%s"' % (result, width)
+
+ if css_class is not None:
+ result = '%s class="%s"' % (result, css_class)
+
+ if args:
+ for key, value in sorted(args.items()):
+ if value:
+ result = '%s %s="%s"' % (result, key, value)
+
+ return '%s />' % result
def getAvailableSizes(self, fieldname=None):
field = self.field(fieldname)
@@ -136,3 +199,9 @@ def getAvailableSizes(self, fieldname=None):
def getImageSize(self, fieldname=None):
field = self.field(fieldname)
return field.getSize(self.context)
+
+ def getInfo(self, fieldname=None, scale=None, height=None, width=None,
+ **parameters):
+ storage = AnnotationStorage(self.context, self.modified)
+ return storage.scale(factory=self.create,
+ fieldname=fieldname, height=height, width=width, **parameters)
View
17 src/plone/app/imaging/tests/test_new_scaling.py
@@ -79,6 +79,23 @@ def testViewTagMethod(self):
view_tag = view.tag()
self.assertEqual(image.tag(), view.tag())
+ def testViewTagMethodCustomScale(self):
+ data = self.getImage()
+ folder = self.folder
+ image = folder['foo']
+ traverse = folder.REQUEST.traverseName
+ view = traverse(image, '@@images')
+ view_tag = view.tag(width=23, height=23, alt="foo", title="foo")
+ base = self.image.absolute_url()
+ expected = r'<img src="%s/@@images/([-0-9a-f]{36}).(jpeg|gif|png)" ' \
+ r'height="(\d+)" width="(\d+)" alt="foo" title="foo" />' % base
+ name, ext, height, width = match(expected, view_tag).groups()
+ self.assertEqual(height, '23')
+ self.assertEqual(width, '23')
+ scale = view.publishTraverse(self.image.REQUEST, name + "." + ext)
+ self.assertEqual(scale.height, 23)
+ self.assertEqual(scale.width, 23)
+
class ImagePublisherTests(ImagingFunctionalTestCase):

6 comments on commit 06b44d5

@witsch
Owner

this looks fine, except for the white space (changes)! :) i know i'm being picky here, but i'd rather keep white space consistent throughout the package than to adjust it to personal taste… what do you think?

@malthe
Collaborator

I'm not sure I understand the whitespace issue? Do you mean "scale.py" which seems to have been included in the commit by mistake?

@witsch
Owner

no, i didn't mean scale.py. i'm just not very fond of inserting arbitrary blank lines here and there, but that's just me being picky about details and not really a problem, of course… :)

@malthe
Collaborator

Oh I see.

I agree, in principle, but it isn't personal taste in this particular case: It's simply PEP8. That said, it is definitely not always clear when a source code should be updated just because it doesn't comply with PEP8.

@witsch
Owner

hmm, again, without being overly picky or wanted to start a big discussion: afaik the extra lines in scale.py (16–17) and scaling.py (121) are not required for pep8. apart from that i agree, but have started to try to leave things (which don't "comply" with my personal taste) mostly as they are, i.e. keep the changes at a minimum. at times i even mimic the existing style re white space, parentheses etc as i find consistency should win over personal taste…

@malthe
Collaborator

+1 for keeping consistency. I agree.

That said, this change was pushed directly to master, so I don't think we should do much here (can't), but had it been a merge proposal, then it would definitely have been good to weed out that scale.py change.

Please sign in to comment.
Something went wrong with that request. Please try again.