2 # -*- coding: utf-8 -*-
4 # This library is free software, distributed under the terms of
5 # the GNU Lesser General Public License Version 2.
6 # See the COPYING.LESSER file included in this archive
8 # The docstrings in this module contain epytext markup; API documentation
9 # may be created by processing this file with epydoc: http://epydoc.sf.net
11 Library for MMS message creation, original by Francois Aucamp
12 Modified by Nick Leppänen Larsson for use in Maemo5/Fremantle on the Nokia N900.
14 @author: Francois Aucamp <faucamp@csir.co.za>
15 @author: Nick Leppänen Larsson <frals@frals.se>
18 """ High-level MMS-message creation/manipulation classes """
20 import xml.dom.minidom
29 @note: References used in this class: [1][2][3][4][5]
31 def __init__(self, noHeaders=None):
32 self.noHeaders = noHeaders
33 transactionid = random.randint(0,100000)
37 if self.noHeaders == None:
38 self.headers = {'Message-Type' : 'm-send-req',
39 'Transaction-Id' : transactionid,
40 'MMS-Version' : '1.0',
41 'Content-Type' : ('application/vnd.wap.multipart.mixed', {'Start': '<smil>', 'Type': 'application/smil'})}
46 self.transactionID = transactionid
47 self.subject = 'Subject'
49 # contentType property
51 def contentType(self):
52 """ Returns the string representation of this data part's
53 "Content-Type" header. No parameter information is returned;
54 to get that, access the "Content-Type" header directly (which has a
55 tuple value)from the message's C{headers} attribute.
57 This is equivalent to calling DataPart.headers['Content-Type'][0]
59 return self.headers['Content-Type'][0]
61 def addPage(self, page):
62 """ Adds a single page/slide (MMSMessagePage object) to the message
64 @param page: The message slide/page to add
65 @type page: MMSMessagPage
67 if self.contentType != 'application/vnd.wap.multipart.related':
68 self.headers['Content-Type'] = ('application/vnd.wap.multipart.related', {'Start': '<smil>', 'Type': 'application/smil'})
69 #self.headers['Content-Type'] = ('application/vnd.wap.multipart.related', {})
70 self._pages.append(page)
74 """ Returns a list of all the pages in this message """
77 def addDataPart(self, dataPart):
78 """ Adds a single data part (DataPart object) to the message, without
79 connecting it to a specific slide/page in the message.
81 A data part encapsulates some form of attachment, e.g. an image, audio
84 @param dataPart: The data part to add
85 @type dataPart: DataPart
87 @note: It is not necessary to explicitly add data parts to the message
88 using this function if "addPage" is used; this method is mainly
89 useful if you want to create MMS messages without SMIL support,
90 i.e. messages of type "application/vnd.wap.multipart.mixed"
92 self._dataParts.append(dataPart)
96 """ Returns a list of all the data parts in this message, including
97 data parts that were added to slides in this message """
99 if len(self._pages) > 0:
100 parts.append(self.smil())
101 for slide in self._mmsMessage._pages:
102 parts.extend(slide.dataParts())
103 parts.extend(self._dataParts)
108 """ Returns the text of the message's SMIL file """
109 impl = xml.dom.minidom.getDOMImplementation()
110 smilDoc = impl.createDocument(None, "smil", None)
112 # Create the SMIL header
113 headNode = smilDoc.createElement('head')
114 # Add metadata to header
115 for tagName in self._metaTags:
116 metaNode = smilDoc.createElement('meta')
117 metaNode.setAttribute(tagName, self._metaTags[tagName])
118 headNode.appendChild(metaNode)
119 # Add layout info to header
120 layoutNode = smilDoc.createElement('layout')
121 rootLayoutNode = smilDoc.createElement('root-layout')
122 rootLayoutNode.setAttribute('width', str(self.width))
123 rootLayoutNode.setAttribute('height', str(self.height))
124 layoutNode.appendChild(rootLayoutNode)
125 #for regionID, left, top, width, height in (('Image', '0', '0', '176', '144'), ('Text', '176', '144', '176', '76')):
126 (regionID, left, top, width, height) = ('Text', '0', '0', '320', '240')
127 regionNode = smilDoc.createElement('region')
128 regionNode.setAttribute('id', regionID)
129 regionNode.setAttribute('left', left)
130 regionNode.setAttribute('top', top)
131 regionNode.setAttribute('width', width)
132 regionNode.setAttribute('height', height)
133 layoutNode.appendChild(regionNode)
134 headNode.appendChild(layoutNode)
135 smilDoc.documentElement.appendChild(headNode)
137 # Create the SMIL body
138 bodyNode = smilDoc.createElement('body')
140 for page in self._pages:
141 parNode = smilDoc.createElement('par')
142 parNode.setAttribute('duration', str(page.duration))
143 # Add the page content information
144 if page.image != None:
145 #TODO: catch unpack exception
146 part, begin, end = page.image
147 if 'Content-Location' in part.headers:
148 src = part.headers['Content-Location']
149 elif 'Content-ID' in part.headers:
150 src = part.headers['Content-ID']
153 imageNode = smilDoc.createElement('img')
154 imageNode.setAttribute('src', src)
155 imageNode.setAttribute('region', 'Image')
156 if begin > 0 or end > 0:
157 if end > page.duration:
159 imageNode.setAttribute('begin', str(begin))
160 imageNode.setAttribute('end', str(end))
161 parNode.appendChild(imageNode)
162 if page.text != None:
163 part, begin, end = page.text
165 textNode = smilDoc.createElement('text')
166 #textNode.setAttribute('src', src)
167 textNode.setAttribute('src', 'text.txt')
168 textNode.setAttribute('region', 'Text')
169 if begin > 0 or end > 0:
170 if end > page.duration:
172 textNode.setAttribute('begin', str(begin))
173 textNode.setAttribute('end', str(end))
174 parNode.appendChild(textNode)
175 if page.audio != None:
176 part, begin, end = page.audio
177 if 'Content-Location' in part.headers:
178 src = part.headers['Content-Location']
179 elif 'Content-ID' in part.headers:
180 src = part.headers['Content-ID']
184 audioNode = smilDoc.createElement('audio')
185 audioNode.setAttribute('src', src)
186 if begin > 0 or end > 0:
187 if end > page.duration:
189 audioNode.setAttribute('begin', str(begin))
190 audioNode.setAttribute('end', str(end))
191 parNode.appendChild(textNode)
192 parNode.appendChild(audioNode)
193 bodyNode.appendChild(parNode)
194 smilDoc.documentElement.appendChild(bodyNode)
196 return smilDoc.documentElement.toprettyxml()
200 """ Convenience funtion that binary-encodes this MMS message
202 @note: This uses the C{mms_pdu.MMSEncoder} class internally
204 @return: The binary-encode MMS data, as an array of bytes
205 @rtype array.array('B')
208 encoder = mms_pdu.MMSEncoder(self.noHeaders)
209 return encoder.encode(self)
212 def toFile(self, filename):
213 """ Convenience funtion that writes this MMS message to disk in
216 @param filename: The name of the file in which to store the message
220 @note: This uses the C{mms_pdu.MMSEncoder} class internally
222 @return: The binary-encode MMS data, as an array of bytes
223 @rtype array.array('B')
225 f = open(filename, 'wb')
226 self.encode().tofile(f)
230 def fromFile(filename):
231 """ Convenience static funtion that loads the specified MMS message
232 file from disk, decodes its data, and returns a new MMSMessage object,
233 which can then be manipulated and re-encoded, for instance.
235 @param filename: The name of the file to load
238 @note: This uses the C{mms_pdu.MMSDecoder} class internally
241 decoder = mms_pdu.MMSDecoder(os.path.dirname(filename))
242 return decoder.decodeFile(filename)
245 def fromData(inputdata):
246 """ Convenience static funtion that loads the specified MMS message
247 file from input, decodes its data, and returns a new MMSMessage object,
248 which can then be manipulated and re-encoded, for instance.
250 @param input: Input data to decode
253 @note: This uses the C{mms_pdu.MMSDecoder} class internally
256 decoder = mms_pdu.MMSDecoder()
257 data = array.array('B')
258 data.fromstring(inputdata)
259 return decoder.decodeData(data)
261 class MMSMessagePage:
262 """ A single page (or "slide") in an MMS Message.
264 In order to ensure that the MMS message can be correctly displayed by most
265 terminals, each page's content is limited to having 1 image, 1 audio clip
266 and 1 block of text, as stated in [1].
268 @note: The default slide duration is set to 4 seconds; use setDuration()
271 @note: References used in this class: [1]
281 """ Returns a list of the data parts in this slide """
283 for part in (self.image, self.audio, self.text):
288 def numberOfParts(self):
289 """ This function calculates the amount of data "parts" (or elements)
292 @return: The number of data parts in this slide
296 for item in (self.image, self.audio, self.text):
301 #TODO: find out what the "ref" element in SMIL does (seen in conformance doc)
303 #TODO: add support for "alt" element; also make sure what it does
304 def addImage(self, filename, timeBegin=0, timeEnd=0):
305 """ Adds an image to this slide.
306 @param filename: The name of the image file to add. Supported formats
307 are JPEG, GIF and WBMP.
309 @param timeBegin: The time (in milliseconds) during the duration of
310 this slide to begin displaying the image. If this is
311 0 or less, the image will be displayed from the
312 moment the slide is opened.
314 @param timeEnd: The time (in milliseconds) during the duration of this
315 slide at which to stop showing (i.e. hide) the image.
316 If this is 0 or less, or if it is greater than the
317 actual duration of this slide, it will be shown until
318 the next slide is accessed.
321 @raise TypeError: An inappropriate variable type was passed in of the
324 if type(filename) != str or type(timeBegin) != type(timeEnd) != int:
326 if not os.path.isfile(filename):
328 if timeEnd > 0 and timeEnd < timeBegin:
329 raise ValueError, 'timeEnd cannot be lower than timeBegin'
330 self.image = (DataPart(filename), timeBegin, timeEnd)
332 def addAudio(self, filename, timeBegin=0, timeEnd=0):
333 """ Adds an audio clip to this slide.
334 @param filename: The name of the audio file to add. Currently the only
335 supported format is AMR.
337 @param timeBegin: The time (in milliseconds) during the duration of
338 this slide to begin playback of the audio clip. If
339 this is 0 or less, the audio clip will be played the
340 moment the slide is opened.
342 @param timeEnd: The time (in milliseconds) during the duration of this
343 slide at which to stop playing (i.e. mute) the audio
344 clip. If this is 0 or less, or if it is greater than
345 the actual duration of this slide, the entire audio
346 clip will be played, or until the next slide is
350 @raise TypeError: An inappropriate variable type was passed in of the
353 if type(filename) != str or type(timeBegin) != type(timeEnd) != int:
355 if not os.path.isfile(filename):
357 if timeEnd > 0 and timeEnd < timeBegin:
358 raise ValueError, 'timeEnd cannot be lower than timeBegin'
359 self.audio = (DataPart(filename), timeBegin, timeEnd)
361 def addText(self, text, timeBegin=0, timeEnd=0):
362 """ Adds a block of text to this slide.
363 @param text: The text to add to the slide.
365 @param timeBegin: The time (in milliseconds) during the duration of
366 this slide to begin displaying the text. If this is
367 0 or less, the text will be displayed from the
368 moment the slide is opened.
370 @param timeEnd: The time (in milliseconds) during the duration of this
371 slide at which to stop showing (i.e. hide) the text.
372 If this is 0 or less, or if it is greater than the
373 actual duration of this slide, it will be shown until
374 the next slide is accessed.
377 @raise TypeError: An inappropriate variable type was passed in of the
380 if type(text) != str or type(timeBegin) != type(timeEnd) != int:
382 if timeEnd > 0 and timeEnd < timeBegin:
383 raise ValueError, 'timeEnd cannot be lower than timeBegin'
386 self.text = (tData, timeBegin, timeEnd)
388 def setDuration(self, duration):
389 """ Sets the maximum duration of this slide (i.e. how long this slide
392 @param duration: the maxium slide duration, in milliseconds
395 @raise TypeError: <duration> must be an integer
396 @raise ValueError: the requested duration is invalid (must be a
397 non-zero, positive integer)
399 if type(duration) != int:
402 raise ValueError, 'duration may not be 0 or negative'
403 self.duration = duration
406 """ This class represents a data entry in the MMS body.
408 A DataPart objectencapsulates any data content that is to be added to the
409 MMS (e.g. an image file, raw image data, audio clips, text, etc).
411 A DataPart object can be queried using the Python built-in C{len()}
414 This encapsulation allows custom header/parameter information to be set
415 for each data entry in the MMS. Refer to [5] for more information on
418 def __init__(self, srcFilename=None):
419 """ @param srcFilename: If specified, load the content of the file
421 @type srcFilename: str
423 #self.contentTypeParameters = {}
424 self.headers = {'Content-Type': ('application/octet-stream', {})}
425 self._filename = None
427 self._part = "<fMMSpart" + str(random.randint(0,10000)) + ">"
428 if srcFilename != None:
429 self.fromFile(srcFilename)
431 # contentType property
432 def _getContentType(self):
433 """ Returns the string representation of this data part's
434 "Content-Type" header. No parameter information is returned;
435 to get that, access the "Content-Type" header directly (which has a
436 tuple value)from this part's C{headers} attribute.
438 This is equivalent to calling DataPart.headers['Content-Type'][0]
440 return self.headers['Content-Type'][0]
441 def _setContentType(self, value):
442 """ Convenience method that sets the content type string, with no
444 self.headers['Content-Type'] = (value, {})
445 contentType = property(_getContentType, _setContentType)
447 def fromFile(self, filename):
448 """ Load the data contained in the specified file
450 @note: This function clears any previously-set header entries.
452 @param filename: The name of the file to open
455 @raises OSError: The filename is invalid
457 if not os.path.isfile(filename):
458 raise OSError, 'The file "%s" does not exist.' % filename
459 # Clear any headers that are currently set
462 self.headers['Content-Location'] = os.path.basename(filename)
463 #self.contentType = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
464 #print mimetypes.guess_type(filename)[0]
465 if mimetypes.guess_type(filename)[0] == 'text/plain':
466 self.headers['Content-Type'] = ('text/plain', {'Charset': 'utf-8', 'Name': os.path.basename(filename)})
468 self.headers['Content-Type'] = (mimetypes.guess_type(filename)[0] or 'application/octet-stream', {'Name': os.path.basename(filename)})
469 #self.headers['Content-Type'] = (mimetypes.guess_type(filename)[0] or 'application/octet-stream', {})
470 #self.headers['Content-ID'] = self._part
471 self._filename = filename
473 def setData(self, data, contentType, ctParameters={}):
474 """ Explicitly set the data contained by this part
476 @note: This function clears any previously-set header entries.
478 @param data: The data to hold
480 @param contentType: The MIME content type of the specified data
481 @type contentType: str
482 @param ctParameters: A dictionary containing any content type header
483 parmaters to add, in the format:
484 C{{<parameter_name> : <parameter_value>}}
485 @type ctParameters: dict
488 self._filename = None
490 # self._data = self._part + "\0"
492 if contentType == "application/smil":
493 #self.headers['Content-Type'] = (contentType, {'Charset': 'utf-8', 'Name': 'smil.smil'})
494 self.headers['Content-Type'] = (contentType, {})
496 self.headers['Content-Type'] = (contentType, ctParameters)
497 self.headers['Content-ID'] = (self._part)
499 def setText(self, text):
500 """ Convenience wrapper method for setData()
502 This method sets the DataPart object to hold the specified text
503 string, with MIME content type "text/plain".
505 @param text: The text to hold
508 self.setData(text, 'text/plain', {'Charset': 'utf-8', 'Name': 'text.txt'})
511 """ Provides the length of the data encapsulated by this object """
512 if self._filename != None:
513 return int(os.stat(self._filename)[6])
515 return len(self.data)
519 """ @return: the data of this part
522 if self._data != None:
523 if type(self._data) == array.array:
524 self._data = self._data.tostring()
526 elif self._filename != None:
527 f = open(self._filename, 'r')
528 self._data = f.read()