Package mms :: Module message
[hide private]
[frames] | no frames]

Source Code for Module mms.message

  1  #!/usr/bin/env python 
  2  # 
  3  # This library is free software, distributed under the terms of 
  4  # the GNU Lesser General Public License Version 2. 
  5  # See the COPYING file included in this archive 
  6  # 
  7  # The docstrings in this module contain epytext markup; API documentation 
  8  # may be created by processing this file with epydoc: http://epydoc.sf.net 
  9   
 10  """ High-level MMS-message creation/manipulation classes """ 
 11   
 12  import xml.dom.minidom 
 13  import os 
 14  import mimetypes 
 15  import array 
 16   
17 -class MMSMessage:
18 """ An MMS message 19 20 @note: References used in this class: [1][2][3][4][5] 21 """
22 - def __init__(self):
23 self._pages = [] 24 self._dataParts = [] 25 self._metaTags = {} 26 self.headers = {'Message-Type' : 'm-send-req', 27 'Transaction-Id' : '1234', 28 'MMS-Version' : '1.0', 29 'Content-Type' : ('application/vnd.wap.multipart.mixed', {})} 30 self.width = 176 31 self.height = 220 32 self.transactionID = '12345' 33 self.subject = 'test'
34 35 # contentType property 36 @property
37 - def contentType(self):
38 """ Returns the string representation of this data part's 39 "Content-Type" header. No parameter information is returned; 40 to get that, access the "Content-Type" header directly (which has a 41 tuple value)from the message's C{headers} attribute. 42 43 This is equivalent to calling DataPart.headers['Content-Type'][0] 44 """ 45 return self.headers['Content-Type'][0]
46
47 - def addPage(self, page):
48 """ Adds a single page/slide (MMSMessagePage object) to the message 49 50 @param page: The message slide/page to add 51 @type page: MMSMessagPage 52 """ 53 if self.contentType != 'application/vnd.wap.multipart.related': 54 self.headers['Content-Type'] = ('application/vnd.wap.multipart.related', {}) 55 self._pages.append(page)
56 57 @property
58 - def pages(self):
59 """ Returns a list of all the pages in this message """ 60 return self._pages
61
62 - def addDataPart(self, dataPart):
63 """ Adds a single data part (DataPart object) to the message, without 64 connecting it to a specific slide/page in the message. 65 66 A data part encapsulates some form of attachment, e.g. an image, audio 67 etc. 68 69 @param dataPart: The data part to add 70 @type dataPart: DataPart 71 72 @note: It is not necessary to explicitly add data parts to the message 73 using this function if "addPage" is used; this method is mainly 74 useful if you want to create MMS messages without SMIL support, 75 i.e. messages of type "application/vnd.wap.multipart.mixed" 76 """ 77 self._dataParts.append(dataPart)
78 79 @property
80 - def dataParts(self):
81 """ Returns a list of all the data parts in this message, including 82 data parts that were added to slides in this message """ 83 parts = [] 84 if len(self._pages) > 0: 85 parts.append(self.smil()) 86 for slide in self._mmsMessage._pages: 87 parts.extend(slide.dataParts()) 88 parts.extend(self._dataParts) 89 return parts
90 91
92 - def smil(self):
93 """ Returns the text of the message's SMIL file """ 94 impl = xml.dom.minidom.getDOMImplementation() 95 smilDoc = impl.createDocument(None, "smil", None) 96 97 # Create the SMIL header 98 headNode = smilDoc.createElement('head') 99 # Add metadata to header 100 for tagName in self._metaTags: 101 metaNode = smilDoc.createElement('meta') 102 metaNode.setAttribute(tagName, self._metaTags[tagName]) 103 headNode.appendChild(metaNode) 104 # Add layout info to header 105 layoutNode = smilDoc.createElement('layout') 106 rootLayoutNode = smilDoc.createElement('root-layout') 107 rootLayoutNode.setAttribute('width', str(self.width)) 108 rootLayoutNode.setAttribute('height', str(self.height)) 109 layoutNode.appendChild(rootLayoutNode) 110 for regionID, left, top, width, height in (('Image', '0', '0', '176', '144'), ('Text', '176', '144', '176', '76')): 111 regionNode = smilDoc.createElement('region') 112 regionNode.setAttribute('id', regionID) 113 regionNode.setAttribute('left', left) 114 regionNode.setAttribute('top', top) 115 regionNode.setAttribute('width', width) 116 regionNode.setAttribute('height', height) 117 layoutNode.appendChild(regionNode) 118 headNode.appendChild(layoutNode) 119 smilDoc.documentElement.appendChild(headNode) 120 121 # Create the SMIL body 122 bodyNode = smilDoc.createElement('body') 123 # Add pages to body 124 for page in self._pages: 125 parNode = smilDoc.createElement('par') 126 parNode.setAttribute('duration', str(page.duration)) 127 # Add the page content information 128 if page.image != None: 129 #TODO: catch unpack exception 130 part, begin, end = page.image 131 if 'Content-Location' in part.headers: 132 src = part.headers['Content-Location'] 133 elif 'Content-ID' in part.headers: 134 src = part.headers['Content-ID'] 135 else: 136 src = part.data 137 imageNode = smilDoc.createElement('img') 138 imageNode.setAttribute('src', src) 139 imageNode.setAttribute('region', 'Image') 140 if begin > 0 or end > 0: 141 if end > page.duration: 142 end = page.duration 143 imageNode.setAttribute('begin', str(begin)) 144 imageNode.setAttribute('end', str(end)) 145 parNode.appendChild(imageNode) 146 if page.text != None: 147 part, begin, end = page.text 148 src = part.data 149 textNode = smilDoc.createElement('text') 150 textNode.setAttribute('src', src) 151 textNode.setAttribute('region', 'Text') 152 if begin > 0 or end > 0: 153 if end > page.duration: 154 end = page.duration 155 textNode.setAttribute('begin', str(begin)) 156 textNode.setAttribute('end', str(end)) 157 parNode.appendChild(textNode) 158 if page.audio != None: 159 part, begin, end = page.audio 160 if 'Content-Location' in part.headers: 161 src = part.headers['Content-Location'] 162 elif 'Content-ID' in part.headers: 163 src = part.headers['Content-ID'] 164 else: 165 src = part.data 166 audioNode = smilDoc.createElement('audio') 167 audioNode.setAttribute('src', src) 168 if begin > 0 or end > 0: 169 if end > page.duration: 170 end = page.duration 171 audioNode.setAttribute('begin', str(begin)) 172 audioNode.setAttribute('end', str(end)) 173 parNode.appendChild(textNode) 174 parNode.appendChild(audioNode) 175 bodyNode.appendChild(parNode) 176 smilDoc.documentElement.appendChild(bodyNode) 177 178 return smilDoc.documentElement.toprettyxml()
179 180
181 - def encode(self):
182 """ Convenience funtion that binary-encodes this MMS message 183 184 @note: This uses the C{mms_pdu.MMSEncoder} class internally 185 186 @return: The binary-encode MMS data, as an array of bytes 187 @rtype array.array('B') 188 """ 189 import mms_pdu 190 encoder = mms_pdu.MMSEncoder() 191 return encoder.encode(self)
192 193
194 - def toFile(self, filename):
195 """ Convenience funtion that writes this MMS message to disk in 196 binary-encoded form. 197 198 @param filename: The name of the file in which to store the message 199 data 200 @type filename: str 201 202 @note: This uses the C{mms_pdu.MMSEncoder} class internally 203 204 @return: The binary-encode MMS data, as an array of bytes 205 @rtype array.array('B') 206 """ 207 f = open(filename, 'wb') 208 self.encode().tofile(f) 209 f.close()
210 211 @staticmethod
212 - def fromFile(filename):
213 """ Convenience static funtion that loads the specified MMS message 214 file from disk, decodes its data, and returns a new MMSMessage object, 215 which can then be manipulated and re-encoded, for instance. 216 217 @param filename: The name of the file to load 218 @type filename: str 219 220 @note: This uses the C{mms_pdu.MMSDecoder} class internally 221 """ 222 import mms_pdu 223 decoder = mms_pdu.MMSDecoder() 224 return decoder.decodeFile(filename)
225 226
227 -class MMSMessagePage:
228 """ A single page (or "slide") in an MMS Message. 229 230 In order to ensure that the MMS message can be correctly displayed by most 231 terminals, each page's content is limited to having 1 image, 1 audio clip 232 and 1 block of text, as stated in [1]. 233 234 @note: The default slide duration is set to 4 seconds; use setDuration() 235 to change this. 236 237 @note: References used in this class: [1] 238 """
239 - def __init__(self):
240 self.duration = 4000 241 self.image = None 242 self.audio = None 243 self.text = None
244 245 @property
246 - def dataParts(self):
247 """ Returns a list of the data parst in this slide """ 248 parts = [] 249 for part in (self.image, self.audio, self.text): 250 if part != None: 251 parts.append(part) 252 return parts
253
254 - def numberOfParts(self):
255 """ This function calculates the amount of data "parts" (or elements) 256 in this slide. 257 258 @return: The number of data parts in this slide 259 @rtype: int 260 """ 261 numParts = 0 262 for item in (self.image, self.audio, self.text): 263 if item != None: 264 numParts += 1 265 return numParts
266 267 #TODO: find out what the "ref" element in SMIL does (seen in conformance doc) 268 269 #TODO: add support for "alt" element; also make sure what it does
270 - def addImage(self, filename, timeBegin=0, timeEnd=0):
271 """ Adds an image to this slide. 272 @param filename: The name of the image file to add. Supported formats 273 are JPEG, GIF and WBMP. 274 @type filename: str 275 @param timeBegin: The time (in milliseconds) during the duration of 276 this slide to begin displaying the image. If this is 277 0 or less, the image will be displayed from the 278 moment the slide is opened. 279 @type timeBegin: int 280 @param timeEnd: The time (in milliseconds) during the duration of this 281 slide at which to stop showing (i.e. hide) the image. 282 If this is 0 or less, or if it is greater than the 283 actual duration of this slide, it will be shown until 284 the next slide is accessed. 285 @type timeEnd: int 286 287 @raise TypeError: An inappropriate variable type was passed in of the 288 parameters 289 """ 290 if type(filename) != str or type(timeBegin) != type(timeEnd) != int: 291 raise TypeError 292 if not os.path.isfile(filename): 293 raise OSError 294 if timeEnd > 0 and timeEnd < timeBegin: 295 raise ValueError, 'timeEnd cannot be lower than timeBegin' 296 self.image = (DataPart(filename), timeBegin, timeEnd)
297
298 - def addAudio(self, filename, timeBegin=0, timeEnd=0):
299 """ Adds an audio clip to this slide. 300 @param filename: The name of the audio file to add. Currently the only 301 supported format is AMR. 302 @type filename: str 303 @param timeBegin: The time (in milliseconds) during the duration of 304 this slide to begin playback of the audio clip. If 305 this is 0 or less, the audio clip will be played the 306 moment the slide is opened. 307 @type timeBegin: int 308 @param timeEnd: The time (in milliseconds) during the duration of this 309 slide at which to stop playing (i.e. mute) the audio 310 clip. If this is 0 or less, or if it is greater than 311 the actual duration of this slide, the entire audio 312 clip will be played, or until the next slide is 313 accessed. 314 @type timeEnd: int 315 316 @raise TypeError: An inappropriate variable type was passed in of the 317 parameters 318 """ 319 if type(filename) != str or type(timeBegin) != type(timeEnd) != int: 320 raise TypeError 321 if not os.path.isfile(filename): 322 raise OSError 323 if timeEnd > 0 and timeEnd < timeBegin: 324 raise ValueError, 'timeEnd cannot be lower than timeBegin' 325 self.audio = (DataPart(filename), timeBegin, timeEnd)
326
327 - def addText(self, text, timeBegin=0, timeEnd=0):
328 """ Adds a block of text to this slide. 329 @param text: The text to add to the slide. 330 @type text: str 331 @param timeBegin: The time (in milliseconds) during the duration of 332 this slide to begin displaying the text. If this is 333 0 or less, the text will be displayed from the 334 moment the slide is opened. 335 @type timeBegin: int 336 @param timeEnd: The time (in milliseconds) during the duration of this 337 slide at which to stop showing (i.e. hide) the text. 338 If this is 0 or less, or if it is greater than the 339 actual duration of this slide, it will be shown until 340 the next slide is accessed. 341 @type timeEnd: int 342 343 @raise TypeError: An inappropriate variable type was passed in of the 344 parameters 345 """ 346 if type(text) != str or type(timeBegin) != type(timeEnd) != int: 347 raise TypeError 348 if timeEnd > 0 and timeEnd < timeBegin: 349 raise ValueError, 'timeEnd cannot be lower than timeBegin' 350 tData = DataPart() 351 tData.setText(text) 352 self.text = (tData, timeBegin, timeEnd)
353
354 - def setDuration(self, duration):
355 """ Sets the maximum duration of this slide (i.e. how long this slide 356 should be displayed) 357 358 @param duration: the maxium slide duration, in milliseconds 359 @type duration: int 360 361 @raise TypeError: <duration> must be an integer 362 @raise ValueError: the requested duration is invalid (must be a 363 non-zero, positive integer) 364 """ 365 if type(duration) != int: 366 raise TypeError 367 elif duration < 1: 368 raise ValueError, 'duration may not be 0 or negative' 369 self.duration = duration
370
371 -class DataPart:
372 """ This class represents a data entry in the MMS body. 373 374 A DataPart objectencapsulates any data content that is to be added to the 375 MMS (e.g. an image file, raw image data, audio clips, text, etc). 376 377 A DataPart object can be queried using the Python built-in C{len()} 378 function. 379 380 This encapsulation allows custom header/parameter information to be set 381 for each data entry in the MMS. Refer to [5] for more information on 382 these. 383 """
384 - def __init__(self, srcFilename=None):
385 """ @param srcFilename: If specified, load the content of the file 386 with this name 387 @type srcFilename: str 388 """ 389 #self.contentTypeParameters = {} 390 self.headers = {'Content-Type': ('application/octet-stream', {})} 391 self._filename = None 392 self._data = None 393 if srcFilename != None: 394 self.fromFile(srcFilename)
395 396 # contentType property
397 - def _getContentType(self):
398 """ Returns the string representation of this data part's 399 "Content-Type" header. No parameter information is returned; 400 to get that, access the "Content-Type" header directly (which has a 401 tuple value)from this part's C{headers} attribute. 402 403 This is equivalent to calling DataPart.headers['Content-Type'][0] 404 """ 405 return self.headers['Content-Type'][0]
406 - def _setContentType(self, value):
407 """ Convenience method that sets the content type string, with no 408 parameters """ 409 self.headers['Content-Type'] = (value, {})
410 contentType = property(_getContentType, _setContentType) 411
412 - def fromFile(self, filename):
413 """ Load the data contained in the specified file 414 415 @note: This function clears any previously-set header entries. 416 417 @param filename: The name of the file to open 418 @type filename: str 419 420 @raises OSError: The filename is invalid 421 """ 422 if not os.path.isfile(filename): 423 raise OSError, 'The file "%s" does not exist.' % filename 424 # Clear any headers that are currently set 425 self.headers = {} 426 self._data = None 427 self.headers['Content-Location'] = os.path.basename(filename) 428 #self.contentType = mimetypes.guess_type(filename)[0] or 'application/octet-stream' 429 self.headers['Content-Type'] = (mimetypes.guess_type(filename)[0] or 'application/octet-stream', {}) 430 self._filename = filename
431
432 - def setData(self, data, contentType, ctParameters={}):
433 """ Explicitly set the data contained by this part 434 435 @note: This function clears any previously-set header entries. 436 437 @param data: The data to hold 438 @type data: str 439 @param contentType: The MIME content type of the specified data 440 @type contentType: str 441 @param ctParameters: A dictionary containing any content type header 442 parmaters to add, in the format: 443 C{{<parameter_name> : <parameter_value>}} 444 @type ctParameters: dict 445 """ 446 self.headers = {} 447 self._filename = None 448 self._data = data 449 self.headers['Content-Type'] = (contentType, ctParameters)
450
451 - def setText(self, text):
452 """ Convenience wrapper method for setData() 453 454 This method sets the DataPart object to hold the specified text 455 string, with MIME content type "text/plain". 456 457 @param text: The text to hold 458 @type text: str 459 """ 460 self.setData(text, 'text/plain')
461
462 - def __len__(self):
463 """ Provides the length of the data encapsulated by this object """ 464 if self._filename != None: 465 return int(os.stat(self._filename)[6]) 466 else: 467 return len(self.data)
468 469 @property
470 - def data(self):
471 """ A buffer containing the binary data of this part 472 """ 473 if self._data != None: 474 if type(self._data) == array.array: 475 self._data = self._data.tostring() 476 return self._data 477 elif self._filename != None: 478 f = open(self._filename, 'r') 479 self._data = f.read() 480 f.close() 481 return self._data 482 else: 483 return ''
484