1
2
3
4
5
6
7
8
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
18 """ An MMS message
19
20 @note: References used in this class: [1][2][3][4][5]
21 """
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
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
59 """ Returns a list of all the pages in this message """
60 return self._pages
61
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
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
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
98 headNode = smilDoc.createElement('head')
99
100 for tagName in self._metaTags:
101 metaNode = smilDoc.createElement('meta')
102 metaNode.setAttribute(tagName, self._metaTags[tagName])
103 headNode.appendChild(metaNode)
104
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
122 bodyNode = smilDoc.createElement('body')
123
124 for page in self._pages:
125 parNode = smilDoc.createElement('par')
126 parNode.setAttribute('duration', str(page.duration))
127
128 if page.image != None:
129
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
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
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
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
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
268
269
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
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 """
385 """ @param srcFilename: If specified, load the content of the file
386 with this name
387 @type srcFilename: str
388 """
389
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
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
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
425 self.headers = {}
426 self._data = None
427 self.headers['Content-Location'] = os.path.basename(filename)
428
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
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
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