+# -*- coding: utf-8 -*-
+##############################################################################
+# This module is based on OFS.Image originaly copyrighted as:
+#
+# Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+"""Image object
+
+$Id: blobbases.py 949 2009-04-30 14:42:24Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/blobbases.py $
+"""
+
+import struct
+from warnings import warn
+from zope.contenttype import guess_content_type
+from Globals import DTMLFile
+from Globals import InitializeClass
+from OFS.PropertyManager import PropertyManager
+from AccessControl import ClassSecurityInfo
+from AccessControl.Role import RoleManager
+from AccessControl.Permissions import change_images_and_files
+from AccessControl.Permissions import view_management_screens
+from AccessControl.Permissions import view as View
+from AccessControl.Permissions import ftp_access
+from AccessControl.Permissions import delete_objects
+from webdav.common import rfc1123_date
+from webdav.Lockable import ResourceLockedError
+from webdav.WriteLockInterface import WriteLockInterface
+from OFS.SimpleItem import Item_w__name__
+from cStringIO import StringIO
+from Globals import Persistent
+from Acquisition import Implicit
+from DateTime import DateTime
+from OFS.Cache import Cacheable
+from mimetools import choose_boundary
+from ZPublisher import HTTPRangeSupport
+from ZPublisher.HTTPRequest import FileUpload
+from ZPublisher.Iterators import filestream_iterator
+from zExceptions import Redirect
+from cgi import escape
+import transaction
+from ZODB.blob import Blob
+
+CHUNK_SIZE = 1 << 16
+
+manage_addFileForm=DTMLFile('dtml/imageAdd', globals(),Kind='File',kind='file')
+def manage_addFile(self,id,file='',title='',precondition='', content_type='',
+ REQUEST=None):
+ """Add a new File object.
+
+ Creates a new File object 'id' with the contents of 'file'"""
+
+ id=str(id)
+ title=str(title)
+ content_type=str(content_type)
+ precondition=str(precondition)
+
+ id, title = cookId(id, title, file)
+
+ self=self.this()
+ self._setObject(id, File(id,title,file,content_type, precondition))
+
+ if REQUEST is not None:
+ REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')
+
+
+class File(Persistent, Implicit, PropertyManager,
+ RoleManager, Item_w__name__, Cacheable):
+ """A File object is a content object for arbitrary files."""
+
+ __implements__ = (WriteLockInterface, HTTPRangeSupport.HTTPRangeInterface)
+ meta_type='Blob File'
+
+ security = ClassSecurityInfo()
+ security.declareObjectProtected(View)
+
+ precondition=''
+ size=None
+
+ manage_editForm =DTMLFile('dtml/fileEdit',globals(),
+ Kind='File',kind='file')
+ manage_editForm._setName('manage_editForm')
+
+ security.declareProtected(view_management_screens, 'manage')
+ security.declareProtected(view_management_screens, 'manage_main')
+ manage=manage_main=manage_editForm
+ manage_uploadForm=manage_editForm
+
+ manage_options=(
+ (
+ {'label':'Edit', 'action':'manage_main',
+ 'help':('OFSP','File_Edit.stx')},
+ {'label':'View', 'action':'',
+ 'help':('OFSP','File_View.stx')},
+ )
+ + PropertyManager.manage_options
+ + RoleManager.manage_options
+ + Item_w__name__.manage_options
+ + Cacheable.manage_options
+ )
+
+ _properties=({'id':'title', 'type': 'string'},
+ {'id':'content_type', 'type':'string'},
+ )
+
+ def __init__(self, id, title, file, content_type='', precondition=''):
+ self.__name__=id
+ self.title=title
+ self.precondition=precondition
+ self.uploaded_filename = cookId('', '', file)[0]
+ self.bdata = Blob()
+
+ content_type=self._get_content_type(file, id, content_type)
+ self.update_data(file, content_type)
+
+ security.declarePrivate('save')
+ def save(self, file):
+ bf = self.bdata.open('w')
+ bf.write(file.read())
+ self.size = bf.tell()
+ bf.close()
+
+ security.declarePrivate('open')
+ def open(self, mode='r'):
+ bf = self.bdata.open(mode)
+ return bf
+
+ security.declarePrivate('updateSize')
+ def updateSize(self, size=None):
+ if size is None :
+ bf = self.open('r')
+ bf.seek(0,2)
+ self.size = bf.tell()
+ bf.close()
+ else :
+ self.size = size
+
+ def _getLegacyData(self) :
+ warn("Accessing 'data' attribute may be inefficient with "
+ "this blob based file. You should refactor your product "
+ "by accessing data like: "
+ "f = self.open('r') "
+ "data = f.read()",
+ DeprecationWarning, stacklevel=2)
+ f = self.open()
+ data = f.read()
+ f.close()
+ return data
+
+ def _setLegacyData(self, data) :
+ warn("Accessing 'data' attribute may be inefficient with "
+ "this blob based file. You should refactor your product "
+ "by accessing data like: "
+ "f = self.save(data)",
+ DeprecationWarning, stacklevel=2)
+ if isinstance(data, str) :
+ sio = StringIO()
+ sio.write(data)
+ sio.seek(0)
+ data = sio
+ self.save(data)
+
+ data = property(_getLegacyData, _setLegacyData,
+ "Data Legacy attribute to ensure compatibility "
+ "with derived classes that access data by this way.")
+
+ def id(self):
+ return self.__name__
+
+ def _if_modified_since_request_handler(self, REQUEST, RESPONSE):
+ # HTTP If-Modified-Since header handling: return True if
+ # we can handle this request by returning a 304 response
+ header=REQUEST.get_header('If-Modified-Since', None)
+ if header is not None:
+ header=header.split( ';')[0]
+ # Some proxies seem to send invalid date strings for this
+ # header. If the date string is not valid, we ignore it
+ # rather than raise an error to be generally consistent
+ # with common servers such as Apache (which can usually
+ # understand the screwy date string as a lucky side effect
+ # of the way they parse it).
+ # This happens to be what RFC2616 tells us to do in the face of an
+ # invalid date.
+ try: mod_since=long(DateTime(header).timeTime())
+ except: mod_since=None
+ if mod_since is not None:
+ if self._p_mtime:
+ last_mod = long(self._p_mtime)
+ else:
+ last_mod = long(0)
+ if last_mod > 0 and last_mod <= mod_since:
+ RESPONSE.setHeader('Last-Modified',
+ rfc1123_date(self._p_mtime))
+ RESPONSE.setHeader('Content-Type', self.content_type)
+ RESPONSE.setHeader('Accept-Ranges', 'bytes')
+ RESPONSE.setStatus(304)
+ return True
+
+ def _range_request_handler(self, REQUEST, RESPONSE):
+ # HTTP Range header handling: return True if we've served a range
+ # chunk out of our data.
+ range = REQUEST.get_header('Range', None)
+ request_range = REQUEST.get_header('Request-Range', None)
+ if request_range is not None:
+ # Netscape 2 through 4 and MSIE 3 implement a draft version
+ # Later on, we need to serve a different mime-type as well.
+ range = request_range
+ if_range = REQUEST.get_header('If-Range', None)
+ if range is not None:
+ ranges = HTTPRangeSupport.parseRange(range)
+
+ if if_range is not None:
+ # Only send ranges if the data isn't modified, otherwise send
+ # the whole object. Support both ETags and Last-Modified dates!
+ if len(if_range) > 1 and if_range[:2] == 'ts':
+ # ETag:
+ if if_range != self.http__etag():
+ # Modified, so send a normal response. We delete
+ # the ranges, which causes us to skip to the 200
+ # response.
+ ranges = None
+ else:
+ # Date
+ date = if_range.split( ';')[0]
+ try: mod_since=long(DateTime(date).timeTime())
+ except: mod_since=None
+ if mod_since is not None:
+ if self._p_mtime:
+ last_mod = long(self._p_mtime)
+ else:
+ last_mod = long(0)
+ if last_mod > mod_since:
+ # Modified, so send a normal response. We delete
+ # the ranges, which causes us to skip to the 200
+ # response.
+ ranges = None
+
+ if ranges:
+ # Search for satisfiable ranges.
+ satisfiable = 0
+ for start, end in ranges:
+ if start < self.size:
+ satisfiable = 1
+ break
+
+ if not satisfiable:
+ RESPONSE.setHeader('Content-Range',
+ 'bytes */%d' % self.size)
+ RESPONSE.setHeader('Accept-Ranges', 'bytes')
+ RESPONSE.setHeader('Last-Modified',
+ rfc1123_date(self._p_mtime))
+ RESPONSE.setHeader('Content-Type', self.content_type)
+ RESPONSE.setHeader('Content-Length', self.size)
+ RESPONSE.setStatus(416)
+ return True
+
+ ranges = HTTPRangeSupport.expandRanges(ranges, self.size)
+
+ if len(ranges) == 1:
+ # Easy case, set extra header and return partial set.
+ start, end = ranges[0]
+ size = end - start
+
+ RESPONSE.setHeader('Last-Modified',
+ rfc1123_date(self._p_mtime))
+ RESPONSE.setHeader('Content-Type', self.content_type)
+ RESPONSE.setHeader('Content-Length', size)
+ RESPONSE.setHeader('Accept-Ranges', 'bytes')
+ RESPONSE.setHeader('Content-Range',
+ 'bytes %d-%d/%d' % (start, end - 1, self.size))
+ RESPONSE.setStatus(206) # Partial content
+
+ bf = self.open('r')
+ bf.seek(start)
+ RESPONSE.write(bf.read(size))
+ bf.close()
+ return True
+
+ else:
+ boundary = choose_boundary()
+
+ # Calculate the content length
+ size = (8 + len(boundary) + # End marker length
+ len(ranges) * ( # Constant lenght per set
+ 49 + len(boundary) + len(self.content_type) +
+ len('%d' % self.size)))
+ for start, end in ranges:
+ # Variable length per set
+ size = (size + len('%d%d' % (start, end - 1)) +
+ end - start)
+
+
+ # Some clients implement an earlier draft of the spec, they
+ # will only accept x-byteranges.
+ draftprefix = (request_range is not None) and 'x-' or ''
+
+ RESPONSE.setHeader('Content-Length', size)
+ RESPONSE.setHeader('Accept-Ranges', 'bytes')
+ RESPONSE.setHeader('Last-Modified',
+ rfc1123_date(self._p_mtime))
+ RESPONSE.setHeader('Content-Type',
+ 'multipart/%sbyteranges; boundary=%s' % (
+ draftprefix, boundary))
+ RESPONSE.setStatus(206) # Partial content
+
+ bf = self.open('r')
+
+ for start, end in ranges:
+ RESPONSE.write('\r\n--%s\r\n' % boundary)
+ RESPONSE.write('Content-Type: %s\r\n' %
+ self.content_type)
+ RESPONSE.write(
+ 'Content-Range: bytes %d-%d/%d\r\n\r\n' % (
+ start, end - 1, self.size))
+
+
+ size = end - start
+ bf.seek(start)
+ RESPONSE.write(bf.read(size))
+
+ bf.close()
+
+ RESPONSE.write('\r\n--%s--\r\n' % boundary)
+ return True
+
+ security.declareProtected(View, 'index_html')
+ def index_html(self, REQUEST, RESPONSE):
+ """
+ The default view of the contents of a File or Image.
+
+ Returns the contents of the file or image. Also, sets the
+ Content-Type HTTP header to the objects content type.
+ """
+
+ if self._if_modified_since_request_handler(REQUEST, RESPONSE):
+ # we were able to handle this by returning a 304
+ # unfortunately, because the HTTP cache manager uses the cache
+ # API, and because 304 responses are required to carry the Expires
+ # header for HTTP/1.1, we need to call ZCacheable_set here.
+ # This is nonsensical for caches other than the HTTP cache manager
+ # unfortunately.
+ self.ZCacheable_set(None)
+ return ''
+
+ if self.precondition and hasattr(self, str(self.precondition)):
+ # Grab whatever precondition was defined and then
+ # execute it. The precondition will raise an exception
+ # if something violates its terms.
+ c=getattr(self, str(self.precondition))
+ if hasattr(c,'isDocTemp') and c.isDocTemp:
+ c(REQUEST['PARENTS'][1],REQUEST)
+ else:
+ c()
+
+ if self._range_request_handler(REQUEST, RESPONSE):
+ # we served a chunk of content in response to a range request.
+ return ''
+
+ RESPONSE.setHeader('Last-Modified', rfc1123_date(self._p_mtime))
+ RESPONSE.setHeader('Content-Type', self.content_type)
+ RESPONSE.setHeader('Content-Length', self.size)
+ RESPONSE.setHeader('Accept-Ranges', 'bytes')
+
+ if self.ZCacheable_isCachingEnabled():
+ result = self.ZCacheable_get(default=None)
+ if result is not None:
+ # We will always get None from RAMCacheManager and HTTP
+ # Accelerated Cache Manager but we will get
+ # something implementing the IStreamIterator interface
+ # from a "FileCacheManager"
+ return result
+
+ self.ZCacheable_set(None)
+
+ bf = self.open('r')
+ chunk = bf.read(CHUNK_SIZE)
+ while chunk :
+ RESPONSE.write(chunk)
+ chunk = bf.read(CHUNK_SIZE)
+ bf.close()
+ return ''
+
+ security.declareProtected(View, 'view_image_or_file')
+ def view_image_or_file(self, URL1):
+ """
+ The default view of the contents of the File or Image.
+ """
+ raise Redirect, URL1
+
+ security.declareProtected(View, 'PrincipiaSearchSource')
+ def PrincipiaSearchSource(self):
+ """ Allow file objects to be searched.
+ """
+ if self.content_type.startswith('text/'):
+ bf = self.open('r')
+ data = bf.read()
+ bf.close()
+ return data
+ return ''
+
+ security.declarePrivate('update_data')
+ def update_data(self, file, content_type=None):
+ if isinstance(file, unicode):
+ raise TypeError('Data can only be str or file-like. '
+ 'Unicode objects are expressly forbidden.')
+ elif isinstance(file, str) :
+ sio = StringIO()
+ sio.write(file)
+ sio.seek(0)
+ file = sio
+
+ if content_type is not None: self.content_type=content_type
+ self.save(file)
+ self.ZCacheable_invalidate()
+ self.ZCacheable_set(None)
+ self.http__refreshEtag()
+
+ security.declareProtected(change_images_and_files, 'manage_edit')
+ def manage_edit(self, title, content_type, precondition='',
+ filedata=None, REQUEST=None):
+ """
+ Changes the title and content type attributes of the File or Image.
+ """
+ if self.wl_isLocked():
+ raise ResourceLockedError, "File is locked via WebDAV"
+
+ self.title=str(title)
+ self.content_type=str(content_type)
+ if precondition: self.precondition=str(precondition)
+ elif self.precondition: del self.precondition
+ if filedata is not None:
+ self.update_data(filedata, content_type)
+ else:
+ self.ZCacheable_invalidate()
+ if REQUEST:
+ message="Saved changes."
+ return self.manage_main(self,REQUEST,manage_tabs_message=message)
+
+ security.declareProtected(change_images_and_files, 'manage_upload')
+ def manage_upload(self,file='',REQUEST=None):
+ """
+ Replaces the current contents of the File or Image object with file.
+
+ The file or images contents are replaced with the contents of 'file'.
+ """
+ if self.wl_isLocked():
+ raise ResourceLockedError, "File is locked via WebDAV"
+
+ content_type=self._get_content_type(file, self.__name__,
+ 'application/octet-stream')
+ self.update_data(file, content_type)
+
+ if REQUEST:
+ message="Saved changes."
+ return self.manage_main(self,REQUEST,manage_tabs_message=message)
+
+ def _get_content_type(self, file, id, content_type=None):
+ headers=getattr(file, 'headers', None)
+ if headers and headers.has_key('content-type'):
+ content_type=headers['content-type']
+ else:
+ name = getattr(file, 'filename', self.uploaded_filename) or id
+ content_type, enc=guess_content_type(name, '', content_type)
+ return content_type
+
+ security.declareProtected(delete_objects, 'DELETE')
+
+ security.declareProtected(change_images_and_files, 'PUT')
+ def PUT(self, REQUEST, RESPONSE):
+ """Handle HTTP PUT requests"""
+ self.dav__init(REQUEST, RESPONSE)
+ self.dav__simpleifhandler(REQUEST, RESPONSE, refresh=1)
+ type=REQUEST.get_header('content-type', None)
+
+ file=REQUEST['BODYFILE']
+
+ content_type = self._get_content_type(file, self.__name__,
+ type or self.content_type)
+ self.update_data(file, content_type)
+
+ RESPONSE.setStatus(204)
+ return RESPONSE
+
+ security.declareProtected(View, 'get_size')
+ def get_size(self):
+ """Get the size of a file or image.
+
+ Returns the size of the file or image.
+ """
+ size=self.size
+ if size is None :
+ bf = self.open('r')
+ bf.seek(0,2)
+ self.size = size = bf.tell()
+ bf.close()
+ return size
+
+ # deprecated; use get_size!
+ getSize=get_size
+
+ security.declareProtected(View, 'getContentType')
+ def getContentType(self):
+ """Get the content type of a file or image.
+
+ Returns the content type (MIME type) of a file or image.
+ """
+ return self.content_type
+
+
+ def __str__(self): return str(self.data)
+ def __len__(self): return 1
+
+ security.declareProtected(ftp_access, 'manage_FTPstat')
+ security.declareProtected(ftp_access, 'manage_FTPlist')
+
+ security.declareProtected(ftp_access, 'manage_FTPget')
+ def manage_FTPget(self):
+ """Return body for ftp."""
+ RESPONSE = self.REQUEST.RESPONSE
+
+ if self.ZCacheable_isCachingEnabled():
+ result = self.ZCacheable_get(default=None)
+ if result is not None:
+ # We will always get None from RAMCacheManager but we will get
+ # something implementing the IStreamIterator interface
+ # from FileCacheManager.
+ # the content-length is required here by HTTPResponse, even
+ # though FTP doesn't use it.
+ RESPONSE.setHeader('Content-Length', self.size)
+ return result
+
+ bf = self.open('r')
+ data = bf.read()
+ bf.close()
+ RESPONSE.setBase(None)
+ return data
+
+manage_addImageForm=DTMLFile('dtml/imageAdd',globals(),
+ Kind='Image',kind='image')
+def manage_addImage(self, id, file, title='', precondition='', content_type='',
+ REQUEST=None):
+ """
+ Add a new Image object.
+
+ Creates a new Image object 'id' with the contents of 'file'.
+ """
+
+ id=str(id)
+ title=str(title)
+ content_type=str(content_type)
+ precondition=str(precondition)
+
+ id, title = cookId(id, title, file)
+
+ self=self.this()
+ self._setObject(id, Image(id,title,file,content_type, precondition))
+
+ if REQUEST is not None:
+ try: url=self.DestinationURL()
+ except: url=REQUEST['URL1']
+ REQUEST.RESPONSE.redirect('%s/manage_main' % url)
+ return id
+
+
+def getImageInfo(file):
+ height = -1
+ width = -1
+ content_type = ''
+
+ # handle GIFs
+ data = file.read(24)
+ size = len(data)
+ if (size >= 10) and data[:6] in ('GIF87a', 'GIF89a'):
+ # Check to see if content_type is correct
+ content_type = 'image/gif'
+ w, h = struct.unpack("<HH", data[6:10])
+ width = int(w)
+ height = int(h)
+
+ # See PNG v1.2 spec (http://www.cdrom.com/pub/png/spec/)
+ # Bytes 0-7 are below, 4-byte chunk length, then 'IHDR'
+ # and finally the 4-byte width, height
+ elif ((size >= 24) and (data[:8] == '\211PNG\r\n\032\n')
+ and (data[12:16] == 'IHDR')):
+ content_type = 'image/png'
+ w, h = struct.unpack(">LL", data[16:24])
+ width = int(w)
+ height = int(h)
+
+ # Maybe this is for an older PNG version.
+ elif (size >= 16) and (data[:8] == '\211PNG\r\n\032\n'):
+ # Check to see if we have the right content type
+ content_type = 'image/png'
+ w, h = struct.unpack(">LL", data[8:16])
+ width = int(w)
+ height = int(h)
+
+ # handle JPEGs
+ elif (size >= 2) and (data[:2] == '\377\330'):
+ content_type = 'image/jpeg'
+ jpeg = file
+ jpeg.seek(0)
+ jpeg.read(2)
+ b = jpeg.read(1)
+ try:
+ while (b and ord(b) != 0xDA):
+ while (ord(b) != 0xFF): b = jpeg.read(1)
+ while (ord(b) == 0xFF): b = jpeg.read(1)
+ if (ord(b) >= 0xC0 and ord(b) <= 0xC3):
+ jpeg.read(3)
+ h, w = struct.unpack(">HH", jpeg.read(4))
+ break
+ else:
+ jpeg.read(int(struct.unpack(">H", jpeg.read(2))[0])-2)
+ b = jpeg.read(1)
+ width = int(w)
+ height = int(h)
+ except: pass
+
+ return content_type, width, height
+
+
+class Image(File):
+ """Image objects can be GIF, PNG or JPEG and have the same methods
+ as File objects. Images also have a string representation that
+ renders an HTML 'IMG' tag.
+ """
+ __implements__ = (WriteLockInterface,)
+ meta_type='Blob Image'
+
+ security = ClassSecurityInfo()
+ security.declareObjectProtected(View)
+
+ alt=''
+ height=''
+ width=''
+
+ # FIXME: Redundant, already in base class
+ security.declareProtected(change_images_and_files, 'manage_edit')
+ security.declareProtected(change_images_and_files, 'manage_upload')
+ security.declareProtected(change_images_and_files, 'PUT')
+ security.declareProtected(View, 'index_html')
+ security.declareProtected(View, 'get_size')
+ security.declareProtected(View, 'getContentType')
+ security.declareProtected(ftp_access, 'manage_FTPstat')
+ security.declareProtected(ftp_access, 'manage_FTPlist')
+ security.declareProtected(ftp_access, 'manage_FTPget')
+ security.declareProtected(delete_objects, 'DELETE')
+
+ _properties=({'id':'title', 'type': 'string'},
+ {'id':'alt', 'type':'string'},
+ {'id':'content_type', 'type':'string','mode':'w'},
+ {'id':'height', 'type':'string'},
+ {'id':'width', 'type':'string'},
+ )
+
+ manage_options=(
+ ({'label':'Edit', 'action':'manage_main',
+ 'help':('OFSP','Image_Edit.stx')},
+ {'label':'View', 'action':'view_image_or_file',
+ 'help':('OFSP','Image_View.stx')},)
+ + PropertyManager.manage_options
+ + RoleManager.manage_options
+ + Item_w__name__.manage_options
+ + Cacheable.manage_options
+ )
+
+ manage_editForm =DTMLFile('dtml/imageEdit',globals(),
+ Kind='Image',kind='image')
+ manage_editForm._setName('manage_editForm')
+
+ security.declareProtected(View, 'view_image_or_file')
+ view_image_or_file =DTMLFile('dtml/imageView',globals())
+
+ security.declareProtected(view_management_screens, 'manage')
+ security.declareProtected(view_management_screens, 'manage_main')
+ manage=manage_main=manage_editForm
+ manage_uploadForm=manage_editForm
+
+ security.declarePrivate('update_data')
+ def update_data(self, file, content_type=None):
+ super(Image, self).update_data(file, content_type)
+ self.updateFormat(size=self.size, content_type=content_type)
+
+ security.declarePrivate('updateFormat')
+ def updateFormat(self, size=None, dimensions=None, content_type=None):
+ self.updateSize(size=size)
+
+ if dimensions is None or content_type is None :
+ bf = self.open('r')
+ ct, width, height = getImageInfo(bf)
+ bf.close()
+ if ct:
+ content_type = ct
+ if width >= 0 and height >= 0:
+ self.width = width
+ self.height = height
+
+ # Now we should have the correct content type, or still None
+ if content_type is not None: self.content_type = content_type
+ else :
+ self.width, self.height = dimensions
+ self.content_type = content_type
+
+ def __str__(self):
+ return self.tag()
+
+ security.declareProtected(View, 'tag')
+ def tag(self, height=None, width=None, alt=None,
+ scale=0, xscale=0, yscale=0, css_class=None, title=None, **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', 'width', and 'alt'. If specified, the 'scale', 'xscale',
+ and 'yscale' keyword arguments 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 height is None: height=self.height
+ if width is None: width=self.width
+
+ # Auto-scaling support
+ xdelta = xscale or scale
+ ydelta = yscale or scale
+
+ if xdelta and width:
+ width = str(int(round(int(width) * xdelta)))
+ if ydelta and height:
+ height = str(int(round(int(height) * ydelta)))
+
+ result='<img src="%s"' % (self.absolute_url())
+
+ if alt is None:
+ alt=getattr(self, 'alt', '')
+ result = '%s alt="%s"' % (result, escape(alt, 1))
+
+ if title is None:
+ title=getattr(self, 'title', '')
+ result = '%s title="%s"' % (result, escape(title, 1))
+
+ if height:
+ result = '%s height="%s"' % (result, height)
+
+ if width:
+ result = '%s width="%s"' % (result, width)
+
+ # Omitting 'border' attribute (Collector #1557)
+# if not 'border' in [ x.lower() for x in args.keys()]:
+# result = '%s border="0"' % result
+
+ if css_class is not None:
+ result = '%s class="%s"' % (result, css_class)
+
+ for key in args.keys():
+ value = args.get(key)
+ if value:
+ result = '%s %s="%s"' % (result, key, value)
+
+ return '%s />' % result
+
+
+def cookId(id, title, file):
+ if not id and hasattr(file,'filename'):
+ filename=file.filename
+ title=title or filename
+ id=filename[max(filename.rfind('/'),
+ filename.rfind('\\'),
+ filename.rfind(':'),
+ )+1:]
+ return id, title
+
+#class Pdata(Persistent, Implicit):
+# # Wrapper for possibly large data
+#
+# next=None
+#
+# def __init__(self, data):
+# self.data=data
+#
+# def __getslice__(self, i, j):
+# return self.data[i:j]
+#
+# def __len__(self):
+# data = str(self)
+# return len(data)
+#
+# def __str__(self):
+# next=self.next
+# if next is None: return self.data
+#
+# r=[self.data]
+# while next is not None:
+# self=next
+# r.append(self.data)
+# next=self.next
+#
+# return ''.join(r)