Oubli : ajout de la notification d'objet modifié, après upload.
[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 notify(ObjectModifiedEvent(self))
491
492 if REQUEST:
493 message="Saved changes."
494 return self.manage_main(self,REQUEST,manage_tabs_message=message)
495
496 def _get_content_type(self, file, id, content_type=None):
497 headers=getattr(file, 'headers', None)
498 if headers and headers.has_key('content-type'):
499 content_type=headers['content-type']
500 else:
501 name = getattr(file, 'filename', self.uploaded_filename) or id
502 content_type, enc=guess_content_type(name, '', content_type)
503 return content_type
504
505 security.declareProtected(delete_objects, 'DELETE')
506
507 security.declareProtected(change_images_and_files, 'PUT')
508 def PUT(self, REQUEST, RESPONSE):
509 """Handle HTTP PUT requests"""
510 self.dav__init(REQUEST, RESPONSE)
511 self.dav__simpleifhandler(REQUEST, RESPONSE, refresh=1)
512 type=REQUEST.get_header('content-type', None)
513
514 file=REQUEST['BODYFILE']
515
516 content_type = self._get_content_type(file, self.__name__,
517 type or self.content_type)
518 self.update_data(file, content_type)
519
520 RESPONSE.setStatus(204)
521 return RESPONSE
522
523 security.declareProtected(View, 'get_size')
524 def get_size(self):
525 """Get the size of a file or image.
526
527 Returns the size of the file or image.
528 """
529 size=self.size
530 if size is None :
531 bf = self.open('r')
532 bf.seek(0,2)
533 self.size = size = bf.tell()
534 bf.close()
535 return size
536
537 # deprecated; use get_size!
538 getSize=get_size
539
540 security.declareProtected(View, 'getContentType')
541 def getContentType(self):
542 """Get the content type of a file or image.
543
544 Returns the content type (MIME type) of a file or image.
545 """
546 return self.content_type
547
548
549 def __str__(self): return str(self.data)
550 def __len__(self): return 1
551
552 security.declareProtected(ftp_access, 'manage_FTPstat')
553 security.declareProtected(ftp_access, 'manage_FTPlist')
554
555 security.declareProtected(ftp_access, 'manage_FTPget')
556 def manage_FTPget(self):
557 """Return body for ftp."""
558 RESPONSE = self.REQUEST.RESPONSE
559
560 if self.ZCacheable_isCachingEnabled():
561 result = self.ZCacheable_get(default=None)
562 if result is not None:
563 # We will always get None from RAMCacheManager but we will get
564 # something implementing the IStreamIterator interface
565 # from FileCacheManager.
566 # the content-length is required here by HTTPResponse, even
567 # though FTP doesn't use it.
568 RESPONSE.setHeader('Content-Length', self.size)
569 return result
570
571 bf = self.open('r')
572 data = bf.read()
573 bf.close()
574 RESPONSE.setBase(None)
575 return data
576
577 manage_addImageForm=DTMLFile('dtml/imageAdd',globals(),
578 Kind='Image',kind='image')
579 def manage_addImage(self, id, file, title='', precondition='', content_type='',
580 REQUEST=None):
581 """
582 Add a new Image object.
583
584 Creates a new Image object 'id' with the contents of 'file'.
585 """
586
587 id=str(id)
588 title=str(title)
589 content_type=str(content_type)
590 precondition=str(precondition)
591
592 id, title = cookId(id, title, file)
593
594 self=self.this()
595 self._setObject(id, Image(id,title,file,content_type, precondition))
596
597 newFile = self._getOb(id)
598 notify(ObjectCreatedEvent(newFile))
599
600 if REQUEST is not None:
601 try: url=self.DestinationURL()
602 except: url=REQUEST['URL1']
603 REQUEST.RESPONSE.redirect('%s/manage_main' % url)
604 return id
605
606
607 def getImageInfo(file):
608 height = -1
609 width = -1
610 content_type = ''
611
612 # handle GIFs
613 data = file.read(24)
614 size = len(data)
615 if (size >= 10) and data[:6] in ('GIF87a', 'GIF89a'):
616 # Check to see if content_type is correct
617 content_type = 'image/gif'
618 w, h = struct.unpack("<HH", data[6:10])
619 width = int(w)
620 height = int(h)
621
622 # See PNG v1.2 spec (http://www.cdrom.com/pub/png/spec/)
623 # Bytes 0-7 are below, 4-byte chunk length, then 'IHDR'
624 # and finally the 4-byte width, height
625 elif ((size >= 24) and (data[:8] == '\211PNG\r\n\032\n')
626 and (data[12:16] == 'IHDR')):
627 content_type = 'image/png'
628 w, h = struct.unpack(">LL", data[16:24])
629 width = int(w)
630 height = int(h)
631
632 # Maybe this is for an older PNG version.
633 elif (size >= 16) and (data[:8] == '\211PNG\r\n\032\n'):
634 # Check to see if we have the right content type
635 content_type = 'image/png'
636 w, h = struct.unpack(">LL", data[8:16])
637 width = int(w)
638 height = int(h)
639
640 # handle JPEGs
641 elif (size >= 2) and (data[:2] == '\377\330'):
642 content_type = 'image/jpeg'
643 jpeg = file
644 jpeg.seek(0)
645 jpeg.read(2)
646 b = jpeg.read(1)
647 try:
648 while (b and ord(b) != 0xDA):
649 while (ord(b) != 0xFF): b = jpeg.read(1)
650 while (ord(b) == 0xFF): b = jpeg.read(1)
651 if (ord(b) >= 0xC0 and ord(b) <= 0xC3):
652 jpeg.read(3)
653 h, w = struct.unpack(">HH", jpeg.read(4))
654 break
655 else:
656 jpeg.read(int(struct.unpack(">H", jpeg.read(2))[0])-2)
657 b = jpeg.read(1)
658 width = int(w)
659 height = int(h)
660 except: pass
661
662 return content_type, width, height
663
664
665 class Image(File):
666 """Image objects can be GIF, PNG or JPEG and have the same methods
667 as File objects. Images also have a string representation that
668 renders an HTML 'IMG' tag.
669 """
670 meta_type='Blob Image'
671
672 security = ClassSecurityInfo()
673 security.declareObjectProtected(View)
674
675 alt=''
676 height=''
677 width=''
678
679 # FIXME: Redundant, already in base class
680 security.declareProtected(change_images_and_files, 'manage_edit')
681 security.declareProtected(change_images_and_files, 'manage_upload')
682 security.declareProtected(change_images_and_files, 'PUT')
683 security.declareProtected(View, 'index_html')
684 security.declareProtected(View, 'get_size')
685 security.declareProtected(View, 'getContentType')
686 security.declareProtected(ftp_access, 'manage_FTPstat')
687 security.declareProtected(ftp_access, 'manage_FTPlist')
688 security.declareProtected(ftp_access, 'manage_FTPget')
689 security.declareProtected(delete_objects, 'DELETE')
690
691 _properties=({'id':'title', 'type': 'string'},
692 {'id':'alt', 'type':'string'},
693 {'id':'content_type', 'type':'string','mode':'w'},
694 {'id':'height', 'type':'string'},
695 {'id':'width', 'type':'string'},
696 )
697
698 manage_options=(
699 ({'label':'Edit', 'action':'manage_main',
700 'help':('OFSP','Image_Edit.stx')},
701 {'label':'View', 'action':'view_image_or_file',
702 'help':('OFSP','Image_View.stx')},)
703 + PropertyManager.manage_options
704 + RoleManager.manage_options
705 + Item_w__name__.manage_options
706 + Cacheable.manage_options
707 )
708
709 manage_editForm =DTMLFile('dtml/imageEdit',globals(),
710 Kind='Image',kind='image')
711 manage_editForm._setName('manage_editForm')
712
713 security.declareProtected(View, 'view_image_or_file')
714 view_image_or_file =DTMLFile('dtml/imageView',globals())
715
716 security.declareProtected(view_management_screens, 'manage')
717 security.declareProtected(view_management_screens, 'manage_main')
718 manage=manage_main=manage_editForm
719 manage_uploadForm=manage_editForm
720
721 security.declarePrivate('update_data')
722 def update_data(self, file, content_type=None):
723 super(Image, self).update_data(file, content_type)
724 self.updateFormat(size=self.size, content_type=content_type)
725
726 security.declarePrivate('updateFormat')
727 def updateFormat(self, size=None, dimensions=None, content_type=None):
728 self.updateSize(size=size)
729
730 if dimensions is None or content_type is None :
731 bf = self.open('r')
732 ct, width, height = getImageInfo(bf)
733 bf.close()
734 if ct:
735 content_type = ct
736 if width >= 0 and height >= 0:
737 self.width = width
738 self.height = height
739
740 # Now we should have the correct content type, or still None
741 if content_type is not None: self.content_type = content_type
742 else :
743 self.width, self.height = dimensions
744 self.content_type = content_type
745
746 def __str__(self):
747 return self.tag()
748
749 security.declareProtected(View, 'tag')
750 def tag(self, height=None, width=None, alt=None,
751 scale=0, xscale=0, yscale=0, css_class=None, title=None, **args):
752 """
753 Generate an HTML IMG tag for this image, with customization.
754 Arguments to self.tag() can be any valid attributes of an IMG tag.
755 'src' will always be an absolute pathname, to prevent redundant
756 downloading of images. Defaults are applied intelligently for
757 'height', 'width', and 'alt'. If specified, the 'scale', 'xscale',
758 and 'yscale' keyword arguments will be used to automatically adjust
759 the output height and width values of the image tag.
760
761 Since 'class' is a Python reserved word, it cannot be passed in
762 directly in keyword arguments which is a problem if you are
763 trying to use 'tag()' to include a CSS class. The tag() method
764 will accept a 'css_class' argument that will be converted to
765 'class' in the output tag to work around this.
766 """
767 if height is None: height=self.height
768 if width is None: width=self.width
769
770 # Auto-scaling support
771 xdelta = xscale or scale
772 ydelta = yscale or scale
773
774 if xdelta and width:
775 width = str(int(round(int(width) * xdelta)))
776 if ydelta and height:
777 height = str(int(round(int(height) * ydelta)))
778
779 result='<img src="%s"' % (self.absolute_url())
780
781 if alt is None:
782 alt=getattr(self, 'alt', '')
783 result = '%s alt="%s"' % (result, escape(alt, 1))
784
785 if title is None:
786 title=getattr(self, 'title', '')
787 result = '%s title="%s"' % (result, escape(title, 1))
788
789 if height:
790 result = '%s height="%s"' % (result, height)
791
792 if width:
793 result = '%s width="%s"' % (result, width)
794
795 # Omitting 'border' attribute (Collector #1557)
796 # if not 'border' in [ x.lower() for x in args.keys()]:
797 # result = '%s border="0"' % result
798
799 if css_class is not None:
800 result = '%s class="%s"' % (result, css_class)
801
802 for key in args.keys():
803 value = args.get(key)
804 if value:
805 result = '%s %s="%s"' % (result, key, value)
806
807 return '%s />' % result
808
809
810 def cookId(id, title, file):
811 if not id and hasattr(file,'filename'):
812 filename=file.filename
813 title=title or filename
814 id=filename[max(filename.rfind('/'),
815 filename.rfind('\\'),
816 filename.rfind(':'),
817 )+1:]
818 return id, title