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