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