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