9d2fb6fdedde0b8921f64ae09ccf6c620fbe3a8f
[Photo.git] / blobbases.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 # This module is based on OFS.Image originaly copyrighted as:
4 #
5 # Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved.
6 #
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
13 #
14 ##############################################################################
15 """Image object
16
17 $Id: blobbases.py 949 2009-04-30 14:42:24Z pin $
18 $URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/blobbases.py $
19 """
20
21 import struct
22 from warnings import warn
23 from zope.contenttype import guess_content_type
24 from Globals import DTMLFile
25 from Globals import InitializeClass
26 from OFS.PropertyManager import PropertyManager
27 from AccessControl import ClassSecurityInfo
28 from AccessControl.Role import RoleManager
29 from AccessControl.Permissions import change_images_and_files
30 from AccessControl.Permissions import view_management_screens
31 from AccessControl.Permissions import view as View
32 from AccessControl.Permissions import ftp_access
33 from AccessControl.Permissions import delete_objects
34 from webdav.common import rfc1123_date
35 from webdav.Lockable import ResourceLockedError
36 from webdav.WriteLockInterface import WriteLockInterface
37 from OFS.SimpleItem import Item_w__name__
38 from cStringIO import StringIO
39 from Globals import Persistent
40 from Acquisition import Implicit
41 from DateTime import DateTime
42 from OFS.Cache import Cacheable
43 from mimetools import choose_boundary
44 from ZPublisher import HTTPRangeSupport
45 from ZPublisher.HTTPRequest import FileUpload
46 from ZPublisher.Iterators import filestream_iterator
47 from zExceptions import Redirect
48 from cgi import escape
49 import transaction
50 from ZODB.blob import Blob
51
52 CHUNK_SIZE = 1 << 16
53
54 manage_addFileForm=DTMLFile('dtml/imageAdd', globals(),Kind='File',kind='file')
55 def manage_addFile(self,id,file='',title='',precondition='', content_type='',
56 REQUEST=None):
57 """Add a new File object.
58
59 Creates a new File object 'id' with the contents of 'file'"""
60
61 id=str(id)
62 title=str(title)
63 content_type=str(content_type)
64 precondition=str(precondition)
65
66 id, title = cookId(id, title, file)
67
68 self=self.this()
69 self._setObject(id, File(id,title,file,content_type, precondition))
70
71 if REQUEST is not None:
72 REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')
73
74
75 class File(Persistent, Implicit, PropertyManager,
76 RoleManager, Item_w__name__, Cacheable):
77 """A File object is a content object for arbitrary files."""
78
79 __implements__ = (WriteLockInterface, HTTPRangeSupport.HTTPRangeInterface)
80 meta_type='Blob File'
81
82 security = ClassSecurityInfo()
83 security.declareObjectProtected(View)
84
85 precondition=''
86 size=None
87
88 manage_editForm =DTMLFile('dtml/fileEdit',globals(),
89 Kind='File',kind='file')
90 manage_editForm._setName('manage_editForm')
91
92 security.declareProtected(view_management_screens, 'manage')
93 security.declareProtected(view_management_screens, 'manage_main')
94 manage=manage_main=manage_editForm
95 manage_uploadForm=manage_editForm
96
97 manage_options=(
98 (
99 {'label':'Edit', 'action':'manage_main',
100 'help':('OFSP','File_Edit.stx')},
101 {'label':'View', 'action':'',
102 'help':('OFSP','File_View.stx')},
103 )
104 + PropertyManager.manage_options
105 + RoleManager.manage_options
106 + Item_w__name__.manage_options
107 + Cacheable.manage_options
108 )
109
110 _properties=({'id':'title', 'type': 'string'},
111 {'id':'content_type', 'type':'string'},
112 )
113
114 def __init__(self, id, title, file, content_type='', precondition=''):
115 self.__name__=id
116 self.title=title
117 self.precondition=precondition
118 self.uploaded_filename = cookId('', '', file)[0]
119 self.bdata = Blob()
120
121 content_type=self._get_content_type(file, id, content_type)
122 self.update_data(file, content_type)
123
124 security.declarePrivate('save')
125 def save(self, file):
126 bf = self.bdata.open('w')
127 bf.write(file.read())
128 self.size = bf.tell()
129 bf.close()
130
131 security.declarePrivate('open')
132 def open(self, mode='r'):
133 bf = self.bdata.open(mode)
134 return bf
135
136 security.declarePrivate('updateSize')
137 def updateSize(self, size=None):
138 if size is None :
139 bf = self.open('r')
140 bf.seek(0,2)
141 self.size = bf.tell()
142 bf.close()
143 else :
144 self.size = size
145
146 def _getLegacyData(self) :
147 warn("Accessing 'data' attribute may be inefficient with "
148 "this blob based file. You should refactor your product "
149 "by accessing data like: "
150 "f = self.open('r') "
151 "data = f.read()",
152 DeprecationWarning, stacklevel=2)
153 f = self.open()
154 data = f.read()
155 f.close()
156 return data
157
158 def _setLegacyData(self, data) :
159 warn("Accessing 'data' attribute may be inefficient with "
160 "this blob based file. You should refactor your product "
161 "by accessing data like: "
162 "f = self.save(data)",
163 DeprecationWarning, stacklevel=2)
164 if isinstance(data, str) :
165 sio = StringIO()
166 sio.write(data)
167 sio.seek(0)
168 data = sio
169 self.save(data)
170
171 data = property(_getLegacyData, _setLegacyData,
172 "Data Legacy attribute to ensure compatibility "
173 "with derived classes that access data by this way.")
174
175 def id(self):
176 return self.__name__
177
178 def _if_modified_since_request_handler(self, REQUEST, RESPONSE):
179 # HTTP If-Modified-Since header handling: return True if
180 # we can handle this request by returning a 304 response
181 header=REQUEST.get_header('If-Modified-Since', None)
182 if header is not None:
183 header=header.split( ';')[0]
184 # Some proxies seem to send invalid date strings for this
185 # header. If the date string is not valid, we ignore it
186 # rather than raise an error to be generally consistent
187 # with common servers such as Apache (which can usually
188 # understand the screwy date string as a lucky side effect
189 # of the way they parse it).
190 # This happens to be what RFC2616 tells us to do in the face of an
191 # invalid date.
192 try: mod_since=long(DateTime(header).timeTime())
193 except: mod_since=None
194 if mod_since is not None:
195 if self._p_mtime:
196 last_mod = long(self._p_mtime)
197 else:
198 last_mod = long(0)
199 if last_mod > 0 and last_mod <= mod_since:
200 RESPONSE.setHeader('Last-Modified',
201 rfc1123_date(self._p_mtime))
202 RESPONSE.setHeader('Content-Type', self.content_type)
203 RESPONSE.setHeader('Accept-Ranges', 'bytes')
204 RESPONSE.setStatus(304)
205 return True
206
207 def _range_request_handler(self, REQUEST, RESPONSE):
208 # HTTP Range header handling: return True if we've served a range
209 # chunk out of our data.
210 range = REQUEST.get_header('Range', None)
211 request_range = REQUEST.get_header('Request-Range', None)
212 if request_range is not None:
213 # Netscape 2 through 4 and MSIE 3 implement a draft version
214 # Later on, we need to serve a different mime-type as well.
215 range = request_range
216 if_range = REQUEST.get_header('If-Range', None)
217 if range is not None:
218 ranges = HTTPRangeSupport.parseRange(range)
219
220 if if_range is not None:
221 # Only send ranges if the data isn't modified, otherwise send
222 # the whole object. Support both ETags and Last-Modified dates!
223 if len(if_range) > 1 and if_range[:2] == 'ts':
224 # ETag:
225 if if_range != self.http__etag():
226 # Modified, so send a normal response. We delete
227 # the ranges, which causes us to skip to the 200
228 # response.
229 ranges = None
230 else:
231 # Date
232 date = if_range.split( ';')[0]
233 try: mod_since=long(DateTime(date).timeTime())
234 except: mod_since=None
235 if mod_since is not None:
236 if self._p_mtime:
237 last_mod = long(self._p_mtime)
238 else:
239 last_mod = long(0)
240 if last_mod > mod_since:
241 # Modified, so send a normal response. We delete
242 # the ranges, which causes us to skip to the 200
243 # response.
244 ranges = None
245
246 if ranges:
247 # Search for satisfiable ranges.
248 satisfiable = 0
249 for start, end in ranges:
250 if start < self.size:
251 satisfiable = 1
252 break
253
254 if not satisfiable:
255 RESPONSE.setHeader('Content-Range',
256 'bytes */%d' % self.size)
257 RESPONSE.setHeader('Accept-Ranges', 'bytes')
258 RESPONSE.setHeader('Last-Modified',
259 rfc1123_date(self._p_mtime))
260 RESPONSE.setHeader('Content-Type', self.content_type)
261 RESPONSE.setHeader('Content-Length', self.size)
262 RESPONSE.setStatus(416)
263 return True
264
265 ranges = HTTPRangeSupport.expandRanges(ranges, self.size)
266
267 if len(ranges) == 1:
268 # Easy case, set extra header and return partial set.
269 start, end = ranges[0]
270 size = end - start
271
272 RESPONSE.setHeader('Last-Modified',
273 rfc1123_date(self._p_mtime))
274 RESPONSE.setHeader('Content-Type', self.content_type)
275 RESPONSE.setHeader('Content-Length', size)
276 RESPONSE.setHeader('Accept-Ranges', 'bytes')
277 RESPONSE.setHeader('Content-Range',
278 'bytes %d-%d/%d' % (start, end - 1, self.size))
279 RESPONSE.setStatus(206) # Partial content
280
281 bf = self.open('r')
282 bf.seek(start)
283 RESPONSE.write(bf.read(size))
284 bf.close()
285 return True
286
287 else:
288 boundary = choose_boundary()
289
290 # Calculate the content length
291 size = (8 + len(boundary) + # End marker length
292 len(ranges) * ( # Constant lenght per set
293 49 + len(boundary) + len(self.content_type) +
294 len('%d' % self.size)))
295 for start, end in ranges:
296 # Variable length per set
297 size = (size + len('%d%d' % (start, end - 1)) +
298 end - start)
299
300
301 # Some clients implement an earlier draft of the spec, they
302 # will only accept x-byteranges.
303 draftprefix = (request_range is not None) and 'x-' or ''
304
305 RESPONSE.setHeader('Content-Length', size)
306 RESPONSE.setHeader('Accept-Ranges', 'bytes')
307 RESPONSE.setHeader('Last-Modified',
308 rfc1123_date(self._p_mtime))
309 RESPONSE.setHeader('Content-Type',
310 'multipart/%sbyteranges; boundary=%s' % (
311 draftprefix, boundary))
312 RESPONSE.setStatus(206) # Partial content
313
314 bf = self.open('r')
315
316 for start, end in ranges:
317 RESPONSE.write('\r\n--%s\r\n' % boundary)
318 RESPONSE.write('Content-Type: %s\r\n' %
319 self.content_type)
320 RESPONSE.write(
321 'Content-Range: bytes %d-%d/%d\r\n\r\n' % (
322 start, end - 1, self.size))
323
324
325 size = end - start
326 bf.seek(start)
327 RESPONSE.write(bf.read(size))
328
329 bf.close()
330
331 RESPONSE.write('\r\n--%s--\r\n' % boundary)
332 return True
333
334 security.declareProtected(View, 'index_html')
335 def index_html(self, REQUEST, RESPONSE):
336 """
337 The default view of the contents of a File or Image.
338
339 Returns the contents of the file or image. Also, sets the
340 Content-Type HTTP header to the objects content type.
341 """
342
343 if self._if_modified_since_request_handler(REQUEST, RESPONSE):
344 # we were able to handle this by returning a 304
345 # unfortunately, because the HTTP cache manager uses the cache
346 # API, and because 304 responses are required to carry the Expires
347 # header for HTTP/1.1, we need to call ZCacheable_set here.
348 # This is nonsensical for caches other than the HTTP cache manager
349 # unfortunately.
350 self.ZCacheable_set(None)
351 return ''
352
353 if self.precondition and hasattr(self, str(self.precondition)):
354 # Grab whatever precondition was defined and then
355 # execute it. The precondition will raise an exception
356 # if something violates its terms.
357 c=getattr(self, str(self.precondition))
358 if hasattr(c,'isDocTemp') and c.isDocTemp:
359 c(REQUEST['PARENTS'][1],REQUEST)
360 else:
361 c()
362
363 if self._range_request_handler(REQUEST, RESPONSE):
364 # we served a chunk of content in response to a range request.
365 return ''
366
367 RESPONSE.setHeader('Last-Modified', rfc1123_date(self._p_mtime))
368 RESPONSE.setHeader('Content-Type', self.content_type)
369 RESPONSE.setHeader('Content-Length', self.size)
370 RESPONSE.setHeader('Accept-Ranges', 'bytes')
371
372 if self.ZCacheable_isCachingEnabled():
373 result = self.ZCacheable_get(default=None)
374 if result is not None:
375 # We will always get None from RAMCacheManager and HTTP
376 # Accelerated Cache Manager but we will get
377 # something implementing the IStreamIterator interface
378 # from a "FileCacheManager"
379 return result
380
381 self.ZCacheable_set(None)
382
383 bf = self.open('r')
384 chunk = bf.read(CHUNK_SIZE)
385 while chunk :
386 RESPONSE.write(chunk)
387 chunk = bf.read(CHUNK_SIZE)
388 bf.close()
389 return ''
390
391 security.declareProtected(View, 'view_image_or_file')
392 def view_image_or_file(self, URL1):
393 """
394 The default view of the contents of the File or Image.
395 """
396 raise Redirect, URL1
397
398 security.declareProtected(View, 'PrincipiaSearchSource')
399 def PrincipiaSearchSource(self):
400 """ Allow file objects to be searched.
401 """
402 if self.content_type.startswith('text/'):
403 bf = self.open('r')
404 data = bf.read()
405 bf.close()
406 return data
407 return ''
408
409 security.declarePrivate('update_data')
410 def update_data(self, file, content_type=None):
411 if isinstance(file, unicode):
412 raise TypeError('Data can only be str or file-like. '
413 'Unicode objects are expressly forbidden.')
414 elif isinstance(file, str) :
415 sio = StringIO()
416 sio.write(file)
417 sio.seek(0)
418 file = sio
419
420 if content_type is not None: self.content_type=content_type
421 self.save(file)
422 self.ZCacheable_invalidate()
423 self.ZCacheable_set(None)
424 self.http__refreshEtag()
425
426 security.declareProtected(change_images_and_files, 'manage_edit')
427 def manage_edit(self, title, content_type, precondition='',
428 filedata=None, REQUEST=None):
429 """
430 Changes the title and content type attributes of the File or Image.
431 """
432 if self.wl_isLocked():
433 raise ResourceLockedError, "File is locked via WebDAV"
434
435 self.title=str(title)
436 self.content_type=str(content_type)
437 if precondition: self.precondition=str(precondition)
438 elif self.precondition: del self.precondition
439 if filedata is not None:
440 self.update_data(filedata, content_type)
441 else:
442 self.ZCacheable_invalidate()
443 if REQUEST:
444 message="Saved changes."
445 return self.manage_main(self,REQUEST,manage_tabs_message=message)
446
447 security.declareProtected(change_images_and_files, 'manage_upload')
448 def manage_upload(self,file='',REQUEST=None):
449 """
450 Replaces the current contents of the File or Image object with file.
451
452 The file or images contents are replaced with the contents of 'file'.
453 """
454 if self.wl_isLocked():
455 raise ResourceLockedError, "File is locked via WebDAV"
456
457 content_type=self._get_content_type(file, self.__name__,
458 'application/octet-stream')
459 self.update_data(file, content_type)
460
461 if REQUEST:
462 message="Saved changes."
463 return self.manage_main(self,REQUEST,manage_tabs_message=message)
464
465 def _get_content_type(self, file, id, content_type=None):
466 headers=getattr(file, 'headers', None)
467 if headers and headers.has_key('content-type'):
468 content_type=headers['content-type']
469 else:
470 name = getattr(file, 'filename', self.uploaded_filename) or id
471 content_type, enc=guess_content_type(name, '', content_type)
472 return content_type
473
474 security.declareProtected(delete_objects, 'DELETE')
475
476 security.declareProtected(change_images_and_files, 'PUT')
477 def PUT(self, REQUEST, RESPONSE):
478 """Handle HTTP PUT requests"""
479 self.dav__init(REQUEST, RESPONSE)
480 self.dav__simpleifhandler(REQUEST, RESPONSE, refresh=1)
481 type=REQUEST.get_header('content-type', None)
482
483 file=REQUEST['BODYFILE']
484
485 content_type = self._get_content_type(file, self.__name__,
486 type or self.content_type)
487 self.update_data(file, content_type)
488
489 RESPONSE.setStatus(204)
490 return RESPONSE
491
492 security.declareProtected(View, 'get_size')
493 def get_size(self):
494 """Get the size of a file or image.
495
496 Returns the size of the file or image.
497 """
498 size=self.size
499 if size is None :
500 bf = self.open('r')
501 bf.seek(0,2)
502 self.size = size = bf.tell()
503 bf.close()
504 return size
505
506 # deprecated; use get_size!
507 getSize=get_size
508
509 security.declareProtected(View, 'getContentType')
510 def getContentType(self):
511 """Get the content type of a file or image.
512
513 Returns the content type (MIME type) of a file or image.
514 """
515 return self.content_type
516
517
518 def __str__(self): return str(self.data)
519 def __len__(self): return 1
520
521 security.declareProtected(ftp_access, 'manage_FTPstat')
522 security.declareProtected(ftp_access, 'manage_FTPlist')
523
524 security.declareProtected(ftp_access, 'manage_FTPget')
525 def manage_FTPget(self):
526 """Return body for ftp."""
527 RESPONSE = self.REQUEST.RESPONSE
528
529 if self.ZCacheable_isCachingEnabled():
530 result = self.ZCacheable_get(default=None)
531 if result is not None:
532 # We will always get None from RAMCacheManager but we will get
533 # something implementing the IStreamIterator interface
534 # from FileCacheManager.
535 # the content-length is required here by HTTPResponse, even
536 # though FTP doesn't use it.
537 RESPONSE.setHeader('Content-Length', self.size)
538 return result
539
540 bf = self.open('r')
541 data = bf.read()
542 bf.close()
543 RESPONSE.setBase(None)
544 return data
545
546 manage_addImageForm=DTMLFile('dtml/imageAdd',globals(),
547 Kind='Image',kind='image')
548 def manage_addImage(self, id, file, title='', precondition='', content_type='',
549 REQUEST=None):
550 """
551 Add a new Image object.
552
553 Creates a new Image object 'id' with the contents of 'file'.
554 """
555
556 id=str(id)
557 title=str(title)
558 content_type=str(content_type)
559 precondition=str(precondition)
560
561 id, title = cookId(id, title, file)
562
563 self=self.this()
564 self._setObject(id, Image(id,title,file,content_type, precondition))
565
566 if REQUEST is not None:
567 try: url=self.DestinationURL()
568 except: url=REQUEST['URL1']
569 REQUEST.RESPONSE.redirect('%s/manage_main' % url)
570 return id
571
572
573 def getImageInfo(file):
574 height = -1
575 width = -1
576 content_type = ''
577
578 # handle GIFs
579 data = file.read(24)
580 size = len(data)
581 if (size >= 10) and data[:6] in ('GIF87a', 'GIF89a'):
582 # Check to see if content_type is correct
583 content_type = 'image/gif'
584 w, h = struct.unpack("<HH", data[6:10])
585 width = int(w)
586 height = int(h)
587
588 # See PNG v1.2 spec (http://www.cdrom.com/pub/png/spec/)
589 # Bytes 0-7 are below, 4-byte chunk length, then 'IHDR'
590 # and finally the 4-byte width, height
591 elif ((size >= 24) and (data[:8] == '\211PNG\r\n\032\n')
592 and (data[12:16] == 'IHDR')):
593 content_type = 'image/png'
594 w, h = struct.unpack(">LL", data[16:24])
595 width = int(w)
596 height = int(h)
597
598 # Maybe this is for an older PNG version.
599 elif (size >= 16) and (data[:8] == '\211PNG\r\n\032\n'):
600 # Check to see if we have the right content type
601 content_type = 'image/png'
602 w, h = struct.unpack(">LL", data[8:16])
603 width = int(w)
604 height = int(h)
605
606 # handle JPEGs
607 elif (size >= 2) and (data[:2] == '\377\330'):
608 content_type = 'image/jpeg'
609 jpeg = file
610 jpeg.seek(0)
611 jpeg.read(2)
612 b = jpeg.read(1)
613 try:
614 while (b and ord(b) != 0xDA):
615 while (ord(b) != 0xFF): b = jpeg.read(1)
616 while (ord(b) == 0xFF): b = jpeg.read(1)
617 if (ord(b) >= 0xC0 and ord(b) <= 0xC3):
618 jpeg.read(3)
619 h, w = struct.unpack(">HH", jpeg.read(4))
620 break
621 else:
622 jpeg.read(int(struct.unpack(">H", jpeg.read(2))[0])-2)
623 b = jpeg.read(1)
624 width = int(w)
625 height = int(h)
626 except: pass
627
628 return content_type, width, height
629
630
631 class Image(File):
632 """Image objects can be GIF, PNG or JPEG and have the same methods
633 as File objects. Images also have a string representation that
634 renders an HTML 'IMG' tag.
635 """
636 __implements__ = (WriteLockInterface,)
637 meta_type='Blob Image'
638
639 security = ClassSecurityInfo()
640 security.declareObjectProtected(View)
641
642 alt=''
643 height=''
644 width=''
645
646 # FIXME: Redundant, already in base class
647 security.declareProtected(change_images_and_files, 'manage_edit')
648 security.declareProtected(change_images_and_files, 'manage_upload')
649 security.declareProtected(change_images_and_files, 'PUT')
650 security.declareProtected(View, 'index_html')
651 security.declareProtected(View, 'get_size')
652 security.declareProtected(View, 'getContentType')
653 security.declareProtected(ftp_access, 'manage_FTPstat')
654 security.declareProtected(ftp_access, 'manage_FTPlist')
655 security.declareProtected(ftp_access, 'manage_FTPget')
656 security.declareProtected(delete_objects, 'DELETE')
657
658 _properties=({'id':'title', 'type': 'string'},
659 {'id':'alt', 'type':'string'},
660 {'id':'content_type', 'type':'string','mode':'w'},
661 {'id':'height', 'type':'string'},
662 {'id':'width', 'type':'string'},
663 )
664
665 manage_options=(
666 ({'label':'Edit', 'action':'manage_main',
667 'help':('OFSP','Image_Edit.stx')},
668 {'label':'View', 'action':'view_image_or_file',
669 'help':('OFSP','Image_View.stx')},)
670 + PropertyManager.manage_options
671 + RoleManager.manage_options
672 + Item_w__name__.manage_options
673 + Cacheable.manage_options
674 )
675
676 manage_editForm =DTMLFile('dtml/imageEdit',globals(),
677 Kind='Image',kind='image')
678 manage_editForm._setName('manage_editForm')
679
680 security.declareProtected(View, 'view_image_or_file')
681 view_image_or_file =DTMLFile('dtml/imageView',globals())
682
683 security.declareProtected(view_management_screens, 'manage')
684 security.declareProtected(view_management_screens, 'manage_main')
685 manage=manage_main=manage_editForm
686 manage_uploadForm=manage_editForm
687
688 security.declarePrivate('update_data')
689 def update_data(self, file, content_type=None):
690 super(Image, self).update_data(file, content_type)
691 self.updateFormat(size=self.size, content_type=content_type)
692
693 security.declarePrivate('updateFormat')
694 def updateFormat(self, size=None, dimensions=None, content_type=None):
695 self.updateSize(size=size)
696
697 if dimensions is None or content_type is None :
698 bf = self.open('r')
699 ct, width, height = getImageInfo(bf)
700 bf.close()
701 if ct:
702 content_type = ct
703 if width >= 0 and height >= 0:
704 self.width = width
705 self.height = height
706
707 # Now we should have the correct content type, or still None
708 if content_type is not None: self.content_type = content_type
709 else :
710 self.width, self.height = dimensions
711 self.content_type = content_type
712
713 def __str__(self):
714 return self.tag()
715
716 security.declareProtected(View, 'tag')
717 def tag(self, height=None, width=None, alt=None,
718 scale=0, xscale=0, yscale=0, css_class=None, title=None, **args):
719 """
720 Generate an HTML IMG tag for this image, with customization.
721 Arguments to self.tag() can be any valid attributes of an IMG tag.
722 'src' will always be an absolute pathname, to prevent redundant
723 downloading of images. Defaults are applied intelligently for
724 'height', 'width', and 'alt'. If specified, the 'scale', 'xscale',
725 and 'yscale' keyword arguments will be used to automatically adjust
726 the output height and width values of the image tag.
727
728 Since 'class' is a Python reserved word, it cannot be passed in
729 directly in keyword arguments which is a problem if you are
730 trying to use 'tag()' to include a CSS class. The tag() method
731 will accept a 'css_class' argument that will be converted to
732 'class' in the output tag to work around this.
733 """
734 if height is None: height=self.height
735 if width is None: width=self.width
736
737 # Auto-scaling support
738 xdelta = xscale or scale
739 ydelta = yscale or scale
740
741 if xdelta and width:
742 width = str(int(round(int(width) * xdelta)))
743 if ydelta and height:
744 height = str(int(round(int(height) * ydelta)))
745
746 result='<img src="%s"' % (self.absolute_url())
747
748 if alt is None:
749 alt=getattr(self, 'alt', '')
750 result = '%s alt="%s"' % (result, escape(alt, 1))
751
752 if title is None:
753 title=getattr(self, 'title', '')
754 result = '%s title="%s"' % (result, escape(title, 1))
755
756 if height:
757 result = '%s height="%s"' % (result, height)
758
759 if width:
760 result = '%s width="%s"' % (result, width)
761
762 # Omitting 'border' attribute (Collector #1557)
763 # if not 'border' in [ x.lower() for x in args.keys()]:
764 # result = '%s border="0"' % result
765
766 if css_class is not None:
767 result = '%s class="%s"' % (result, css_class)
768
769 for key in args.keys():
770 value = args.get(key)
771 if value:
772 result = '%s %s="%s"' % (result, key, value)
773
774 return '%s />' % result
775
776
777 def cookId(id, title, file):
778 if not id and hasattr(file,'filename'):
779 filename=file.filename
780 title=title or filename
781 id=filename[max(filename.rfind('/'),
782 filename.rfind('\\'),
783 filename.rfind(':'),
784 )+1:]
785 return id, title
786
787 #class Pdata(Persistent, Implicit):
788 # # Wrapper for possibly large data
789 #
790 # next=None
791 #
792 # def __init__(self, data):
793 # self.data=data
794 #
795 # def __getslice__(self, i, j):
796 # return self.data[i:j]
797 #
798 # def __len__(self):
799 # data = str(self)
800 # return len(data)
801 #
802 # def __str__(self):
803 # next=self.next
804 # if next is None: return self.data
805 #
806 # r=[self.data]
807 # while next is not None:
808 # self=next
809 # r.append(self.data)
810 # next=self.next
811 #
812 # return ''.join(r)