1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 # This module is based on OFS.Image originaly copyrighted as:
5 # Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved.
7 # This software is subject to the provisions of the Zope Public License,
8 # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
9 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
10 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
11 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
12 # FOR A PARTICULAR PURPOSE
14 ##############################################################################
19 from cgi
import escape
20 from cStringIO
import StringIO
21 from mimetools
import choose_boundary
24 from AccessControl
.Permissions
import change_images_and_files
25 from AccessControl
.Permissions
import view_management_screens
26 from AccessControl
.Permissions
import view
as View
27 from AccessControl
.Permissions
import ftp_access
28 from AccessControl
.Permissions
import delete_objects
29 from AccessControl
.Role
import RoleManager
30 from AccessControl
.SecurityInfo
import ClassSecurityInfo
31 from Acquisition
import Implicit
32 from App
.class_init
import InitializeClass
33 from App
.special_dtml
import DTMLFile
34 from DateTime
.DateTime
import DateTime
35 from Persistence
import Persistent
36 from webdav
.common
import rfc1123_date
37 from webdav
.interfaces
import IWriteLock
38 from webdav
.Lockable
import ResourceLockedError
39 from ZPublisher
import HTTPRangeSupport
40 from ZPublisher
.HTTPRequest
import FileUpload
41 from ZPublisher
.Iterators
import filestream_iterator
42 from zExceptions
import Redirect
43 from zope
.contenttype
import guess_content_type
44 from zope
.interface
import implementedBy
45 from zope
.interface
import implements
47 from OFS
.Cache
import Cacheable
48 from OFS
.PropertyManager
import PropertyManager
49 from OFS
.SimpleItem
import Item_w__name__
51 from zope
.event
import notify
52 from zope
.lifecycleevent
import ObjectModifiedEvent
53 from zope
.lifecycleevent
import ObjectCreatedEvent
55 manage_addFileForm
= DTMLFile('dtml/imageAdd',
60 def manage_addFile(self
, id, file='', title
='', precondition
='',
61 content_type
='', REQUEST
=None):
62 """Add a new File object.
64 Creates a new File object 'id' with the contents of 'file'"""
68 content_type
= str(content_type
)
69 precondition
= str(precondition
)
71 id, title
= cookId(id, title
, file)
75 # First, we create the file without data:
76 self
._setObject
(id, File(id,title
,'',content_type
, precondition
))
78 newFile
= self
._getOb
(id)
80 # Now we "upload" the data. By doing this in two steps, we
81 # can use a database trick to make the upload more efficient.
83 newFile
.manage_upload(file)
85 newFile
.content_type
=content_type
87 notify(ObjectCreatedEvent(newFile
))
89 if REQUEST
is not None:
90 REQUEST
['RESPONSE'].redirect(self
.absolute_url()+'/manage_main')
93 class File(Persistent
, Implicit
, PropertyManager
,
94 RoleManager
, Item_w__name__
, Cacheable
):
95 """A File object is a content object for arbitrary files."""
97 implements(implementedBy(Persistent
),
98 implementedBy(Implicit
),
99 implementedBy(PropertyManager
),
100 implementedBy(RoleManager
),
101 implementedBy(Item_w__name__
),
102 implementedBy(Cacheable
),
104 HTTPRangeSupport
.HTTPRangeInterface
,
108 security
= ClassSecurityInfo()
109 security
.declareObjectProtected(View
)
114 manage_editForm
=DTMLFile('dtml/fileEdit',globals(),
115 Kind
='File',kind
='file')
116 manage_editForm
._setName
('manage_editForm')
118 security
.declareProtected(view_management_screens
, 'manage')
119 security
.declareProtected(view_management_screens
, 'manage_main')
120 manage
=manage_main
=manage_editForm
121 manage_uploadForm
=manage_editForm
125 {'label':'Edit', 'action':'manage_main',
126 'help':('OFSP','File_Edit.stx')},
127 {'label':'View', 'action':'',
128 'help':('OFSP','File_View.stx')},
130 + PropertyManager
.manage_options
131 + RoleManager
.manage_options
132 + Item_w__name__
.manage_options
133 + Cacheable
.manage_options
136 _properties
=({'id':'title', 'type': 'string'},
137 {'id':'content_type', 'type':'string'},
140 def __init__(self
, id, title
, file, content_type
='', precondition
=''):
143 self
.precondition
=precondition
145 data
, size
= self
._read
_data
(file)
146 content_type
=self
._get
_content
_type
(file, data
, id, content_type
)
147 self
.update_data(data
, content_type
, size
)
152 def _if_modified_since_request_handler(self
, REQUEST
, RESPONSE
):
153 # HTTP If-Modified-Since header handling: return True if
154 # we can handle this request by returning a 304 response
155 header
=REQUEST
.get_header('If-Modified-Since', None)
156 if header
is not None:
157 header
=header
.split( ';')[0]
158 # Some proxies seem to send invalid date strings for this
159 # header. If the date string is not valid, we ignore it
160 # rather than raise an error to be generally consistent
161 # with common servers such as Apache (which can usually
162 # understand the screwy date string as a lucky side effect
163 # of the way they parse it).
164 # This happens to be what RFC2616 tells us to do in the face of an
166 try: mod_since
=long(DateTime(header
).timeTime())
167 except: mod_since
=None
168 if mod_since
is not None:
170 last_mod
= long(self
._p
_mtime
)
173 if last_mod
> 0 and last_mod
<= mod_since
:
174 RESPONSE
.setHeader('Last-Modified',
175 rfc1123_date(self
._p
_mtime
))
176 RESPONSE
.setHeader('Content-Type', self
.content_type
)
177 RESPONSE
.setHeader('Accept-Ranges', 'bytes')
178 RESPONSE
.setStatus(304)
181 def _range_request_handler(self
, REQUEST
, RESPONSE
):
182 # HTTP Range header handling: return True if we've served a range
183 # chunk out of our data.
184 range = REQUEST
.get_header('Range', None)
185 request_range
= REQUEST
.get_header('Request-Range', None)
186 if request_range
is not None:
187 # Netscape 2 through 4 and MSIE 3 implement a draft version
188 # Later on, we need to serve a different mime-type as well.
189 range = request_range
190 if_range
= REQUEST
.get_header('If-Range', None)
191 if range is not None:
192 ranges
= HTTPRangeSupport
.parseRange(range)
194 if if_range
is not None:
195 # Only send ranges if the data isn't modified, otherwise send
196 # the whole object. Support both ETags and Last-Modified dates!
197 if len(if_range
) > 1 and if_range
[:2] == 'ts':
199 if if_range
!= self
.http__etag():
200 # Modified, so send a normal response. We delete
201 # the ranges, which causes us to skip to the 200
206 date
= if_range
.split( ';')[0]
207 try: mod_since
=long(DateTime(date
).timeTime())
208 except: mod_since
=None
209 if mod_since
is not None:
211 last_mod
= long(self
._p
_mtime
)
214 if last_mod
> mod_since
:
215 # Modified, so send a normal response. We delete
216 # the ranges, which causes us to skip to the 200
221 # Search for satisfiable ranges.
223 for start
, end
in ranges
:
224 if start
< self
.size
:
229 RESPONSE
.setHeader('Content-Range',
230 'bytes */%d' % self
.size
)
231 RESPONSE
.setHeader('Accept-Ranges', 'bytes')
232 RESPONSE
.setHeader('Last-Modified',
233 rfc1123_date(self
._p
_mtime
))
234 RESPONSE
.setHeader('Content-Type', self
.content_type
)
235 RESPONSE
.setHeader('Content-Length', self
.size
)
236 RESPONSE
.setStatus(416)
239 ranges
= HTTPRangeSupport
.expandRanges(ranges
, self
.size
)
242 # Easy case, set extra header and return partial set.
243 start
, end
= ranges
[0]
246 RESPONSE
.setHeader('Last-Modified',
247 rfc1123_date(self
._p
_mtime
))
248 RESPONSE
.setHeader('Content-Type', self
.content_type
)
249 RESPONSE
.setHeader('Content-Length', size
)
250 RESPONSE
.setHeader('Accept-Ranges', 'bytes')
251 RESPONSE
.setHeader('Content-Range',
252 'bytes %d-%d/%d' % (start
, end
- 1, self
.size
))
253 RESPONSE
.setStatus(206) # Partial content
256 if isinstance(data
, str):
257 RESPONSE
.write(data
[start
:end
])
260 # Linked Pdata objects. Urgh.
262 while data
is not None:
266 # We are within the range
267 lstart
= l
- (pos
- start
)
269 if lstart
< 0: lstart
= 0
273 lend
= l
- (pos
- end
)
275 # Send and end transmission
276 RESPONSE
.write(data
[lstart
:lend
])
279 # Not yet at the end, transmit what we have.
280 RESPONSE
.write(data
[lstart
:])
287 boundary
= choose_boundary()
289 # Calculate the content length
290 size
= (8 + len(boundary
) + # End marker length
291 len(ranges
) * ( # Constant lenght per set
292 49 + len(boundary
) + len(self
.content_type
) +
293 len('%d' % self
.size
)))
294 for start
, end
in ranges
:
295 # Variable length per set
296 size
= (size
+ len('%d%d' % (start
, end
- 1)) +
300 # Some clients implement an earlier draft of the spec, they
301 # will only accept x-byteranges.
302 draftprefix
= (request_range
is not None) and 'x-' or ''
304 RESPONSE
.setHeader('Content-Length', size
)
305 RESPONSE
.setHeader('Accept-Ranges', 'bytes')
306 RESPONSE
.setHeader('Last-Modified',
307 rfc1123_date(self
._p
_mtime
))
308 RESPONSE
.setHeader('Content-Type',
309 'multipart/%sbyteranges; boundary=%s' % (
310 draftprefix
, boundary
))
311 RESPONSE
.setStatus(206) # Partial content
314 # The Pdata map allows us to jump into the Pdata chain
315 # arbitrarily during out-of-order range searching.
319 for start
, end
in ranges
:
320 RESPONSE
.write('\r\n--%s\r\n' % boundary
)
321 RESPONSE
.write('Content-Type: %s\r\n' %
324 'Content-Range: bytes %d-%d/%d\r\n\r\n' % (
325 start
, end
- 1, self
.size
))
327 if isinstance(data
, str):
328 RESPONSE
.write(data
[start
:end
])
331 # Yippee. Linked Pdata objects. The following
332 # calculations allow us to fast-forward through the
333 # Pdata chain without a lot of dereferencing if we
334 # did the work already.
335 first_size
= len(pdata_map
[0].data
)
336 if start
< first_size
:
340 ((start
- first_size
) >> 16 << 16) +
342 pos
= min(closest_pos
, max(pdata_map
.keys()))
343 data
= pdata_map
[pos
]
345 while data
is not None:
349 # We are within the range
350 lstart
= l
- (pos
- start
)
352 if lstart
< 0: lstart
= 0
356 lend
= l
- (pos
- end
)
358 # Send and loop to next range
359 RESPONSE
.write(data
[lstart
:lend
])
362 # Not yet at the end, transmit what we have.
363 RESPONSE
.write(data
[lstart
:])
366 # Store a reference to a Pdata chain link so we
367 # don't have to deref during this request again.
368 pdata_map
[pos
] = data
370 # Do not keep the link references around.
373 RESPONSE
.write('\r\n--%s--\r\n' % boundary
)
376 security
.declareProtected(View
, 'index_html')
377 def index_html(self
, REQUEST
, RESPONSE
):
379 The default view of the contents of a File or Image.
381 Returns the contents of the file or image. Also, sets the
382 Content-Type HTTP header to the objects content type.
385 if self
._if
_modified
_since
_request
_handler
(REQUEST
, RESPONSE
):
386 # we were able to handle this by returning a 304
387 # unfortunately, because the HTTP cache manager uses the cache
388 # API, and because 304 responses are required to carry the Expires
389 # header for HTTP/1.1, we need to call ZCacheable_set here.
390 # This is nonsensical for caches other than the HTTP cache manager
392 self
.ZCacheable_set(None)
395 if self
.precondition
and hasattr(self
, str(self
.precondition
)):
396 # Grab whatever precondition was defined and then
397 # execute it. The precondition will raise an exception
398 # if something violates its terms.
399 c
=getattr(self
, str(self
.precondition
))
400 if hasattr(c
,'isDocTemp') and c
.isDocTemp
:
401 c(REQUEST
['PARENTS'][1],REQUEST
)
405 if self
._range
_request
_handler
(REQUEST
, RESPONSE
):
406 # we served a chunk of content in response to a range request.
409 RESPONSE
.setHeader('Last-Modified', rfc1123_date(self
._p
_mtime
))
410 RESPONSE
.setHeader('Content-Type', self
.content_type
)
411 RESPONSE
.setHeader('Content-Length', self
.size
)
412 RESPONSE
.setHeader('Accept-Ranges', 'bytes')
414 if self
.ZCacheable_isCachingEnabled():
415 result
= self
.ZCacheable_get(default
=None)
416 if result
is not None:
417 # We will always get None from RAMCacheManager and HTTP
418 # Accelerated Cache Manager but we will get
419 # something implementing the IStreamIterator interface
420 # from a "FileCacheManager"
423 self
.ZCacheable_set(None)
426 if isinstance(data
, str):
427 RESPONSE
.setBase(None)
430 while data
is not None:
431 RESPONSE
.write(data
.data
)
436 security
.declareProtected(View
, 'view_image_or_file')
437 def view_image_or_file(self
, URL1
):
439 The default view of the contents of the File or Image.
443 security
.declareProtected(View
, 'PrincipiaSearchSource')
444 def PrincipiaSearchSource(self
):
445 """ Allow file objects to be searched.
447 if self
.content_type
.startswith('text/'):
448 return str(self
.data
)
451 security
.declarePrivate('update_data')
452 def update_data(self
, data
, content_type
=None, size
=None):
453 if isinstance(data
, unicode):
454 raise TypeError('Data can only be str or file-like. '
455 'Unicode objects are expressly forbidden.')
457 if content_type
is not None: self
.content_type
=content_type
458 if size
is None: size
=len(data
)
461 self
.ZCacheable_invalidate()
462 self
.ZCacheable_set(None)
463 self
.http__refreshEtag()
465 security
.declareProtected(change_images_and_files
, 'manage_edit')
466 def manage_edit(self
, title
, content_type
, precondition
='',
467 filedata
=None, REQUEST
=None):
469 Changes the title and content type attributes of the File or Image.
471 if self
.wl_isLocked():
472 raise ResourceLockedError
, "File is locked via WebDAV"
474 self
.title
=str(title
)
475 self
.content_type
=str(content_type
)
476 if precondition
: self
.precondition
=str(precondition
)
477 elif self
.precondition
: del self
.precondition
478 if filedata
is not None:
479 self
.update_data(filedata
, content_type
, len(filedata
))
481 self
.ZCacheable_invalidate()
483 notify(ObjectModifiedEvent(self
))
486 message
="Saved changes."
487 return self
.manage_main(self
,REQUEST
,manage_tabs_message
=message
)
489 security
.declareProtected(change_images_and_files
, 'manage_upload')
490 def manage_upload(self
,file='',REQUEST
=None):
492 Replaces the current contents of the File or Image object with file.
494 The file or images contents are replaced with the contents of 'file'.
496 if self
.wl_isLocked():
497 raise ResourceLockedError
, "File is locked via WebDAV"
499 data
, size
= self
._read
_data
(file)
500 content_type
=self
._get
_content
_type
(file, data
, self
.__name
__,
501 'application/octet-stream')
502 self
.update_data(data
, content_type
, size
)
504 notify(ObjectModifiedEvent(self
))
507 message
="Saved changes."
508 return self
.manage_main(self
,REQUEST
,manage_tabs_message
=message
)
510 def _get_content_type(self
, file, body
, id, content_type
=None):
511 headers
=getattr(file, 'headers', None)
512 if headers
and headers
.has_key('content-type'):
513 content_type
=headers
['content-type']
515 if not isinstance(body
, str): body
=body
.data
516 content_type
, enc
=guess_content_type(
517 getattr(file, 'filename',id), body
, content_type
)
520 def _read_data(self
, file):
525 if isinstance(file, str):
527 if size
< n
: return file, size
528 # Big string: cut it into smaller chunks
529 file = StringIO(file)
531 if isinstance(file, FileUpload
) and not file:
532 raise ValueError, 'File not specified'
534 if hasattr(file, '__class__') and file.__class
__ is Pdata
:
546 if size
< n
: return read(size
), size
547 return Pdata(read(size
)), size
549 # Make sure we have an _p_jar, even if we are a new object, by
550 # doing a sub-transaction commit.
551 transaction
.savepoint(optimistic
=True)
553 if self
._p
_jar
is None:
556 return Pdata(read(size
)), size
558 # Now we're going to build a linked list from back
559 # to front to minimize the number of database updates
560 # and to allow us to get things out of memory as soon as
566 pos
= 0 # we always want at least n bytes
569 # Create the object and assign it a next pointer
570 # in the same transaction, so that there is only
571 # a single database update for it.
572 data
= Pdata(read(end
-pos
))
573 self
._p
_jar
.add(data
)
576 # Save the object so that we can release its memory.
577 transaction
.savepoint(optimistic
=True)
579 # The object should be assigned an oid and be a ghost.
580 assert data
._p
_oid
is not None
581 assert data
._p
_state
== -1
588 security
.declareProtected(delete_objects
, 'DELETE')
590 security
.declareProtected(change_images_and_files
, 'PUT')
591 def PUT(self
, REQUEST
, RESPONSE
):
592 """Handle HTTP PUT requests"""
593 self
.dav__init(REQUEST
, RESPONSE
)
594 self
.dav__simpleifhandler(REQUEST
, RESPONSE
, refresh
=1)
595 type=REQUEST
.get_header('content-type', None)
597 file=REQUEST
['BODYFILE']
599 data
, size
= self
._read
_data
(file)
600 content_type
=self
._get
_content
_type
(file, data
, self
.__name
__,
601 type or self
.content_type
)
602 self
.update_data(data
, content_type
, size
)
604 RESPONSE
.setStatus(204)
607 security
.declareProtected(View
, 'get_size')
609 """Get the size of a file or image.
611 Returns the size of the file or image.
614 if size
is None: size
=len(self
.data
)
617 # deprecated; use get_size!
620 security
.declareProtected(View
, 'getContentType')
621 def getContentType(self
):
622 """Get the content type of a file or image.
624 Returns the content type (MIME type) of a file or image.
626 return self
.content_type
629 def __str__(self
): return str(self
.data
)
630 def __len__(self
): return 1
632 security
.declareProtected(ftp_access
, 'manage_FTPstat')
633 security
.declareProtected(ftp_access
, 'manage_FTPlist')
635 security
.declareProtected(ftp_access
, 'manage_FTPget')
636 def manage_FTPget(self
):
637 """Return body for ftp."""
638 RESPONSE
= self
.REQUEST
.RESPONSE
640 if self
.ZCacheable_isCachingEnabled():
641 result
= self
.ZCacheable_get(default
=None)
642 if result
is not None:
643 # We will always get None from RAMCacheManager but we will get
644 # something implementing the IStreamIterator interface
645 # from FileCacheManager.
646 # the content-length is required here by HTTPResponse, even
647 # though FTP doesn't use it.
648 RESPONSE
.setHeader('Content-Length', self
.size
)
652 if isinstance(data
, str):
653 RESPONSE
.setBase(None)
656 while data
is not None:
657 RESPONSE
.write(data
.data
)
662 manage_addImageForm
=DTMLFile('dtml/imageAdd',globals(),
663 Kind
='Image',kind
='image')
664 def manage_addImage(self
, id, file, title
='', precondition
='', content_type
='',
667 Add a new Image object.
669 Creates a new Image object 'id' with the contents of 'file'.
674 content_type
=str(content_type
)
675 precondition
=str(precondition
)
677 id, title
= cookId(id, title
, file)
681 # First, we create the image without data:
682 self
._setObject
(id, Image(id,title
,'',content_type
, precondition
))
684 newFile
= self
._getOb
(id)
686 # Now we "upload" the data. By doing this in two steps, we
687 # can use a database trick to make the upload more efficient.
689 newFile
.manage_upload(file)
691 newFile
.content_type
=content_type
693 notify(ObjectCreatedEvent(newFile
))
695 if REQUEST
is not None:
696 try: url
=self
.DestinationURL()
697 except: url
=REQUEST
['URL1']
698 REQUEST
.RESPONSE
.redirect('%s/manage_main' % url
)
702 def getImageInfo(data
):
710 if (size
>= 10) and data
[:6] in ('GIF87a', 'GIF89a'):
711 # Check to see if content_type is correct
712 content_type
= 'image/gif'
713 w
, h
= struct
.unpack("<HH", data
[6:10])
717 # See PNG v1.2 spec (http://www.cdrom.com/pub/png/spec/)
718 # Bytes 0-7 are below, 4-byte chunk length, then 'IHDR'
719 # and finally the 4-byte width, height
720 elif ((size
>= 24) and (data
[:8] == '\211PNG\r\n\032\n')
721 and (data
[12:16] == 'IHDR')):
722 content_type
= 'image/png'
723 w
, h
= struct
.unpack(">LL", data
[16:24])
727 # Maybe this is for an older PNG version.
728 elif (size
>= 16) and (data
[:8] == '\211PNG\r\n\032\n'):
729 # Check to see if we have the right content type
730 content_type
= 'image/png'
731 w
, h
= struct
.unpack(">LL", data
[8:16])
736 elif (size
>= 2) and (data
[:2] == '\377\330'):
737 content_type
= 'image/jpeg'
738 jpeg
= StringIO(data
)
742 while (b
and ord(b
) != 0xDA):
743 while (ord(b
) != 0xFF): b
= jpeg
.read(1)
744 while (ord(b
) == 0xFF): b
= jpeg
.read(1)
745 if (ord(b
) >= 0xC0 and ord(b
) <= 0xC3):
747 h
, w
= struct
.unpack(">HH", jpeg
.read(4))
750 jpeg
.read(int(struct
.unpack(">H", jpeg
.read(2))[0])-2)
756 return content_type
, width
, height
760 """Image objects can be GIF, PNG or JPEG and have the same methods
761 as File objects. Images also have a string representation that
762 renders an HTML 'IMG' tag.
766 security
= ClassSecurityInfo()
767 security
.declareObjectProtected(View
)
773 # FIXME: Redundant, already in base class
774 security
.declareProtected(change_images_and_files
, 'manage_edit')
775 security
.declareProtected(change_images_and_files
, 'manage_upload')
776 security
.declareProtected(change_images_and_files
, 'PUT')
777 security
.declareProtected(View
, 'index_html')
778 security
.declareProtected(View
, 'get_size')
779 security
.declareProtected(View
, 'getContentType')
780 security
.declareProtected(ftp_access
, 'manage_FTPstat')
781 security
.declareProtected(ftp_access
, 'manage_FTPlist')
782 security
.declareProtected(ftp_access
, 'manage_FTPget')
783 security
.declareProtected(delete_objects
, 'DELETE')
785 _properties
=({'id':'title', 'type': 'string'},
786 {'id':'alt', 'type':'string'},
787 {'id':'content_type', 'type':'string','mode':'w'},
788 {'id':'height', 'type':'string'},
789 {'id':'width', 'type':'string'},
793 ({'label':'Edit', 'action':'manage_main',
794 'help':('OFSP','Image_Edit.stx')},
795 {'label':'View', 'action':'view_image_or_file',
796 'help':('OFSP','Image_View.stx')},)
797 + PropertyManager
.manage_options
798 + RoleManager
.manage_options
799 + Item_w__name__
.manage_options
800 + Cacheable
.manage_options
803 manage_editForm
=DTMLFile('dtml/imageEdit',globals(),
804 Kind
='Image',kind
='image')
805 manage_editForm
._setName
('manage_editForm')
807 security
.declareProtected(View
, 'view_image_or_file')
808 view_image_or_file
=DTMLFile('dtml/imageView',globals())
810 security
.declareProtected(view_management_screens
, 'manage')
811 security
.declareProtected(view_management_screens
, 'manage_main')
812 manage
=manage_main
=manage_editForm
813 manage_uploadForm
=manage_editForm
815 security
.declarePrivate('update_data')
816 def update_data(self
, data
, content_type
=None, size
=None):
817 if isinstance(data
, unicode):
818 raise TypeError('Data can only be str or file-like. '
819 'Unicode objects are expressly forbidden.')
821 if size
is None: size
=len(data
)
826 ct
, width
, height
= getImageInfo(data
)
829 if width
>= 0 and height
>= 0:
833 # Now we should have the correct content type, or still None
834 if content_type
is not None: self
.content_type
= content_type
836 self
.ZCacheable_invalidate()
837 self
.ZCacheable_set(None)
838 self
.http__refreshEtag()
843 security
.declareProtected(View
, 'tag')
844 def tag(self
, height
=None, width
=None, alt
=None,
845 scale
=0, xscale
=0, yscale
=0, css_class
=None, title
=None, **args
):
847 Generate an HTML IMG tag for this image, with customization.
848 Arguments to self.tag() can be any valid attributes of an IMG tag.
849 'src' will always be an absolute pathname, to prevent redundant
850 downloading of images. Defaults are applied intelligently for
851 'height', 'width', and 'alt'. If specified, the 'scale', 'xscale',
852 and 'yscale' keyword arguments will be used to automatically adjust
853 the output height and width values of the image tag.
855 Since 'class' is a Python reserved word, it cannot be passed in
856 directly in keyword arguments which is a problem if you are
857 trying to use 'tag()' to include a CSS class. The tag() method
858 will accept a 'css_class' argument that will be converted to
859 'class' in the output tag to work around this.
861 if height
is None: height
=self
.height
862 if width
is None: width
=self
.width
864 # Auto-scaling support
865 xdelta
= xscale
or scale
866 ydelta
= yscale
or scale
869 width
= str(int(round(int(width
) * xdelta
)))
870 if ydelta
and height
:
871 height
= str(int(round(int(height
) * ydelta
)))
873 result
='<img src="%s"' % (self
.absolute_url())
876 alt
=getattr(self
, 'alt', '')
877 result
= '%s alt="%s"' % (result
, escape(alt
, 1))
880 title
=getattr(self
, 'title', '')
881 result
= '%s title="%s"' % (result
, escape(title
, 1))
884 result
= '%s height="%s"' % (result
, height
)
887 result
= '%s width="%s"' % (result
, width
)
889 # Omitting 'border' attribute (Collector #1557)
890 # if not 'border' in [ x.lower() for x in args.keys()]:
891 # result = '%s border="0"' % result
893 if css_class
is not None:
894 result
= '%s class="%s"' % (result
, css_class
)
896 for key
in args
.keys():
897 value
= args
.get(key
)
899 result
= '%s %s="%s"' % (result
, key
, value
)
901 return '%s />' % result
904 def cookId(id, title
, file):
905 if not id and hasattr(file,'filename'):
906 filename
=file.filename
907 title
=title
or filename
908 id=filename
[max(filename
.rfind('/'),
909 filename
.rfind('\\'),
914 class Pdata(Persistent
, Implicit
):
915 # Wrapper for possibly large data
919 def __init__(self
, data
):
922 def __getslice__(self
, i
, j
):
923 return self
.data
[i
:j
]
931 if next
is None: return self
.data
934 while next
is not None: