Commit 4b448f8a authored by Jean-Paul Smets's avatar Jean-Paul Smets

Early refactoring of document related classes.

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@11807 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 35f0ca7a
......@@ -31,27 +31,158 @@ from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5Type.WebDAVSupport import TextContent
from DateTime import DateTime
class Document(XMLObject, TextContent):
def makeSortedTuple(kw):
items = kw.items()
items.sort()
return tuple(items)
class ConversionCacheMixin:
"""
This class provides a generic API to store in the ZODB
various converted versions of a file or of a string.
TODO:
* Implement ZODB BLOB
"""
# time of generation of various formats
_cached_time = {}
# generated files (cache)
_cached_data = {}
# mime types for cached formats XXX to be refactored
_cached_mime = {}
# Declarative security
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
security.declareProtected(Permissions.ModifyPortalContent, 'clearConversionCache')
def clearConversionCache(self):
"""
Clear cache (invoked by interaction workflow upon file upload
needed here to overwrite class attribute with instance attrs
"""
self._cached_time = {}
self._cached_data = {}
self._cached_mime = {}
security.declareProtected(Permissions.View, 'hasConversion')
def hasConversion(self, **format):
"""
Checks whether we have a version in this format
"""
return self._cached_data.has_key(makeSortedTuple(format))
def getCacheTime(self, **format):
"""
Checks when if ever was the file produced
"""
return self._cached_time.get(makeSortedTuple(format), 0)
def updateConversion(self, **format):
self._cached_time[makeSortedTuple(format)] = DateTime()
def setConversion(self, data, mime=None, **format):
tformat = makeSortedTuple(format)
if mime is not None:
self._cached_mime[tformat] = mime
if data is not None:
self._cached_data[tformat] = data
self.updateConversion(format = format)
self._p_changed = 1
def getConversion(self, **format):
'''
we could be much cooler here - pass testing and updating methods to this function
so that it does it all by itself; this'd eliminate the need for cacheSet public method
'''
tformat = makeSortedTuple(format)
return self._cached_mime.get(tformat, ''), self._cached_data.get(tformat, '')
security.declareProtected(Permissions.View, 'getConversionCacheInfo')
def getConversionCacheInfo(self):
"""
Get cache details as string (for debugging)
"""
A Document can contain text that can be formatted using
*Structured Text* or *HTML*. Text can be automatically translated
through the use of 'message catalogs'.
Document inherits from XMLObject and can
be synchronized accross multiple sites.
Version Management: the notion of version depends on the
type of application. For example, in the case (1) of Transformation
(BOM), all versions are considered as equal and may be kept
indefinitely for both archive and usage purpose. In the case (2)
of Person data, the new version replaces the previous one
in place and is not needed for archive. In the case (3) of
a web page, the new version replaces the previous one,
the previous one being kept in place for archive.
s = 'CACHE INFO:<br/><table><tr><td>format</td><td>size</td><td>time</td><td>is changed</td></tr>'
#self.log('getCacheInfo',self.cached_time)
#self.log('getCacheInfo',self.cached_data)
for f in self._cached_time.keys():
t = self._cached_time[f]
data = self._cached_data.get(f)
if data:
if isinstance(data, str):
ln = len(data)
else:
ln = 0
while data is not None:
ln += len(data.data)
data = data.next
else:
ln = 'no data!!!'
s += '<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>' % (f, str(ln), str(t), '-')
s += '</table>'
return s
class Document(XMLObject):
"""
Document is an abstract class with all methods
related to document management in ERP5. This includes
searchable text, explicit relations, implicit relations,
metadata, versions, languages, etc.
There currently two types of Document subclasses:
* File for binary file based documents. File
has subclasses such as Image, OOoDocument,
PDFDocument, etc. to implement specific conversion
methods
* TextDocument for text based documents. TextDocument
has subclasses such as Wiki to implement specific
methods
Document classes which implement conversion should use
the CachingMixin class so that converted values are
stored.
The Document class behaviour can be extended through scripts.
* Document_discoverMetadata (DMS_ingestFile)
finds all metadata or uses the metadata which was
provided as parameter. Document_discoverMetadata should
be overloaded if necessary for some classes
(ex. TextDocument_discoverMetadata, Image_discoverMetadata)
and should be called through a single API discoverMetadata()
Consider using _getTypeBasedMethod for implementation
* Document_ingestFile (Document_uploadFile)
is called for http based ingestion and itself calls
Document_discoverMetadata. Many parameters may be
passed to Document_ingest through an
online form.
* Document_ingestEmail is called for email based
ingestion and itself calls Document_ingestFile.
Document_ingestEmail is in charge of parsing email
to extract metadata before calling Document_ingestFile.
* PUT is called for DAV/FTP based ingestion directly from the class.
It itself calls Document_discoverMetadata.
Custom scripts for automatic classification:
* Document_findWikiPredecessorList finds a list of documents
which are referencing us.
Should this be merged with WebSite_getDocumentValue ? XXX
* Document_findWikiSuccessor tries to find a document matching with
a given regexp.
Should this be merged with WebSite_getDocumentValue ? XXX
Subcontent: documents may include subcontent (files, images, etc.)
so that publication of rich content can be path independent
so that publication of rich content can be path independent.
"""
meta_type = 'ERP5 Document'
......@@ -59,6 +190,7 @@ class Document(XMLObject, TextContent):
add_permission = Permissions.AddPortalContent
isPortalContent = 1
isRADContent = 1
isDocument = 1
# Declarative security
security = ClassSecurityInfo()
......@@ -76,19 +208,102 @@ class Document(XMLObject, TextContent):
# Declarative interfaces
__implements__ = ()
# Patch
PUT = TextContent.PUT
searchable_property_list = ('title', 'description', 'id', 'reference',
'version', 'short_title', 'keywords',
'subject', 'source_reference', 'source_project_title')
# What is keywords ?
# XXX-JPS This is a plural
# XXX-JPS subject_list would be better than subject in this case
# and the getSearchableText should be able to process lists
# Same for source_reference_list, source_project_title_list
### Content indexing methods
security.declareProtected(Permissions.View, 'getSearchableText')
def getSearchableText(self, md=None):
"""\
Used by the catalog for basic full text indexing
We should try to do some kind of file conversion here
"""
searchable_text = "%s %s %s %s" % (self.getTitle(), self.getDescription(),
self.getId(), self.getTextContent())
return searchable_text
# Compatibility with CMF Catalog / CPS sites
SearchableText = getSearchableText
"""
Used by the catalog for basic full text indexing.
XXX-JPS - This method is nice. It should probably be moved to Base class
searchable_property_list could become a standard class attribute.
TODO (future): Make this property a per portal type property.
"""
searchable_text = ' '.join(map(lambda x: self.getProperty(x) or ' ',self.searchable_property_list))
return searchable_text
# Compatibility with CMF Catalog
SearchableText = getSearchableText # XXX-JPS - Here wa have a security issue - ask seb what to do
security.declareProtected(Permissions.ModifyPortalContent, 'setPropertyListFromFilename')
def setPropertyListFromFilename(self, fname):
"""
XXX-JPS missing description
"""
rx_src = self.portal_preferences.getPreferredDocumentFilenameRegexp()
if rx_src:
rx_parse = re.compile()
if rx_parse is None:
self.setReference(fname) # XXX-JPS please use _setReference to prevent reindexing all the time
return
m = rx_parse.match(fname)
if m is None:
self.setReference(fname) # XXX-JPS please use _setReference to prevent reindexing all the time
return
for k,v in m.groupdict().items():
self.setProperty(k,v) # XXX-JPS please use _setProperty to prevent reindexing all the time
# XXX-JPS finally call self.reindexObject()
else:
# If no regexp defined, we use the file name as reference
# this is the failover behaviour
self.setReference(fname)
security.declareProtected(Permissions.View, 'getWikiSuccessorReferenceList')
def getWikiSuccessorReferenceList(self):
"""
find references in text_content, return matches
with this we can then find objects
"""
if self.getTextContent() is None:
return []
rx_search = re.compile(self.portal_preferences.getPreferredDocumentReferenceRegexp()) # XXX-JPS Safe ? Better error required ?
try:
res = rx_search.finditer(self.getTextContent())
except AttributeError:
return []
res = [(r.group(),r.groupdict()) for r in res]
return res
security.declareProtected(Permissions.View, 'getWikiSuccessorValueList')
def getWikiSuccessorValueList(self):
"""
XXX-JPS Put a description then add notes (notes only is not enough)
getWikiSuccessorValueList - the way to find objects is on
implementation level
"""
# XXX results should be cached as volatile attributes
# XXX-JPS - Please use TransactionCache in ERP5Type for this
# TransactionCache does all the work for you
lst = []
for ref in self.getWikiSuccessorReferenceList():
r = ref[1]
res = self.Document_findWikiSuccessor(**r)
if len(res)>0:
lst.append(res[0].getObject())
return lst
security.declareProtected(Permissions.View, 'getWikiPredecessorValueList')
def getWikiPredecessorValueList(self):
"""
XXX-JPS Put a description then add notes (notes only is not enough)
it is mostly implementation level - depends on what parameters we use to identify
document, and on how a doc must reference me to be my predecessor (reference only,
or with a language, etc
"""
# XXX results should be cached as volatile attributes
lst = self.Document_findWikiPredecessorList()
lst = [r.getObject() for r in lst]
di = dict.fromkeys(lst) # make it unique
ref = self.getReference()
return [o for o in di.keys() if o.getReference() != ref] # every object has its own reference in SearchableText
......@@ -30,86 +30,165 @@ from AccessControl import ClassSecurityInfo
from Products.CMFCore.WorkflowCore import WorkflowMethod
from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5Type.Cache import CachingMethod
from Products.ERP5.Document.Document import Document
from Products.ERP5Type.Base import Base
from Products.CMFDefault.File import File as CMFFile
from zLOG import LOG
class File(XMLObject, CMFFile):
import mimetypes, re
from DateTime import DateTime
mimetypes.init()
rs=[]
rs.append(re.compile('<HEAD>.*</HEAD>',re.DOTALL|re.MULTILINE|re.IGNORECASE))
rs.append(re.compile('<!DOCTYPE[^>]*>'))
rs.append(re.compile('<.?(HTML|BODY)[^>]*>',re.DOTALL|re.MULTILINE|re.IGNORECASE))
def stripHtml(txt): # XXX-JPS to be moved to TextDocument
for r in rs:
txt=r.sub('',txt)
return txt
class File(Document, CMFFile):
"""
A File can contain raw data which can be uploaded and downloaded.
It is the root class of Image, OOoDocument (ERP5OOo product),
etc. The main purpose of the File class is to handle efficiently
large files. It uses Pdata from OFS.File for this purpose.
File inherits from XMLObject and can be synchronized
accross multiple sites.
Subcontent: File can only contain role information.
TODO:
* make sure ZODB BLOBS are supported to prevent
feeding the ZODB cache with unnecessary large data
"""
meta_type = 'ERP5 File'
portal_type = 'File'
add_permission = Permissions.AddPortalContent
isPortalContent = 1
isRADContent = 1
__dav_collection__=0
# Declarative security
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
# Default global values
content_type = '' # Required for WebDAV support (default value)
# Default Properties
property_sheets = ( PropertySheet.Base
, PropertySheet.CategoryCore
, PropertySheet.DublinCore
, PropertySheet.Version
, PropertySheet.Reference
, PropertySheet.Document
, PropertySheet.Data
)
# Declarative interfaces
#__implements__ = ( , )
### Special edit method
security.declarePrivate( '_edit' )
def _edit(self, **kw):
"""\
This is used to edit files
"""
A File can contain text that can be formatted using
*Structured Text* or *HTML*. Text can be automatically translated
through the use of 'message catalogs'.
if kw.has_key('file'):
file = kw.get('file')
precondition = kw.get('precondition')
if self._isNotEmpty(file):
CMFFile._edit(self, precondition=precondition, file=file)
del kw['file']
Base._edit(self, **kw)
security.declareProtected( Permissions.ModifyPortalContent, 'edit' )
edit = WorkflowMethod( _edit )
# Copy support needs to be implemented by ExtFile
################################
# Special management methods #
################################
def manage_afterClone(self, item):
Base.manage_afterClone(self, item)
CMFFile.manage_afterClone(self, item)
def manage_afterAdd(self, item, container):
Base.manage_afterAdd(self, item, container)
CMFFile.manage_afterAdd(self, item, container)
def manage_beforeDelete(self, item, container):
CMFFile.manage_beforeDelete(self, item, container)
def get_size(self):
"""
has to be overwritten here, otherwise WebDAV fails
"""
try:
return len(self.data)
except (AttributeError, TypeError):
return 0
File can only contain role information.
getcontentlength = get_size
File inherits from XMLObject and can
be synchronized accross multiple sites.
security.declareProtected(Permissions.View, 'hasFile')
def hasFile(self):
"""
Checks whether we have an initial file
"""
_marker = []
if getattr(self,'data', _marker) is not _marker: # XXX-JPS - use propertysheet accessors
return getattr(self,'data') is not None
return False
meta_type = 'ERP5 File'
portal_type = 'File'
add_permission = Permissions.AddPortalContent
isPortalContent = 1
isRADContent = 1
# Declarative security
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
# Default global values
content_type = '' # Required for WebDAV support (default value)
# Declarative properties
property_sheets = ( PropertySheet.Base
, PropertySheet.CategoryCore
, PropertySheet.DublinCore
, PropertySheet.Data
)
# Declarative interfaces
#__implements__ = ( , )
### Special edit method
security.declarePrivate( '_edit' )
def _edit(self, **kw):
"""\
This is used to edit files
"""
if kw.has_key('file'):
file = kw.get('file')
precondition = kw.get('precondition')
if self._isNotEmpty(file):
CMFFile._edit(self, precondition=precondition, file=file)
del kw['file']
Base._edit(self, **kw)
security.declareProtected( Permissions.ModifyPortalContent, 'edit' )
edit = WorkflowMethod( _edit )
# Copy support needs to be implemented by ExtFile
################################
# Special management methods #
################################
def manage_afterClone(self, item):
Base.manage_afterClone(self, item)
CMFFile.manage_afterClone(self, item)
def manage_afterAdd(self, item, container):
Base.manage_afterAdd(self, item, container)
CMFFile.manage_afterAdd(self, item, container)
def manage_beforeDelete(self, item, container):
CMFFile.manage_beforeDelete(self, item, container)
# DAV Support
index_html = CMFFile.index_html
PUT = CMFFile.PUT
security.declareProtected('FTP access', 'manage_FTPget', 'manage_FTPstat', 'manage_FTPlist')
manage_FTPget = CMFFile.manage_FTPget
manage_FTPlist = CMFFile.manage_FTPlist
manage_FTPstat = CMFFile.manage_FTPstat
security.declarePrivate('_unpackData')
def _unpackData(self,data):
"""
Unpack Pdata into string
"""
if isinstance(data, str):
return data
else:
data_list = []
while data is not None:
data_list.append(data.data)
data=data.next
return ''.join(data_list)
security.declareProtected(Permissions.ModifyPortalContent, 'guessMimeType')
def guessMimeType(self, fname=''):
"""
get mime type from file name
"""
if fname == '': fname = self.getOriginalFilename()
if fname:
content_type,enc = mimetypes.guess_type(fname)
if content_type is not None:
self.content_type = content_type
return content_type
security.declareProtected(Permissions.ModifyPortalContent,'PUT')
def PUT(self,REQUEST,RESPONSE):
CMFFile.PUT(self,REQUEST,RESPONSE)
self.DMS_ingestFile(fname=self.getId()) # XXX-JPS we should call here Document_discoverMetadata
# with the filename as parameter
# DAV Support
index_html = CMFFile.index_html # XXX-JPS - Here we have a security issue - ask seb what to do
PUT = CMFFile.PUT # XXX-JPS - Here we have a security issue - ask seb what to do
security.declareProtected('FTP access', 'manage_FTPget', 'manage_FTPstat', 'manage_FTPlist')
manage_FTPget = CMFFile.manage_FTPget # XXX-JPS - Here we have a security issue - ask seb what to do
manage_FTPlist = CMFFile.manage_FTPlist # XXX-JPS - Here we have a security issue - ask seb what to do
manage_FTPstat = CMFFile.manage_FTPstat # XXX-JPS - Here we have a security issue - ask seb what to do
# vim: syntax=python shiftwidth=2
......@@ -28,119 +28,340 @@
from AccessControl import ClassSecurityInfo
from Products.CMFCore.WorkflowCore import WorkflowMethod
from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
from Products.ERP5Type.Base import Base
from Products.Photo.Photo import Photo
from Products.ERP5.Document.File import File
from Products.ERP5.Document.Document import ConversionCacheMixin
from OFS.Image import Image as OFSImage
from OFS.Image import getImageInfo
from OFS.content_types import guess_content_type
import string, time, sys
from cStringIO import StringIO
from zLOG import LOG
class Image (Base, Photo):
"""
An Image can contain text that can be formatted using
*Structured Text* or *HTML*. Text can be automatically translated
through the use of 'message catalogs'.
# XXX This should be move to preferences
defaultdisplays = {'thumbnail' : (128,128),
'xsmall' : (200,200),
'small' : (320,320),
'medium' : (480,480),
'large' : (768,768),
'xlarge' : (1024,1024)
}
Image can only contain role information.
default_formats = ['jpg', 'jpeg', 'png', 'gif', 'pnm', 'ppm']
Image inherits from XMLObject and can
be synchronized accross multiple sites.
class Image(File, OFSImage, ConversionCacheMixin):
"""
An Image is a File which contains image data. It supports
various conversions of format, size, resolution through
imagemagick. imagemagick was preferred due to its ability
to support PDF files (incl. Adobe Illustrator) which make
it very useful in the context of a graphic design shop.
Image inherits from XMLObject and can be synchronized
accross multiple sites.
Subcontent: Image can only contain role information.
TODO:
* extend Image to support more image file formats,
including Xara Xtreme (http://www.xaraxtreme.org/)
* include upgrade methods so that previous images
in ERP5 get upgraded automatically to new class
"""
# CMF Type Definition
meta_type = 'ERP5 Image'
portal_type = 'Image'
add_permission = Permissions.AddPortalContent
isPortalContent = 1
isRADContent = 1
# Default attribute values
width = 0
height = 0
# Declarative security
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
# Declarative properties
# Default Properties
property_sheets = ( PropertySheet.Base
, PropertySheet.CategoryCore
, PropertySheet.DublinCore
, PropertySheet.Version
, PropertySheet.Reference
, PropertySheet.Document
, PropertySheet.Data
)
def __init__( self, id, title='', file='', store='Image'
, engine='ImageMagick', quality=75, pregen=0, timeout=0):
Photo.__init__(self, id=id, title=title, file=file, store=store
, engine=engine, quality=quality, pregen=pregen, timeout=timeout)
Base.__init__(self, id=id)
self._data = ''
self.store = store
self._checkOriginal()
### Special edit method
def _checkOriginal(self):
if not hasattr(self, '_original'):
if self.store == 'Image' : from Products.Photo.PhotoImage import PhotoImage
elif self.store == 'ExtImage': from Products.Photo.ExtPhotoImage import PhotoImage
self._original = PhotoImage(self.id, self.title, path=self.absolute_url(1))
security.declarePrivate('_setFile')
def _setFile(self, file):
#
# Original photo attributes
#
def _update_image_info(self):
"""
This is used to set files
This method tries to determine the content type of an image and
its geometry. It uses currently OFS.Image for this purpose.
However, this method is known to be too simplistic.
TODO:
- use image magick or PIL
"""
content_type, width, height = getImageInfo(self.data)
self.height = height
self.width = width
self._setContentType(content_type)
security.declareProtected(Permissions.AccessContentsInformation, 'getWidth')
def getWidth(self):
"""
Tries to get the width from the image data.
"""
Photo.manage_editPhoto(self, file=file)
self.manage_purgeDisplays()
if self.get_size() and not self.width: self._update_image_info()
return self.width
security.declarePrivate('_edit')
def _edit(self, **kw):
security.declareProtected(Permissions.AccessContentsInformation, 'getHeight')
def getHeight(self):
"""
This is used to edit files
Tries to get the height from the image data.
"""
self._checkOriginal()
if kw.has_key('file'):
file = kw.get('file')
precondition = kw.get('precondition')
self._setFile(file)
del kw['file']
Base._edit(self, **kw)
if self.get_size() and not self.height: self._update_image_info()
return self.height
security.declareProtected(Permissions.AccessContentsInformation, 'getContentType')
def getContentType(self, format=''):
"""Original photo content_type."""
if format == '':
return self._baseGetContentType()
else:
return guess_content_type('myfile.' + format)[0]
#
# Photo display methods
#
security.declareProtected('View', 'tag')
def tag(self, display=None, height=None, width=None, cookie=0,
alt=None, css_class=None, format='', quality=75,
resolution=None, **kw):
"""Return HTML img tag."""
# Get cookie if display is not specified.
if display is None:
display = self.REQUEST.cookies.get('display', None)
# display may be set from a cookie.
if display is not None and defaultdisplays.has_key(display):
if not self.hasConversion(display=display, format=format,
quality=quality, resolution=resolution):
# Generate photo on-the-fly
self._makeDisplayPhoto(display, 1, format=format, quality=quality, resolution=resolution)
mime, image = self.getConversion(display=display, format=format,
quality=quality ,resolution=resolution)
width, height = (image.width, image.height)
# Set cookie for chosen size
if cookie:
self.REQUEST.RESPONSE.setCookie('display', display, path="/")
else:
# TODO: Add support for on-the-fly resize?
height = self.getHeight()
width = self.getWidth()
if display:
result = '<img src="%s?display=%s"' % (self.absolute_url(), display)
else:
result = '<img src="%s"' % (self.absolute_url())
if alt is None:
alt = getattr(self, 'title', '')
if alt == '':
alt = self.getId()
result = '%s alt="%s"' % (result, html_quote(alt))
if height:
result = '%s height="%s"' % (result, height)
if width:
result = '%s width="%s"' % (result, width)
if not 'border' in map(string.lower, kw.keys()):
result = '%s border="0"' % (result)
if css_class is not None:
result = '%s class="%s"' % (result, css_class)
for key in kw.keys():
value = kw.get(key)
result = '%s %s="%s"' % (result, key, value)
result = '%s />' % (result)
return result
def __str__(self):
return self.tag()
security.declareProtected('Access contents information', 'displayIds')
def displayIds(self, exclude=('thumbnail',)):
"""Return list of display Ids."""
ids = defaultdisplays.keys()
# Exclude specified displays
if exclude:
for id in exclude:
if id in ids:
ids.remove(id)
# Sort by desired photo surface area
ids.sort(lambda x,y,d=self._displays: cmp(d[x][0]*d[x][1], d[y][0]*d[y][1]))
return ids
security.declareProtected('Access contents information', 'displayLinks')
def displayLinks(self, exclude=('thumbnail',)):
"""Return list of HTML <a> tags for displays."""
links = []
for display in self.displayIds(exclude):
links.append('<a href="%s?display=%s">%s</a>' % (self.REQUEST['URL'], display, display))
return links
security.declareProtected('Access contents information', 'displayMap')
def displayMap(self, exclude=None, format='', quality=75, resolution=None):
"""Return list of displays with size info."""
displays = []
for id in self.displayIds(exclude):
if self._isGenerated(id, format=format, quality=quality,resolution=resolution):
photo_width = self._photos[(id,format)].width
photo_height = self._photos[(id,format)].height
bytes = self._photos[(id,format)]._size()
age = self._photos[(id,format)]._age()
else:
(photo_width, photo_height, bytes, age) = (None, None, None, None)
displays.append({'id': id,
'width': defaultdisplays[id][0],
'height': defaultdisplays[id][1],
'photo_width': photo_width,
'photo_height': photo_height,
'bytes': bytes,
'age': age
})
return displays
security.declareProtected('View', 'index_html')
index_html = Photo.index_html
def index_html(self, REQUEST, RESPONSE, display=None, format='', quality=75, resolution=None):
"""Return the image data."""
security.declareProtected(Permissions.AccessContentsInformation, 'content_type')
content_type = Photo.content_type
# display may be set from a cookie (?)
if (display is not None or resolution is not None or quality != 75) and defaultdisplays.has_key(display):
if not self.hasConversion(display=display, format=format,
quality=quality,resolution=resolution):
# Generate photo on-the-fly
self._makeDisplayPhoto(display, 1, format=format, quality=quality,resolution=resolution)
# Return resized image
mime, image = self.getConversion(display=display, format=format,
quality=quality ,resolution=resolution)
return image.index_html(REQUEST, RESPONSE)
# Copy support needs to be implemented by ExtFile
################################
# Special management methods #
################################
# Return original image
return OFSImage.index_html(self, REQUEST, RESPONSE)
def manage_afterClone(self, item):
Base.manage_afterClone(self, item)
self._checkOriginal()
Photo.manage_afterClone(self, item)
def manage_afterAdd(self, item, container):
Photo.manage_afterAdd(self, item, container)
#
# Photo processing
#
def manage_beforeDelete(self, item, container):
Photo.manage_beforeDelete(self, item, container)
def _resize(self, display, width, height, quality=75, format='', resolution=None):
"""Resize and resample photo."""
newimg = StringIO()
# Some ERPish
def getWidth(self):
"""
Alias for width
"""
return self.width()
if sys.platform == 'win32':
from win32pipe import popen2
from tempfile import mktemp
newimg_path = mktemp(suffix=format)
if resolution is None:
imgin, imgout = popen2('convert -quality %s -geometry %sx%s - %s'
% (quality, width, height, newimg_path), 'b')
else:
imgin, imgout = popen2('convert -density %sx%s -quality %s -geometry %sx%s - %s'
% (resolution, resolution, quality, width, height, newimg_path), 'b')
def getHeight(self):
"""
Alias for width
"""
return self.height()
# Aliases for uniform update of data
def manage_upload(self, file='', REQUEST=None):
self.manage_file_upload(self, file=file, REQUEST=None)
# DAV Support
PUT = Photo.PUT
security.declareProtected('FTP access', 'manage_FTPget', 'manage_FTPstat', 'manage_FTPlist')
manage_FTPget = Photo.manage_FTPget
manage_FTPlist = Photo.manage_FTPlist
manage_FTPstat = Photo.manage_FTPstat
else:
from popen2 import popen2
import tempfile
tempdir = tempfile.tempdir
tempfile.tempdir = '/tmp'
newimg_path = tempfile.mktemp(suffix='.' + format)
tempfile.tempdir = tempdir
if resolution is None:
imgout, imgin = popen2('convert -quality %s -geometry %sx%s - %s'
% (quality, width, height, newimg_path))
else:
LOG('Resolution',0,str(resolution))
imgout, imgin = popen2('convert -density %sx%s -quality %s -geometry %sx%s - %s'
% (resolution, resolution, quality, width, height, newimg_path))
imgin.write(str(self.getData()))
imgin.close()
imgout.read()
imgout.close()
newimg_file = open(newimg_path, 'r')
newimg.write(newimg_file.read())
newimg_file.close()
newimg.seek(0)
return newimg
def _getDisplayData(self, display, format='', quality=75, resolution=None):
"""Return raw photo data for given display."""
(width, height) = defaultdisplays[display]
if width == 0 and height == 0:
width = self.getWidth()
height = self.getHeight()
(width, height) = self._getAspectRatioSize(width, height)
return self._resize(display, width, height, quality, format=format, resolution=resolution)
def _getDisplayPhoto(self, display, format='', quality=75, resolution=None):
"""Return photo object for given display."""
try:
base, ext = string.split(self.id, '.')
id = base+'_'+display+'.'+ext
except ValueError:
id = self.id+'_'+display
image = OFSImage(id, self.getTitle(), self._getDisplayData(display, format=format,
quality=quality,resolution=resolution))
return image
def _makeDisplayPhoto(self, display, force=0, format='', quality=75, resolution=None):
"""Create given display."""
if not self.hasConversion(display=display, format=format, quality=quality,resolution=resolution) or force:
image = self._getDisplayPhoto(display, format=format, quality=quality, resolution=resolution)
self.setConversion(image, mime=image.content_type,
display=display, format=format,
quality=quality ,resolution=resolution)
def _getAspectRatioSize(self, width, height):
"""Return proportional dimensions within desired size."""
img_width, img_height = (self.getWidth(), self.getHeight())
if height > img_height * width / img_width:
height = img_height * width / img_width
else:
width = img_width * height / img_height
return (width, height)
def _validImage(self):
"""At least see if it *might* be valid."""
return self.getWidth() and self.getHeight() and self.getData() and self.getContentType()
#
# FTP/WebDAV support
#
#if hasattr(self, '_original'):
## Updating existing Photo
#self._original.manage_upload(file, self.content_type())
#if self._validImage():
#self._makeDisplayPhotos()
# Maybe needed
#def manage_afterClone(self, item):
# Maybe needed
#def manage_afterAdd(self, item, container):
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment