Reverted wrong commit - sorry.
[docutils.git] / sandbox / rst2odp / odplib / preso.py
blob5999a8699686e951e3414b2be3e907bc99770a4a
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 """
4 Object oriented lib to Open Office Presentations
6 Copyright 2008-2009 Matt Harrison
7 Licensed under Apache License, Version 2.0 (current)
8 """
10 import copy
11 import cStringIO as sio
12 import xml.etree.ElementTree as et
13 from xml.dom import minidom
14 import os
15 import sys
16 import tempfile
18 import pygments
19 from pygments import formatter, lexers
20 import zipwrap
21 import Image
22 import imagescale
24 DOC_CONTENT_ATTRIB = {
25 'office:version': '1.0',
26 'xmlns:anim':'urn:oasis:names:tc:opendocument:xmlns:animation:1.0',
27 'xmlns:chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
28 'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
29 'xmlns:dom': 'http://www.w3.org/2001/xml-events',
30 'xmlns:dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0',
31 'xmlns:draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
32 'xmlns:fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
33 'xmlns:form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
34 'xmlns:math': 'http://www.w3.org/1998/Math/MathML',
35 'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
36 'xmlns:number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0',
37 'xmlns:office': 'urn:oasis:names:tc:opendocument:xmlns:office:1.0',
38 'xmlns:presentation': 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
39 'xmlns:ooo': 'http://openoffice.org/2004/office',
40 'xmlns:oooc': 'http://openoffice.org/2004/calc',
41 'xmlns:ooow': 'http://openoffice.org/2004/writer',
42 'xmlns:script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
43 'xmlns:smil':'urn:oasis:names:tc:opendocument:xmlns:smil-compatible:1.0',
44 'xmlns:style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
45 'xmlns:svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0',
46 'xmlns:table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
47 'xmlns:text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
48 'xmlns:xforms': 'http://www.w3.org/2002/xforms',
49 'xmlns:xlink': 'http://www.w3.org/1999/xlink',
50 'xmlns:xsd': 'http://www.w3.org/2001/XMLSchema',
51 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
54 NS2PREFIX = {}
55 for key, value in DOC_CONTENT_ATTRIB.items():
56 NS2PREFIX[value] = key.split(':')[-1]
59 TEXT_COUNT = 100
60 DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
62 MONO_FONT = 'Courier New' # I like 'Envy Code R'
63 NORMAL_FONT = 'Arial'
65 SLIDE_WIDTH = 30 # cm
66 SLIDE_HEIGHT = 21
68 def cwd_decorator(func):
69 """
70 decorator to change cwd to directory containing rst for this function
71 """
72 def wrapper(*args, **kw):
73 cur_dir = os.getcwd()
74 found = False
75 for arg in sys.argv:
76 if arg.endswith(".rst"):
77 found = arg
78 break
79 if found:
80 directory = os.path.dirname(arg)
81 if directory:
82 os.chdir(directory)
83 data = func(*args, **kw)
84 os.chdir(cur_dir)
85 return data
86 return wrapper
88 class PrefixedWriter(et.ElementTree):
89 """ hacked to pass NS2PREFIX to _write """
90 def write(self, file, encoding="us-ascii"):
91 assert self._root is not None
92 if not hasattr(file, "write"):
93 file = open(file, "wb")
94 if not encoding:
95 encoding = "us-ascii"
96 elif encoding != "utf-8" and encoding != "us-ascii":
97 file.write("<?xml version='1.0' encoding='%s'?>\n" % encoding)
98 self._write(file, self._root, encoding, NS2PREFIX)
99 #self._write(file, self._root, encoding, {})
101 # Wrap etree elements to add parent attribute
102 def el(tag, attrib=None):
103 attrib = attrib or {}
104 el = et.Element(tag, attrib)
105 el.parent = None
106 return el
108 def sub_el(parent, tag, attrib=None):
109 attrib = attrib or {}
110 el = et.SubElement(parent, tag, attrib)
111 el.parent = parent
112 return el
114 def to_xml(node):
115 """ convert an etree node to xml """
116 fout = sio.StringIO()
117 etree = PrefixedWriter(node)
118 etree.write(fout)
119 xml = fout.getvalue()
120 return xml
122 def pretty_xml(string_input, add_ns=False):
123 """ pretty indent string_input """
124 if add_ns:
125 elem = '<foo '
126 for key, value in DOC_CONTENT_ATTRIB.items():
127 elem += ' %s="%s"' %(key, value)
128 string_input = elem + '>' + string_input + '</foo>'
129 doc = minidom.parseString(string_input)
130 if add_ns:
131 s1 = doc.childNodes[0].childNodes[0].toprettyxml(' ')
132 else:
133 s1 = doc.toprettyxml(' ')
134 return s1
137 class Preso(object):
138 mime_type = 'application/vnd.oasis.opendocument.presentation'
140 def __init__(self):
141 self.slides = []
142 self.limit_pages = [] # can be list of page numbers (not indexes to export)
143 self._pictures = [] # list of Picture instances
144 self._footer_count = 0
145 # xml elements
146 self._root = None
147 self._auto_styles = None
148 self._presentation = None
150 self._styles_added = {}
152 self._init_xml()
154 def _init_xml(self):
155 self._root = el('office:document-content', attrib=DOC_CONTENT_ATTRIB)
156 o_scripts = sub_el(self._root, 'office:scripts')
157 self._auto_styles = sub_el(self._root, 'office:automatic-styles')
158 o_body = sub_el(self._root, 'office:body')
159 self._presentation = sub_el(o_body, 'office:presentation')
161 def add_imported_auto_style(self, style_node):
162 self._auto_styles.append(style_node)
163 style_node.parent = self._auto_styles
165 def import_slide(self, preso_file, page_num):
166 odp = zipwrap.ZipWrap(preso_file)
167 content = odp.cat('content.xml')
168 content_tree = et.fromstring(content)
169 slides = content_tree.findall('{urn:oasis:names:tc:opendocument:xmlns:office:1.0}body/{urn:oasis:names:tc:opendocument:xmlns:office:1.0}presentation/{urn:oasis:names:tc:opendocument:xmlns:drawing:1.0}page')
170 try:
171 slide_xml = slides[page_num - 1]
172 except IndexError, e:
173 print "Can't find page_num %d only %d slides" %(page_num, len(slides))
174 raise
175 if slide_xml:
176 self.slides.append(XMLSlide(self, slide_xml, odp))
178 def get_data(self, style_file=None):
179 fd, filename = tempfile.mkstemp()
180 zip_odp = self.to_file()
181 if style_file:
182 self.add_otp_style(zip_odp, style_file)
183 zip_odp.zipit(filename)
184 data = open(filename).read()
185 os.remove(filename)
186 return data
188 def add_otp_style(self, zip_odp, style_file):
190 takes the slide content and merges in the style_file
192 style = zipwrap.ZipWrap(style_file)
193 if 'Pictures' in style.cat(''):
194 for p in style.cat('Pictures'):
195 picture_file = 'Pictures/'+p
196 zip_odp.touch(picture_file, style.cat(picture_file))
197 zip_odp.touch('styles.xml', style.cat('styles.xml'))
198 return zip_odp
201 def to_file(self, filename=None):
203 >>> p = Preso()
204 >>> z = p.to_file('/tmp/foo.odp')
205 >>> z.cat('/')
206 ['settings.xml', 'META-INF', 'styles.xml', 'meta.xml', 'content.xml', 'mimetype']
208 out = zipwrap.ZipWrap('')
209 out.touch('mimetype', self.mime_type)
210 for p in self._pictures:
211 out.touch('Pictures/%s' % p.internal_name, p.get_data())
212 out.touch('content.xml', self.to_xml())
213 out.touch('styles.xml', self.styles_xml())
214 out.touch('meta.xml', self.meta_xml())
215 out.touch('settings.xml', self.settings_xml())
216 out.touch('META-INF/manifest.xml', self.manifest_xml(out))
217 if filename:
218 out.zipit(filename)
219 return out
221 def manifest_xml(self, zip):
222 content = """<?xml version="1.0" encoding="UTF-8"?>
223 <manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0">
224 <manifest:file-entry manifest:media-type="application/vnd.oasis.opendocument.presentation" manifest:full-path="/"/>
226 files = zip.cat('/')
227 try:
228 files.extend(zip.cat('/Pictures'))
229 except IOError, e:
230 # it's ok to not have pictures ;)
231 pass
232 for filename in files:
233 filetype = ''
234 if filename.endswith('.xml'):
235 filetype = 'text/xml'
236 elif filename.endswith('.jpg'):
237 filetype = 'image/jpeg'
238 elif filename.endswith('.gif'):
239 filetype = 'image/gif'
240 elif filename.endswith('.png'):
241 filetype = 'image/png'
242 elif filename == 'Configurations2/':
243 filetype = 'application/vnd.sun.xml.ui.configuration'
245 content += """ <manifest:file-entry manifest:media-type="%s" manifest:full-path="%s"/> """ % (filetype, filename)
247 content += """</manifest:manifest>"""
248 return content
251 def meta_xml(self):
252 return """<?xml version="1.0" encoding="UTF-8"?>
253 <office:document-meta xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:presentation="urn:oasis:names:tc:opendocument:xmlns:presentation:1.0" xmlns:ooo="http://openoffice.org/2004/office" xmlns:smil="urn:oasis:names:tc:opendocument:xmlns:smil-compatible:1.0" xmlns:anim="urn:oasis:names:tc:opendocument:xmlns:animation:1.0" office:version="1.1">
254 <office:meta>
255 <meta:generator>odplib(python)</meta:generator>
256 <meta:creation-date>2008-09-15T11:12:02</meta:creation-date>
257 <dc:date>2008-10-01T20:32:43</dc:date>
258 <meta:editing-cycles>3</meta:editing-cycles>
259 <meta:editing-duration>PT26M35S</meta:editing-duration>
260 <meta:user-defined meta:name="Info 1"/>
261 <meta:user-defined meta:name="Info 2"/>
262 <meta:user-defined meta:name="Info 3"/>
263 <meta:user-defined meta:name="Info 4"/>
264 <meta:document-statistic meta:object-count="37"/>
265 </office:meta>
266 </office:document-meta>
269 def settings_xml(self):
270 filename = os.path.join(DATA_DIR, 'settings.xml')
271 return open(filename).read()
273 def styles_xml(self):
274 filename = os.path.join(DATA_DIR, 'styles.xml')
275 data = open(filename).read()
276 if NORMAL_FONT != 'Arial':
277 data = data.replace('fo:font-family="Arial"',
278 'fo:font-family="%s"' %NORMAL_FONT)
279 return data
281 def to_xml(self):
282 for i, slide in enumerate(self.slides):
283 if self.limit_pages and i+1 not in self.limit_pages:
285 continue
286 if slide.footer:
287 footer_node = slide.footer.get_node()
288 self._presentation.append(footer_node)
289 footer_node.parent = self._presentation
290 node = slide.get_node()
291 self._presentation.append(node)
292 node.parent = self._presentation
293 return to_xml(self._root)
295 def add_style(self, style):
296 name = style.name
297 node = style.style_node()
298 if name not in self._styles_added:
299 self._styles_added[name] = 1
300 self._auto_styles.append(node)
303 def add_slide(self):
304 pnum = len(self.slides)+1
305 s = Slide(self, page_number=pnum)
306 self.slides.append(s)
307 return s
309 def copy_slide(self, s):
310 new_s = s._copy()
311 self.slides.append(new_s)
312 return new_s
314 def add_footer(self, f):
315 f.name = 'ftr%d'%(self._footer_count)
316 self._footer_count += 1
317 self.slides[-1].footer = f
320 class Animation(object):
321 ANIM_COUNT = 1
322 def __init__(self):
323 self.id = self._get_id()
325 def _get_id(self):
326 my_id = "id%d" % self.__class__.ANIM_COUNT
327 self.__class__.ANIM_COUNT += 1
328 return my_id
330 def get_node(self):
331 """
332 <anim:par smil:begin="next">
333 <anim:par smil:begin="0s">
334 <anim:par smil:begin="0s" smil:fill="hold" presentation:node-type="on-click" presentation:preset-class="entrance" presentation:preset-id="ooo-entrance-appear">
335 <anim:set smil:begin="0s" smil:dur="0.001s" smil:fill="hold" smil:targetElement="id1" anim:sub-item="text" smil:attributeName="visibility" smil:to="visible"/>
336 </anim:par>
337 </anim:par>
338 </anim:par>
340 par = el('anim:par', attrib={'smil:begin':'next'})
341 par2 = sub_el(par, 'anim:par', attrib={'smil:begin':'0s'})
342 par3 = sub_el(par2, 'anim:par', attrib={'smil:begin':'0s',
343 'smil:fill':'hold',
344 'presentation:node-type':'on-click',
345 'presentation:preset-class':'entrance',
346 'presentation:preset-id':'ooo-entrance-appear'})
347 anim_set = sub_el(par3, 'anim:set', attrib={'smil:begin':'0s',
348 'smil:dur':'0.001s',
349 'smil:fill':'hold',
350 'smil:targetElement':self.id,
351 'anim:sub-item':'text',
352 'smil:attributeName':'visibility',
353 'smil:to':'visible'})
354 return par
357 class ImportedPicture(object):
359 Pictures used when importing slides
361 def __init__(self, name, data):
362 self.internal_name = name
363 self.data = data
365 def get_data(self):
366 return self.data
369 class Picture(object):
371 Need to convert to use image scale::
372 im = imagescale.ImageScale(uri)
373 x, y, w, h = im.adjust_size(WIDTH, HEIGHT)
374 x_str = "%fcm" % x
375 y_str = "%fcm" % y
376 w_str = "%fcm" % w
377 h_str = "%fcm" % h
378 frame = self._create_frame(attrib={ "draw:style-name":style_name,
379 "draw:text-style-name":"P6",
380 "draw:layer":"layout",
381 "svg:width":w_str, #"31.585cm",
382 "svg:height":h_str, #"21cm",
383 "svg:x":x_str, #"-1.781cm",
384 "svg:y":y_str #"0cm"
387 COUNT = 0
388 CM_SCALE = 30.
390 def __init__(self, filepath, **kw):
391 self.filepath = filepath
392 image = Image.open(filepath)
393 self.w, self.h = image.size
394 self.internal_name = self._gen_name()
395 self.user_defined = {}
396 self._process_kw(kw)
398 def update_frame_attributes(self, attrib):
399 """ For positioning update the frame """
401 if 'align' in self.user_defined:
402 align = self.user_defined['align']
403 if 'top' in align:
404 attrib['style:vertical-pos'] = 'top'
405 if 'right' in align:
406 attrib['style:horizontal-pos'] = 'right'
407 return attrib
409 def _process_kw(self, kw):
410 self.user_defined = kw
412 def _gen_name(self):
413 ext = os.path.splitext(self.filepath)[1]
414 name = str(Picture.COUNT) + ext
415 Picture.COUNT += 1
416 return name
418 def get_xywh(self, measurement=None):
419 if measurement is None or measurement == 'cm':
420 measurement = 'cm'
421 scale = Picture.CM_SCALE
423 DPCM = 1 # dots per cm
424 if 'crop' in self.user_defined.get('classes', []):
425 x,y,w,h = imagescale.adjust_crop(SLIDE_WIDTH*DPCM, SLIDE_HEIGHT*DPCM,self.w, self.h)
426 elif 'fit' in self.user_defined.get('classes', []):
427 x,y,w,h = imagescale.adjust_fit(SLIDE_WIDTH*DPCM, SLIDE_HEIGHT*DPCM,self.w, self.h)
428 elif 'fill' in self.user_defined.get('classes', []):
429 x,y,w,h = 0,0,SLIDE_WIDTH,SLIDE_HEIGHT
430 else:
431 x,y,w,h = 1.4, 4.6, self.get_width(), self.get_height()
432 return [str(foo)+measurement for foo in [x,y,w,h]]
434 def get_width(self, measurement=None):
435 if measurement is None or measurement == 'cm':
436 measurement = 'cm'
437 scale = Picture.CM_SCALE
438 if 'width' in self.user_defined:
439 return self.user_defined['width'] + 'pt'
440 if 'scale' in self.user_defined:
441 return '%spt' % (self.w * float(self.user_defined['scale'])/100)
442 return str(self.w/scale)
444 def get_height(self, measurement=None):
445 if measurement is None:
446 measurement = 'cm'
447 if measurement == 'cm':
448 scale = Picture.CM_SCALE
449 if 'height' in self.user_defined:
450 return self.user_defined['height'] + 'pt'
451 if 'scale' in self.user_defined:
452 return '%spt' % (self.h * float(self.user_defined['scale'])/100)
453 return str(self.h/scale)
457 def get_data(self):
458 return open(self.filepath).read()
460 class XMLSlide(object):
461 PREFIX = 'IMPORT%d-%s'
462 COUNT = 0
463 def __init__(self, preso, node, odp_zipwrap):
464 self.preso = preso
465 self.page_node = node
466 self.footer = None
467 self.mangled = self._mangle_name()
468 self._init(odp_zipwrap)
470 def page_num(self):
471 """ not an int, usually 'Slide 1' or 'page1' """
472 name = self.page_node.attrib.get('{urn:oasis:names:tc:opendocument:xmlns:drawing:1.0}name', None)
473 return name
475 def _mangle_name(self):
476 name = self.PREFIX%(self.COUNT, self.page_num())
477 self.COUNT += 1
478 return name
480 def _init(self, odp_zipwrap):
482 # pull pictures out of slide
483 images = self.page_node.findall('*/{urn:oasis:names:tc:opendocument:xmlns:drawing:1.0}image')
484 for image in images:
485 path = image.attrib.get('{http://www.w3.org/1999/xlink}href')
486 data = odp_zipwrap.cat(path)
487 name = path.split('/')[1]
488 self.preso._pictures.append(ImportedPicture(name, data))
490 # pull styles out of content.xml (draw:style-name, draw:text-style-name, text:style-name)
491 styles_to_copy = {} #map of (attr_name, value) to value
492 attr_names = ['{urn:oasis:names:tc:opendocument:xmlns:drawing:1.0}style-name',
493 '{urn:oasis:names:tc:opendocument:xmlns:drawing:1.0}text-style-name',
494 '{urn:oasis:names:tc:opendocument:xmlns:text:1.0}style-name']
495 for node in self.page_node.getiterator():
496 for attr_name in attr_names:
497 style = node.attrib.get(attr_name, None)
498 if style:
499 styles_to_copy[style] = attr_name
500 # mangle name
501 node.attrib[attr_name] = self.mangled + style
503 auto_attr_names = ['{urn:oasis:names:tc:opendocument:xmlns:style:1.0}name']
504 found = {}
505 # get content.xml automatic-styles
506 content = odp_zipwrap.cat('content.xml')
507 content_node = et.fromstring(content)
508 auto_node = content_node.findall('{urn:oasis:names:tc:opendocument:xmlns:office:1.0}automatic-styles')[0]
510 for node in auto_node.getchildren():
511 for attr_name in auto_attr_names:
512 attr_value = node.attrib.get(attr_name, None)
513 if attr_value in styles_to_copy:
514 found[attr_value] = 1
515 # mangle name
516 node.attrib[attr_name] = self.mangled + attr_value
517 self.preso.add_imported_auto_style(node)
523 def get_node(self):
524 return self.page_node
528 class Slide(object):
529 def __init__(self, preso, page_number=None):
530 self.title_frame = None
531 self.text_frames = []
532 self._cur_text_frame = None
533 self.pic_frame = None
534 self._preso = preso
535 self.footer_frame = None
536 self.notes_frame = None
537 self.page_number = page_number
538 self.bullet_list = None # current bullet list
539 self.footer = None
540 self.animations = []
541 self.paragraph_attribs = {} # used to mark id's for animations
542 self.page_number_listeners = [self]
543 self.pending_styles = []
545 self.element_stack = [] # allow us to push pop
546 self.cur_element = None # if we write it could be to title,
547 # text or notes (Subclass of
548 # MixedContent)
550 self.insert_line_break = 0
552 # xml elements
553 self._page = None
555 self._init_xml()
557 def insert_line_breaks(self):
559 If you want to write out existing line breaks, but don't have content to write
560 call this
562 if self.cur_element:
563 self.cur_element.line_break()
565 def start_animation(self, anim):
566 self.animations.append(anim)
567 self.paragraph_attribs['text:id'] = anim.id
569 def end_animation(self):
570 # jump out of text:p
571 self.parent_of('text:p')
572 if 'text:id' in self.paragraph_attribs:
573 del self.paragraph_attribs['text:id']
575 def push_pending_node(self, name, attr):
577 pending nodes are for affecting type, such as wrapping content
578 with text:a to make a hyperlink. Anything in pending nodes
579 will be written before the actual text.
580 User needs to remember to pop out of it.
582 if self.cur_element is None:
583 self.add_text_frame()
584 self.cur_element.pending_nodes.append((name,attr))
586 def push_style(self, style):
587 if self.cur_element is None:
588 self.add_text_frame()
589 self.pending_styles.append(style)
591 def pop_style(self):
592 popped = self.pending_styles.pop()
594 def add_code(self, code, language):
595 if self.cur_element is None:
596 self.add_text_frame()
597 style = ParagraphStyle(**{'fo:text-align':'start'})
598 self.push_style(style)
599 output = pygments.highlight(code, lexers.get_lexer_by_name(language, stripall=True),
600 OdtCodeFormatter(self.cur_element, self._preso))
601 self.pop_style()
602 self.pop_node()
604 def add_picture(self, p):
606 needs to look like this (under draw:page)
608 <draw:frame draw:style-name="gr2" draw:text-style-name="P2" draw:layer="layout" svg:width="19.589cm" svg:height="13.402cm" svg:x="3.906cm" svg:y="4.378cm">
609 <draw:image xlink:href="Pictures/10000201000002F800000208188B22AE.png" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad">
610 <text:p text:style-name="P1"/>
611 </draw:image>
612 </draw:frame>
614 # pictures should be added the the draw:frame element
615 self.pic_frame = PictureFrame(self, p)
616 self.pic_frame.add_node('draw:image', attrib={'xlink:href':os.path.join('Pictures', p.internal_name),
617 'xlink:type':'simple',
618 'xlink:show':'embed',
619 'xlink:actuate':'onLoad' })
620 self._preso._pictures.append(p)
621 node = self.pic_frame.get_node()
622 self._page.append(node)
623 node.parent = self._page
625 def push_element(self):
626 """ element push/pop is used to remember previous cur_elem, since
627 lists might need to mess with that"""
628 self.element_stack.append(self.cur_element)
630 def pop_element(self):
631 self.cur_element = self.element_stack.pop()
633 def to_xml(self):
634 node = self.get_node()
635 return to_xml(node)
637 def _fire_page_number(self, new_num):
638 for listener in self.page_number_listeners:
639 listener.new_page_num(new_num)
641 def new_page_num(self, new_num):
642 self._page.attrib['draw:name'] = 'page%d' % self.page_number
644 def _copy(self):
645 ''' needs to update page numbers '''
646 ins = copy.copy(self)
647 ins._fire_page_number(self.page_number+1)
648 return ins
650 def _init_xml(self):
651 self._page = el('draw:page', attrib={
652 'draw:name':'page%d' % self.page_number,
653 'draw:style-name':'dp1',
654 'draw:master-page-name':'Default',
655 'presentation:presentation-page-layout-name':'AL1T0'
657 office_forms = sub_el(self._page, 'office:forms',
658 attrib={'form:automatic-focus':'false',
659 'form:apply-design-mode':'false'})
661 def get_node(self):
662 """return etree Element representing this slide"""
663 # already added title, text frames
664 # add animation chunks
665 if self.animations:
666 anim_par = el('anim:par', attrib={'presentation:node-type':'timing-root'})
667 self._page.append(anim_par)
668 anim_par.parent = self._page
669 anim_seq = sub_el(anim_par, 'anim:seq', attrib={'presentation:node-type':'main-sequence'})
670 for a in self.animations:
671 a_node = a.get_node()
672 anim_seq.append(a_node)
673 a_node.parent = anim_seq
675 # add notes now (so they are last)
676 if self.notes_frame:
677 notes = self.notes_frame.get_node()
678 self._page.append(notes)
679 notes.parent = self._page
680 if self.footer:
681 self._page.attrib['presentation:use-footer-name'] = self.footer.name
682 return self._page
684 def add_text_frame(self, attrib=None):
685 # should adjust width, x based on if existing boxes
686 self.text_frames.append(TextFrame(self, attrib))
687 node = self.text_frames[-1].get_node()
688 self._page.append(node)
689 node.parent = self._page
690 self.cur_element = self.text_frames[-1]
691 return self.text_frames[-1]
693 def add_title_frame(self):
694 self.title_frame = TitleFrame(self)
695 node = self.title_frame.get_node()
696 self._page.append(node)
697 node.parent = self._page
698 self.cur_element = self.title_frame
699 return self.title_frame
701 def add_notes_frame(self):
702 self.notes_frame = NotesFrame(self)
703 self.page_number_listeners.append(self.notes_frame)
704 self.cur_element = self.notes_frame
705 return self.notes_frame
707 def add_list(self, bl):
709 note that this pushes the cur_element, but doesn't pop it.
710 You'll need to do that
712 # text:list doesn't like being a child of text:p
713 if self.cur_element is None:
714 self.add_text_frame()
715 self.push_element()
716 self.cur_element._text_box.append(bl.node)
717 bl.node.parent = self.cur_element._text_box
718 style = bl.style_name
719 if style not in self._preso._styles_added:
720 self._preso._styles_added[style] = 1
721 self._preso._auto_styles.append(et.fromstring(bl.default_styles())[0])
722 self.cur_element = bl
724 def add_table(self, t):
726 remember to call pop_element after done with table
728 self.push_element()
729 self._page.append(t.node)
730 t.node.parent = self._page
731 self.cur_element = t
733 def write(self, text, **kw):
734 if self.cur_element is None:
735 self.add_text_frame()
736 self.cur_element.write(text, **kw)
738 def add_node(self, node, attrib=None):
739 attrib = attrib or {}
740 if self.cur_element is None:
741 self.add_text_frame()
742 self.cur_element.add_node(node, attrib)
745 def pop_node(self):
746 self.cur_element.pop_node()
748 def parent_of(self, name):
749 """
750 like pop_node, but traverse up parents. When you find a node
751 with name, set cur_node to that
753 if self.cur_element:
754 self.cur_element.parent_of(name)
756 class MixedContent(object):
757 def __init__(self, slide, name, attrib=None):
758 self._default_align = 'start'
759 self.slide = slide
760 if attrib is None:
761 attrib = {}
762 self.node = el(name, attrib)
763 self.cur_node = self.node
764 # store nodes that affect output (such as text:a)
765 self.pending_nodes = [] # typles of (name, attr)
766 self.dirty = False # keep track if we have been written to
768 def parent_of(self, name):
769 """
770 go to parent of node with name, and set as cur_node. Useful
771 for creating new paragraphs
773 if not self._in_tag(name):
774 return
775 node = self.cur_node
776 while node.tag != name:
777 node = node.parent
778 self.cur_node = node.parent
780 def _in_p(self):
782 Determine if we are already in a text:p, odp doesn't like
783 nested ones too much
785 return self._in_tag('text:p')
787 def _is_last_child(self, tagname, attributes=None):
789 Check if last child of cur_node is tagname with attributes
791 children = self.cur_node.getchildren()
792 if children:
793 result = self._is_node(tagname, attributes, node=children[-1])
794 return result
795 return False
797 def _is_node(self, tagname, attributes=None, node=None):
798 if node is None:
799 node = self.cur_node
800 if attributes:
801 return node.tag == tagname and node.attrib == attributes
802 else:
803 return node.tag == tagname
805 def _in_tag(self, tagname, attributes=None):
807 Determine if we are already in a certain tag.
808 If we give attributes, make sure they match.
810 node = self.cur_node
811 while not node is None:
812 if node.tag == tagname:
813 if attributes and node.attrib == attributes:
814 return True
815 elif attributes:
816 return False
817 return True
818 node = node.parent
819 return False
821 def to_xml(self):
822 return to_xml(self.node)
824 def get_node(self):
825 return self.node
827 def append(self, node):
828 self.cur_node.append(node)
829 node.parent = self.cur_node
831 def _check_add_node(self, parent, name):
832 ''' Returns False if bad to make name a child of parent '''
834 if name == 'text:a':
835 if parent.tag == 'draw:text-box':
836 return False
837 return True
839 def _add_node(self, parent, name, attrib):
840 if not self._check_add_node(parent, name):
841 raise Exception, 'Bad child (%s) for %s)' %(name, parent.tag)
842 new_node = sub_el(parent, name, attrib)
843 return new_node
845 def add_node(self, node_name, attrib=None):
846 if attrib is None:
847 attrib = {}
848 new_node = self._add_node(self.cur_node, node_name, attrib)
849 self.cur_node = new_node
850 return self.cur_node
852 def pop_node(self):
853 if self.cur_node.parent == self.node:
854 # Don't pop too far !!
855 return
856 if self.cur_node.parent is None:
857 return
858 self.cur_node = self.cur_node.parent
860 def _add_styles(self, add_paragraph=True, add_text=True):
861 p_styles = {'fo:text-align':self._default_align}
862 t_styles = {}
863 for s in self.slide.pending_styles:
864 if isinstance(s, ParagraphStyle):
865 p_styles.update(s.styles)
866 elif isinstance(s, TextStyle):
867 t_styles.update(s.styles)
869 para = ParagraphStyle(**p_styles)
871 if add_paragraph or self.slide.paragraph_attribs:
872 p_attrib = {'text:style-name':para.name}
873 p_attrib.update(self.slide.paragraph_attribs)
874 if self._is_last_child('text:p', p_attrib):
875 children = self.cur_node.getchildren()
876 self.cur_node = children[-1]
877 elif not self._in_tag('text:p', p_attrib):
878 self.parent_of('text:p')
879 # Create paragraph style first
880 self.slide._preso.add_style(para)
881 self.add_node('text:p', attrib=p_attrib)
883 # span is only necessary if style changes
884 if add_text and t_styles:
885 text = TextStyle(**t_styles)
886 children = self.cur_node.getchildren()
887 if children:
888 # if we already are using this text style, reuse the last one
889 last = children[-1]
890 if last.tag == 'text:span' and \
891 last.attrib['text:style-name'] == text.name and \
892 last.tail is None: # if we have a tail, we can't reuse
893 self.cur_node = children[-1]
894 return
895 if not self._is_node('text:span', {'text:style-name':text.name}):
896 # Create text style
897 self.slide._preso.add_style(text)
898 self.add_node('text:span', attrib={'text:style-name':text.name})
901 def _add_pending_nodes(self):
902 for node, attr in self.pending_nodes:
903 self.add_node(node, attr)
906 def line_break(self):
907 """insert as many line breaks as the insert_line_break variable says
909 for i in range(self.slide.insert_line_break):
910 # needs to be inside text:p
911 if not self._in_tag('text:p'):
912 # we can just add a text:p and no line-break
913 # Create paragraph style first
914 self.add_node('text:p')
915 else:
916 self.add_node('text:line-break')
917 self.pop_node()
918 if self.cur_node.parent.tag != 'text:p':
919 self.pop_node()
921 self.slide.insert_line_break = 0
924 def write(self, text, add_p_style=True, add_t_style=True):
926 see mixed content
927 http://effbot.org/zone/element-infoset.htm#mixed-content
928 Writing is complicated by requirements of odp to ignore
929 duplicate spaces together. Deal with this by splitting on
930 white spaces then dealing with the '' (empty strings) which
931 would be the extra spaces
933 self.line_break()
934 self._add_styles(add_p_style, add_t_style)
935 self._add_pending_nodes()
937 spaces = []
938 for i, letter in enumerate(text):
939 if letter == ' ':
940 spaces.append(letter)
941 continue
942 elif len(spaces) == 1:
943 self._write(' ')
944 self._write(letter)
945 spaces = []
946 continue
947 elif spaces:
948 num_spaces = len(spaces) - 1
949 # write just a plain space at the start
950 self._write(' ')
951 if num_spaces > 1:
952 # write the attrib only if more than one space
953 self.add_node('text:s', {'text:c':str(num_spaces)})
954 else:
955 self.add_node('text:s')
956 self.pop_node()
957 self._write(letter)
958 spaces = []
959 continue
960 self._write(letter)
962 # might have dangling spaces
963 # if len(spaces) == 1:
964 # self._write(' ')
965 #elif spaces:
966 if spaces:
967 num_spaces = len(spaces)
968 ##num_spaces = len(spaces) - 1
969 # write space
970 ##self._write(' ')
971 if num_spaces > 1:
972 self.add_node('text:s', {'text:c':str(num_spaces)})
973 else:
974 self.add_node('text:s')
975 self.pop_node()
978 def _write(self, letter):
979 children = self.cur_node.getchildren()
980 if children:
981 child = children[-1]
982 cur_text = child.tail or ''
983 child.tail = cur_text + letter
984 else:
985 cur_text = self.cur_node.text or ''
986 self.cur_node.text = cur_text + letter
987 self.dirty = True
989 class Footer(MixedContent):
990 def __init__(self, slide):
991 self._default_align = 'center'
992 MixedContent.__init__(self, slide, 'presentation:footer-decl')
993 self.name = None
995 def get_node(self):
996 if self.name is None:
997 raise Exception("set footer name")
998 self.node.attrib['presentation:name'] = self.name
999 return self.node
1003 class PictureFrame(MixedContent):
1004 def __init__(self, slide, picture, attrib=None):
1005 x,y,w,h = picture.get_xywh()
1006 attrib = attrib or {
1007 'presentation:style-name':'pr2',
1008 'draw:style-name':'gr2',
1009 'draw:layer':'layout',
1010 'svg:width':w, #picture.get_width(),
1011 'svg:height':h, #picture.get_height(),
1012 'svg:x':x, #'1.4cm',
1013 'svg:y':y, #'4.577cm',
1015 attrib = picture.update_frame_attributes(attrib)
1016 MixedContent.__init__(self, slide, 'draw:frame', attrib=attrib)
1019 class TextFrame(MixedContent):
1020 def __init__(self, slide, attrib=None):
1021 attrib = attrib or {
1022 'presentation:style-name':'pr2',
1023 'draw:layer':'layout',
1024 'svg:width':'25.199cm',
1025 'svg:height':'13.86cm',
1026 'svg:x':'1.4cm',
1027 'svg:y':'4.577cm',
1028 'presentation:class':'subtitle'
1031 MixedContent.__init__(self, slide, 'draw:frame', attrib=attrib)
1032 self._text_box = sub_el(self.node, 'draw:text-box')
1033 self.cur_node = self._text_box
1034 self.text_styles = ['P1']
1036 self.cur_node = self._text_box
1039 def to_xml(self):
1040 return to_xml(self.get_node())
1042 def _in_bullet(self):
1043 return self._in_tag('text:list')
1046 class TitleFrame(TextFrame):
1047 def __init__(self, slide, attrib=None):
1048 attrib = attrib or {
1049 'presentation:style-name':'Default-title',
1050 'draw:layer':'layout',
1051 'svg:width':'25.199cm',
1052 'svg:height':'1.737cm',
1053 'svg:x':'1.4cm',
1054 'svg:y':'1.721cm',
1055 'presentation:class':'title'
1058 TextFrame.__init__(self, slide, attrib)
1059 self._default_align = 'center'
1061 class NotesFrame(TextFrame):
1062 def __init__(self, slide, attrib=None):
1063 attrib = attrib or {
1064 'presentation:style-name':'pr1',
1065 'draw:layer':'layout',
1066 'svg:width':'17.271cm',
1067 'svg:height':'12.322cm',
1068 'svg:x':'2.159cm',
1069 'svg:y':'13.271cm',
1070 'presentation:class':'notes',
1071 'presentation:placeholder':'true'
1073 TextFrame.__init__(self, slide, attrib)
1074 self._preso_notes = el('presentation:notes', attrib={'draw:style-name':'dp2'})
1075 self._page_thumbnail = sub_el(self._preso_notes,
1076 'draw:page-thumbnail',
1077 attrib={
1078 'presentation:style-name':'gr1',
1079 'draw:layer':'layout',
1080 'svg:width':'13.968cm',
1081 'svg:height':'10.476cm',
1082 'svg:x':'3.81cm',
1083 'svg:y':'2.123cm',
1084 'draw:page-number':'%d'%slide.page_number,
1085 'presentation:class':'page'})
1086 self._preso_notes.append(self.node)
1087 self.node.parent = self._preso_notes
1089 def new_page_num(self, new_num):
1090 self._page_thumbnail.attrib['draw:page-number']='%d'%new_num
1092 def get_node(self):
1093 return self._preso_notes
1096 class TextStyle(object):
1098 based on
1099 http://books.evc-cit.info/odbook/ch03.html#char-para-styling-section
1101 font_weight = dict(
1102 BOLD = 'bold',
1103 NORMAL = 'normal'
1105 font_style = dict(
1106 ITALIC = 'italic',
1107 NORMAL = 'normal'
1109 text_underline_style = dict(
1110 NONE = 'none',
1111 SOLID = 'solid',
1112 DOTTED = 'dotted',
1113 DASH = 'dash',
1114 LONG_DASH = 'long-dash',
1115 DOT_DASH = 'dot-dash',
1116 DOT_DOT_DASH = 'dot-dot-dash',
1117 WAVE = 'wave'
1119 text_underline_type = dict(
1120 NONE = 'none',
1121 SINGLE = 'single', #default
1122 DOUBLE = 'double'
1124 text_underline_width = dict(
1125 AUTO = 'auto',
1126 NORMAL = 'normal',
1127 BOLD = 'bold',
1128 THIN = 'thin',
1129 DASH = 'dash',
1130 MEDIUM = 'medium',
1131 THICK = 'thick'
1133 text_underline_mode = dict(
1134 SKIP_WHITE_SPACE = 'skip-white-space'
1136 font_variant = dict(
1137 NORMAL = 'normal',
1138 SMALL_CAPS = 'small-caps'
1140 text_transform = dict(
1141 NONE = 'none',
1142 LOWERCASE = 'lowercase',
1143 UPPERCASE = 'uppercase',
1144 CAPITALIZE = 'capitalize',
1145 SMALL_CAPS = 'small-caps'
1147 text_outline = dict(
1148 TRUE = 'true'
1150 text_rotation_angle = dict(
1151 ZERO = '0',
1152 NINETY = '90',
1153 TWOSEVENTY = '270'
1155 text_rotation_scale = dict(
1156 LINE_HEIGHT = 'line-height',
1157 FIXED = 'fixed'
1160 FAMILY = 'text'
1161 STYLE_PROP = 'style:text-properties'
1162 PREFIX = 'T%d'
1163 ATTRIB2NAME = {}
1164 TEXT_COUNT = 0
1165 def __init__(self, **kw):
1167 pass in a dictionary containing the style attributes you want for your text
1169 self.styles = kw
1170 self.name = self._gen_name()
1172 def _gen_name(self):
1173 key = self.styles.items()
1174 key.sort()
1175 key = tuple(key)
1176 if key in self.__class__.ATTRIB2NAME:
1177 return self.__class__.ATTRIB2NAME[key]
1178 else:
1179 name = self.PREFIX % self.__class__.TEXT_COUNT
1180 self.__class__.TEXT_COUNT += 1
1181 self.__class__.ATTRIB2NAME[key] = name
1182 return name
1184 def style_node(self, additional_style_attrib=None):
1186 generate a style node (for automatic-styles)
1188 could specify additional attributes such as
1189 'style:parent-style-name' or 'style:list-style-name'
1192 style_attrib = {'style:name':self.name,
1193 'style:family':self.FAMILY}
1194 if additional_style_attrib:
1195 style_attrib.update(additional_style_attrib)
1197 node = el('style:style', attrib=style_attrib)
1198 props = sub_el(node, self.STYLE_PROP,
1199 attrib=self.styles)
1200 return node
1203 class ParagraphStyle(TextStyle):
1204 text_align = dict(
1205 START = 'start',
1206 END = 'end',
1207 CENTER = 'center',
1208 JUSTIFY = 'justify'
1211 FAMILY = 'paragraph'
1212 STYLE_PROP = 'style:paragraph-properties'
1213 PREFIX = 'P%d'
1216 class OdtCodeFormatter(formatter.Formatter):
1217 def __init__(self, writable, preso):
1218 formatter.Formatter.__init__(self)
1219 self.writable = writable
1220 self.preso = preso
1222 def format(self, source, outfile):
1223 tclass = pygments.token.Token
1224 for ttype, value in source:
1225 # getting ttype, values like (Token.Keyword.Namespace, u'')
1226 if value == '':
1227 continue
1228 style_attrib = self.get_style(ttype)
1229 tstyle = TextStyle(**style_attrib)
1230 self.writable.slide.push_style(tstyle)
1231 if value == '\n':
1232 self.writable.slide.insert_line_break = 1
1233 self.writable.line_break()
1234 else:
1235 parts = value.split('\n')
1236 for part in parts[:-1]:
1237 self.writable.write(part)
1238 self.writable.slide.insert_line_break = 1
1239 self.writable.line_break()
1240 self.writable.write(parts[-1])
1241 self.writable.slide.pop_style()
1242 self.writable.pop_node()
1245 def get_style(self, tokentype):
1246 while not self.style.styles_token(tokentype):
1247 tokentype = tokentype.parent
1248 value = self.style.style_for_token(tokentype)
1249 # default to monospace
1250 results = {
1251 'fo:font-family':MONO_FONT,
1252 'style:font-family-generic':"swiss",
1253 'style:font-pitch':"fixed"}
1254 if value['color']:
1255 results['fo:color'] = '#' + value['color']
1256 if value['bold']:
1257 results['fo:font-weight'] = 'bold'
1258 if value['italic']:
1259 results['fo:font-weight'] = 'italic'
1260 return results
1263 class OutlineList(MixedContent):
1265 see the following for lists
1266 http://books.evc-cit.info/odbook/ch03.html#list-spec-fig
1268 >>> o = OutlineList()
1269 >>> o.new_item('dogs')
1270 >>> o.indent()
1271 >>> o.new_item('small')
1272 >>> o.indent()
1273 >>> o.new_item('weiner')
1274 >>> o.write(' - more junk about German dogs')
1275 >>> o.new_item('fido')
1276 >>> o.dedent()
1277 >>> o.dedent()
1278 >>> o.new_item('cats')
1279 >>> o.to_xml()
1280 '<text:list text:style-name="L1"><text:list_item><text:p text:style-name="P1">dogs</text:p><text:list><text:list_item><text:p text:style-name="P1">small</text:p><text:list><text:list_item><text:p text:style-name="P1">weiner</text:p><text:p text:style-name="P1"> - more junk about German dogs</text:p></text:list_item><text:list_item><text:p text:style-name="P1">fido</text:p></text:list_item></text:list></text:list_item><text:list_item><text:p text:style-name="P1">cats</text:p></text:list_item></text:list></text:list_item></text:list>'
1282 See also:
1283 http://books.evc-cit.info/odbook/ch03.html#bulleted-numbered-lists-section
1285 Bonafide OOo output looks like this:
1287 <draw:text-box>
1288 <text:list text:style-name="L2">
1289 <text:list-item>
1290 <text:p text:style-name="P1">Foo</text:p>
1291 </text:list-item>
1292 <text:list-item>
1293 <text:p text:style-name="P1">Bar</text:p>
1294 </text:list-item>
1295 <text:list-item> <!-- Important for indents!!! -->
1296 <text:list>
1297 <text:list-item>
1298 <text:p text:style-name="P1">barbie</text:p>
1299 </text:list-item>
1300 <text:list-item>
1301 <text:p text:style-name="P1">ken</text:p>
1302 </text:list-item>
1303 </text:list>
1304 </text:list-item>
1305 <text:list-item>
1306 <text:p text:style-name="P1">Baz</text:p>
1307 </text:list-item>
1308 </text:list>
1309 </draw:text-box>
1311 def __init__(self, slide, attrib=None):
1312 self._default_align = 'start'
1313 self.attrib = attrib or {'text:style-name':'L2'}
1314 MixedContent.__init__(self, slide, 'text:list', attrib=self.attrib)
1315 # if slide already has text insert line break
1316 self.line_break()
1318 self.parents = [self.node]
1319 self.level = 0
1320 self.style_file = 'auto_list.xml'
1321 self.style_name = 'default-list'
1322 self.slide.pending_styles.append(ParagraphStyle(**{'text:enable-numbering':'true'}))
1324 def new_item(self, text=None):
1325 li = self._add_node(self.parents[-1], 'text:list-item', {})
1326 self.cur_node = li
1327 if text:
1328 self.write(text)
1330 def indent(self):
1331 self.level += 1
1332 li = self._add_node(self.parents[-1], 'text:list-item', {})
1333 l = self._add_node(li, 'text:list', {})
1334 self.cur_node = l
1335 self.parents.append(self.cur_node)
1337 def dedent(self):
1338 self.level -= 1
1339 self.parents.pop()
1340 self.cur_node = self.parents[-1]
1342 def default_styles(self):
1343 filename = os.path.join(DATA_DIR, self.style_file)
1344 return open(filename).read()
1346 class NumberList(OutlineList):
1347 def __init__(self, slide):
1348 self.attrib = {'text:style-name':'L3'}
1349 OutlineList.__init__(self, slide, self.attrib)
1350 self.style_file = 'number_list.xml'
1351 self.style_name = 'number-list'
1354 class TableFrame(MixedContent):
1356 Tables look like this:
1357 <draw:frame draw:style-name="standard" draw:layer="layout"
1358 svg:width="14.098cm" svg:height="1.943cm"
1359 svg:x="7.01cm" svg:y="10.44cm">
1360 <table:table table:template-name="default"
1361 table:use-first-row-styles="true"
1362 table:use-banding-rows-styles="true">
1363 <table:table-column table:style-name="co1"/>
1364 <table:table-column table:style-name="co1"/>
1365 <table:table-column table:style-name="co1"/>
1366 <table:table-column table:style-name="co1"/>
1367 <table:table-column table:style-name="co2"/>
1368 <table:table-row table:style-name="ro1"
1369 table:default-cell-style-name="ce1">
1370 <table:table-cell>
1371 <text:p>Header
1372 </text:p>
1373 </table:table-cell>
1374 <table:table-cell>
1375 <text:p>2
1376 </text:p>
1377 </table:table-cell>
1378 <table:table-cell>
1379 <text:p>3
1380 </text:p>
1381 </table:table-cell>
1382 <table:table-cell>
1383 <text:p>4
1384 </text:p>
1385 </table:table-cell>
1386 <table:table-cell>
1387 <text:p>5
1388 </text:p>
1389 </table:table-cell>
1390 </table:table-row>
1391 <table:table-row table:style-name="ro1"
1392 table:default-cell-style-name="ce1">
1393 <table:table-cell>
1394 <text:p>row1
1395 </text:p>
1396 </table:table-cell>
1397 <table:table-cell>
1398 <text:p>2
1399 </text:p>
1400 </table:table-cell>
1401 <table:table-cell>
1402 <text:p>3
1403 </text:p>
1404 </table:table-cell>
1405 <table:table-cell>
1406 <text:p>4
1407 </text:p>
1408 </table:table-cell>
1409 <table:table-cell>
1410 <text:p>5
1411 </text:p>
1412 </table:table-cell>
1413 </table:table-row>
1414 </table:table>
1415 <draw:image xlink:href="Pictures/TablePreview1.svm"
1416 xlink:type="simple" xlink:show="embed"
1417 xlink:actuate="onLoad"/>
1418 </draw:frame>
1420 def __init__(self, slide, frame_attrib=None, table_attrib=None):
1421 self.frame_attrib = frame_attrib or {'draw:style-name':'standard',
1422 'draw:layer':'layout',
1423 'svg:width':'25.199cm',#'14.098cm',
1424 #'svg:height':'13.86cm', #'''1.943cm',
1425 'svg:x':'1.4cm',
1426 'svg:y':'147pt'#'24.577m'
1428 MixedContent.__init__(self, slide, 'draw:frame', attrib=self.frame_attrib)
1430 self.attrib = table_attrib or {'table:template-name':'default',
1431 'table:use-first-row-styles':'true',
1432 'table:use-banding-rows-styles':'true'}
1433 self.table = self.add_node('table:table', attrib=self.attrib)
1434 self.row = None
1436 def add_row(self, attrib=None):
1437 # rows always go on the table:table elem
1438 self.cur_node = self.table
1439 attrib = attrib or {'table:style-name':'ro1',
1440 'table:default-cell-style-name':'ce1'}
1441 self.add_node( 'table:table-row', attrib)
1443 def add_cell(self, attrib=None):
1444 self.slide.insert_line_break = 0
1445 if self._in_tag('table:table-cell'):
1446 self.parent_of('table:table-cell')
1447 elif not self._in_tag('table:table-row'):
1448 self.add_row()
1449 self.add_node('table:table-cell', attrib)
1451 def _test():
1452 import doctest
1453 doctest.testmod()
1455 if __name__ == '__main__':
1456 _test()