##############################################################################
"""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 $
"""
+from cgi import escape
+from cStringIO import StringIO
+from mimetools import choose_boundary
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 AccessControl.Role import RoleManager
+from AccessControl.SecurityInfo import ClassSecurityInfo
+from Acquisition import Implicit
+from App.class_init import InitializeClass
+from App.special_dtml import DTMLFile
+from DateTime.DateTime import DateTime
+from Persistence import Persistent
from webdav.common import rfc1123_date
+from webdav.interfaces import IWriteLock
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)
+from zope.contenttype import guess_content_type
+from zope.interface import implementedBy
+from zope.interface import implements
- self=self.this()
- self._setObject(id, File(id,title,file,content_type, precondition))
+from OFS.Cache import Cacheable
+from OFS.PropertyManager import PropertyManager
+from OFS.SimpleItem import Item_w__name__
- if REQUEST is not None:
- REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')
+from zope.event import notify
+from zope.lifecycleevent import ObjectModifiedEvent
+from zope.lifecycleevent import ObjectCreatedEvent
+
+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()
+
+ # First, we create the file without data:
+ self._setObject(id, File(id,title,'',content_type, precondition))
+
+ newFile = self._getOb(id)
+
+ # Now we "upload" the data. By doing this in two steps, we
+ # can use a database trick to make the upload more efficient.
+ if file:
+ newFile.manage_upload(file)
+ if content_type:
+ newFile.content_type=content_type
+
+ notify(ObjectCreatedEvent(newFile))
+
+ 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
+ RoleManager, Item_w__name__, Cacheable):
+ """A File object is a content object for arbitrary files."""
+
+ implements(implementedBy(Persistent),
+ implementedBy(Implicit),
+ implementedBy(PropertyManager),
+ implementedBy(RoleManager),
+ implementedBy(Item_w__name__),
+ implementedBy(Cacheable),
+ IWriteLock,
+ HTTPRangeSupport.HTTPRangeInterface,
+ )
+ meta_type='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
+
+ data, size = self._read_data(file)
+ content_type=self._get_content_type(file, data, id, content_type)
+ self.update_data(data, content_type, size)
+
+ 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
+
+ data = self.data
+ if isinstance(data, str):
+ RESPONSE.write(data[start:end])
+ return True
+
+ # Linked Pdata objects. Urgh.
+ pos = 0
+ while data is not None:
+ l = len(data.data)
+ pos = pos + l
+ if pos > start:
+ # We are within the range
+ lstart = l - (pos - start)
+
+ if lstart < 0: lstart = 0
+
+ # find the endpoint
+ if end <= pos:
+ lend = l - (pos - end)
+
+ # Send and end transmission
+ RESPONSE.write(data[lstart:lend])
+ break
+
+ # Not yet at the end, transmit what we have.
+ RESPONSE.write(data[lstart:])
+
+ data = data.next
+
+ 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
+
+ data = self.data
+ # The Pdata map allows us to jump into the Pdata chain
+ # arbitrarily during out-of-order range searching.
+ pdata_map = {}
+ pdata_map[0] = data
+
+ 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))
+
+ if isinstance(data, str):
+ RESPONSE.write(data[start:end])
+
+ else:
+ # Yippee. Linked Pdata objects. The following
+ # calculations allow us to fast-forward through the
+ # Pdata chain without a lot of dereferencing if we
+ # did the work already.
+ first_size = len(pdata_map[0].data)
+ if start < first_size:
+ closest_pos = 0
+ else:
+ closest_pos = (
+ ((start - first_size) >> 16 << 16) +
+ first_size)
+ pos = min(closest_pos, max(pdata_map.keys()))
+ data = pdata_map[pos]
+
+ while data is not None:
+ l = len(data.data)
+ pos = pos + l
+ if pos > start:
+ # We are within the range
+ lstart = l - (pos - start)
+
+ if lstart < 0: lstart = 0
+
+ # find the endpoint
+ if end <= pos:
+ lend = l - (pos - end)
+
+ # Send and loop to next range
+ RESPONSE.write(data[lstart:lend])
+ break
+
+ # Not yet at the end, transmit what we have.
+ RESPONSE.write(data[lstart:])
+
+ data = data.next
+ # Store a reference to a Pdata chain link so we
+ # don't have to deref during this request again.
+ pdata_map[pos] = data
+
+ # Do not keep the link references around.
+ del pdata_map
+
+ 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)
+
+ data=self.data
+ if isinstance(data, str):
+ RESPONSE.setBase(None)
+ return data
+
+ while data is not None:
+ RESPONSE.write(data.data)
+ data=data.next
+
+ 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/'):
+ return str(self.data)
+ return ''
+
+ security.declarePrivate('update_data')
+ def update_data(self, data, content_type=None, size=None):
+ if isinstance(data, unicode):
+ raise TypeError('Data can only be str or file-like. '
+ 'Unicode objects are expressly forbidden.')
+
+ if content_type is not None: self.content_type=content_type
+ if size is None: size=len(data)
+ self.size=size
+ self.data=data
+ 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, len(filedata))
+ else:
+ self.ZCacheable_invalidate()
+
+ notify(ObjectModifiedEvent(self))
+
+ 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"
+
+ data, size = self._read_data(file)
+ content_type=self._get_content_type(file, data, self.__name__,
+ 'application/octet-stream')
+ self.update_data(data, content_type, size)
+
+ notify(ObjectModifiedEvent(self))
+
+ if REQUEST:
+ message="Saved changes."
+ return self.manage_main(self,REQUEST,manage_tabs_message=message)
+
+ def _get_content_type(self, file, body, id, content_type=None):
+ headers=getattr(file, 'headers', None)
+ if headers and headers.has_key('content-type'):
+ content_type=headers['content-type']
+ else:
+ if not isinstance(body, str): body=body.data
+ content_type, enc=guess_content_type(
+ getattr(file, 'filename',id), body, content_type)
+ return content_type
+
+ def _read_data(self, file):
+ import transaction
+
+ n=1 << 16
+
+ if isinstance(file, str):
+ size=len(file)
+ if size < n: return file, size
+ # Big string: cut it into smaller chunks
+ file = StringIO(file)
+
+ if isinstance(file, FileUpload) and not file:
+ raise ValueError, 'File not specified'
+
+ if hasattr(file, '__class__') and file.__class__ is Pdata:
+ size=len(file)
+ return file, size
+
+ seek=file.seek
+ read=file.read
+
+ seek(0,2)
+ size=end=file.tell()
+
+ if size <= 2*n:
+ seek(0)
+ if size < n: return read(size), size
+ return Pdata(read(size)), size
+
+ # Make sure we have an _p_jar, even if we are a new object, by
+ # doing a sub-transaction commit.
+ transaction.savepoint(optimistic=True)
+
+ if self._p_jar is None:
+ # Ugh
+ seek(0)
+ return Pdata(read(size)), size
+
+ # Now we're going to build a linked list from back
+ # to front to minimize the number of database updates
+ # and to allow us to get things out of memory as soon as
+ # possible.
+ next = None
+ while end > 0:
+ pos = end-n
+ if pos < n:
+ pos = 0 # we always want at least n bytes
+ seek(pos)
+
+ # Create the object and assign it a next pointer
+ # in the same transaction, so that there is only
+ # a single database update for it.
+ data = Pdata(read(end-pos))
+ self._p_jar.add(data)
+ data.next = next
+
+ # Save the object so that we can release its memory.
+ transaction.savepoint(optimistic=True)
+ data._p_deactivate()
+ # The object should be assigned an oid and be a ghost.
+ assert data._p_oid is not None
+ assert data._p_state == -1
+
+ next = data
+ end = pos
+
+ return next, size
+
+ 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']
+
+ data, size = self._read_data(file)
+ content_type=self._get_content_type(file, data, self.__name__,
+ type or self.content_type)
+ self.update_data(data, content_type, size)
+
+ 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: size=len(self.data)
+ 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
+
+ data = self.data
+ if isinstance(data, str):
+ RESPONSE.setBase(None)
+ return data
+
+ while data is not None:
+ RESPONSE.write(data.data)
+ data = data.next
+
+ return ''
manage_addImageForm=DTMLFile('dtml/imageAdd',globals(),
- Kind='Image',kind='image')
+ 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
+ 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()
+
+ # First, we create the image without data:
+ self._setObject(id, Image(id,title,'',content_type, precondition))
+
+ newFile = self._getOb(id)
+
+ # Now we "upload" the data. By doing this in two steps, we
+ # can use a database trick to make the upload more efficient.
+ if file:
+ newFile.manage_upload(file)
+ if content_type:
+ newFile.content_type=content_type
+
+ notify(ObjectCreatedEvent(newFile))
+
+ if REQUEST is not None:
+ try: url=self.DestinationURL()
+ except: url=REQUEST['URL1']
+ REQUEST.RESPONSE.redirect('%s/manage_main' % url)
+ return id
+
+
+def getImageInfo(data):
+ data = str(data)
+ size = len(data)
+ height = -1
+ width = -1
+ content_type = ''
+
+ # handle GIFs
+ 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 = StringIO(data)
+ 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
+ """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.
+ """
+ meta_type='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, data, content_type=None, size=None):
+ if isinstance(data, unicode):
+ raise TypeError('Data can only be str or file-like. '
+ 'Unicode objects are expressly forbidden.')
+
+ if size is None: size=len(data)
+
+ self.size=size
+ self.data=data
+
+ ct, width, height = getImageInfo(data)
+ 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
+
+ self.ZCacheable_invalidate()
+ self.ZCacheable_set(None)
+ self.http__refreshEtag()
+
+ 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)
+ 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)