2 # -*- coding: utf-8 -*-
4 Object oriented lib to Open Office Presentations
6 Copyright 2008-2009 Matt Harrison
7 Licensed under Apache License, Version 2.0 (current)
11 import cStringIO
as sio
12 import xml
.etree
.ElementTree
as et
13 from xml
.dom
import minidom
19 from pygments
import formatter
, lexers
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',
55 for key
, value
in DOC_CONTENT_ATTRIB
.items():
56 NS2PREFIX
[value
] = key
.split(':')[-1]
60 DATA_DIR
= os
.path
.join(os
.path
.dirname(__file__
), 'data')
62 MONO_FONT
= 'Courier New' # I like 'Envy Code R'
68 def cwd_decorator(func
):
70 decorator to change cwd to directory containing rst for this function
72 def wrapper(*args
, **kw
):
76 if arg
.endswith(".rst"):
80 directory
= os
.path
.dirname(arg
)
83 data
= func(*args
, **kw
)
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")
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
)
108 def sub_el(parent
, tag
, attrib
=None):
109 attrib
= attrib
or {}
110 el
= et
.SubElement(parent
, tag
, attrib
)
115 """ convert an etree node to xml """
116 fout
= sio
.StringIO()
117 etree
= PrefixedWriter(node
)
119 xml
= fout
.getvalue()
122 def pretty_xml(string_input
, add_ns
=False):
123 """ pretty indent string_input """
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
)
131 s1
= doc
.childNodes
[0].childNodes
[0].toprettyxml(' ')
133 s1
= doc
.toprettyxml(' ')
138 mime_type
= 'application/vnd.oasis.opendocument.presentation'
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
147 self
._auto
_styles
= None
148 self
._presentation
= None
150 self
._styles
_added
= {}
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')
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
))
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()
182 self
.add_otp_style(zip_odp
, style_file
)
183 zip_odp
.zipit(filename
)
184 data
= open(filename
).read()
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'))
201 def to_file(self
, filename
=None):
204 >>> z = p.to_file('/tmp/foo.odp')
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
))
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="/"/>
228 files
.extend(zip.cat('/Pictures'))
230 # it's ok to not have pictures ;)
232 for filename
in files
:
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>"""
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">
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"/>
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
)
282 for i
, slide
in enumerate(self
.slides
):
283 if self
.limit_pages
and i
+1 not in self
.limit_pages
:
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
):
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
)
304 pnum
= len(self
.slides
)+1
305 s
= Slide(self
, page_number
=pnum
)
306 self
.slides
.append(s
)
309 def copy_slide(self
, s
):
311 self
.slides
.append(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):
323 self
.id = self
._get
_id
()
326 my_id
= "id%d" % self
.__class
__.ANIM_COUNT
327 self
.__class
__.ANIM_COUNT
+= 1
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"/>
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',
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',
350 'smil:targetElement':self
.id,
351 'anim:sub-item':'text',
352 'smil:attributeName':'visibility',
353 'smil:to':'visible'})
357 class ImportedPicture(object):
359 Pictures used when importing slides
361 def __init__(self
, name
, data
):
362 self
.internal_name
= name
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)
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",
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
= {}
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']
404 attrib
['style:vertical-pos'] = 'top'
406 attrib
['style:horizontal-pos'] = 'right'
409 def _process_kw(self
, kw
):
410 self
.user_defined
= kw
413 ext
= os
.path
.splitext(self
.filepath
)[1]
414 name
= str(Picture
.COUNT
) + ext
418 def get_xywh(self
, measurement
=None):
419 if measurement
is None or 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
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':
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:
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
)
458 return open(self
.filepath
).read()
460 class XMLSlide(object):
461 PREFIX
= 'IMPORT%d-%s'
463 def __init__(self
, preso
, node
, odp_zipwrap
):
465 self
.page_node
= node
467 self
.mangled
= self
._mangle
_name
()
468 self
._init
(odp_zipwrap
)
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)
475 def _mangle_name(self
):
476 name
= self
.PREFIX
%(self
.COUNT
, self
.page_num())
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')
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)
499 styles_to_copy
[style
] = attr_name
501 node
.attrib
[attr_name
] = self
.mangled
+ style
503 auto_attr_names
= ['{urn:oasis:names:tc:opendocument:xmlns:style:1.0}name']
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
516 node
.attrib
[attr_name
] = self
.mangled
+ attr_value
517 self
.preso
.add_imported_auto_style(node
)
524 return self
.page_node
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
535 self
.footer_frame
= None
536 self
.notes_frame
= None
537 self
.page_number
= page_number
538 self
.bullet_list
= None # current bullet list
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
550 self
.insert_line_break
= 0
557 def insert_line_breaks(self
):
559 If you want to write out existing line breaks, but don't have content to write
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
):
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
)
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
))
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"/>
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()
634 node
= self
.get_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
645 ''' needs to update page numbers '''
646 ins
= copy
.copy(self
)
647 ins
._fire
_page
_number
(self
.page_number
+1)
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'})
662 """return etree Element representing this slide"""
663 # already added title, text frames
664 # add animation chunks
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)
677 notes
= self
.notes_frame
.get_node()
678 self
._page
.append(notes
)
679 notes
.parent
= self
._page
681 self
._page
.attrib
['presentation:use-footer-name'] = self
.footer
.name
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()
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
729 self
._page
.append(t
.node
)
730 t
.node
.parent
= self
._page
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
)
746 self
.cur_element
.pop_node()
748 def parent_of(self
, name
):
750 like pop_node, but traverse up parents. When you find a node
751 with name, set cur_node to that
754 self
.cur_element
.parent_of(name
)
756 class MixedContent(object):
757 def __init__(self
, slide
, name
, attrib
=None):
758 self
._default
_align
= 'start'
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
):
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
):
776 while node
.tag
!= name
:
778 self
.cur_node
= node
.parent
782 Determine if we are already in a text:p, odp doesn't like
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()
793 result
= self
._is
_node
(tagname
, attributes
, node
=children
[-1])
797 def _is_node(self
, tagname
, attributes
=None, node
=None):
801 return node
.tag
== tagname
and node
.attrib
== attributes
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.
811 while not node
is None:
812 if node
.tag
== tagname
:
813 if attributes
and node
.attrib
== attributes
:
822 return to_xml(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 '''
835 if parent
.tag
== 'draw:text-box':
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
)
845 def add_node(self
, node_name
, attrib
=None):
848 new_node
= self
._add
_node
(self
.cur_node
, node_name
, attrib
)
849 self
.cur_node
= new_node
853 if self
.cur_node
.parent
== self
.node
:
854 # Don't pop too far !!
856 if self
.cur_node
.parent
is None:
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
}
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()
888 # if we already are using this text style, reuse the last one
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]
895 if not self
._is
_node
('text:span', {'text:style-name':text
.name
}):
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')
916 self
.add_node('text:line-break')
918 if self
.cur_node
.parent
.tag
!= 'text:p':
921 self
.slide
.insert_line_break
= 0
924 def write(self
, text
, add_p_style
=True, add_t_style
=True):
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
934 self
._add
_styles
(add_p_style
, add_t_style
)
935 self
._add
_pending
_nodes
()
938 for i
, letter
in enumerate(text
):
940 spaces
.append(letter
)
942 elif len(spaces
) == 1:
948 num_spaces
= len(spaces
) - 1
949 # write just a plain space at the start
952 # write the attrib only if more than one space
953 self
.add_node('text:s', {'text:c':str(num_spaces
)})
955 self
.add_node('text:s')
962 # might have dangling spaces
963 # if len(spaces) == 1:
967 num_spaces
= len(spaces
)
968 ##num_spaces = len(spaces) - 1
972 self
.add_node('text:s', {'text:c':str(num_spaces
)})
974 self
.add_node('text:s')
978 def _write(self
, letter
):
979 children
= self
.cur_node
.getchildren()
982 cur_text
= child
.tail
or ''
983 child
.tail
= cur_text
+ letter
985 cur_text
= self
.cur_node
.text
or ''
986 self
.cur_node
.text
= cur_text
+ letter
989 class Footer(MixedContent
):
990 def __init__(self
, slide
):
991 self
._default
_align
= 'center'
992 MixedContent
.__init
__(self
, slide
, 'presentation:footer-decl')
996 if self
.name
is None:
997 raise Exception("set footer name")
998 self
.node
.attrib
['presentation:name'] = self
.name
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',
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
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',
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',
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',
1078 'presentation:style-name':'gr1',
1079 'draw:layer':'layout',
1080 'svg:width':'13.968cm',
1081 'svg:height':'10.476cm',
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
1093 return self
._preso
_notes
1096 class TextStyle(object):
1099 http://books.evc-cit.info/odbook/ch03.html#char-para-styling-section
1109 text_underline_style
= dict(
1114 LONG_DASH
= 'long-dash',
1115 DOT_DASH
= 'dot-dash',
1116 DOT_DOT_DASH
= 'dot-dot-dash',
1119 text_underline_type
= dict(
1121 SINGLE
= 'single', #default
1124 text_underline_width
= dict(
1133 text_underline_mode
= dict(
1134 SKIP_WHITE_SPACE
= 'skip-white-space'
1136 font_variant
= dict(
1138 SMALL_CAPS
= 'small-caps'
1140 text_transform
= dict(
1142 LOWERCASE
= 'lowercase',
1143 UPPERCASE
= 'uppercase',
1144 CAPITALIZE
= 'capitalize',
1145 SMALL_CAPS
= 'small-caps'
1147 text_outline
= dict(
1150 text_rotation_angle
= dict(
1155 text_rotation_scale
= dict(
1156 LINE_HEIGHT
= 'line-height',
1161 STYLE_PROP
= 'style:text-properties'
1165 def __init__(self
, **kw
):
1167 pass in a dictionary containing the style attributes you want for your text
1170 self
.name
= self
._gen
_name
()
1172 def _gen_name(self
):
1173 key
= self
.styles
.items()
1176 if key
in self
.__class
__.ATTRIB2NAME
:
1177 return self
.__class
__.ATTRIB2NAME
[key
]
1179 name
= self
.PREFIX
% self
.__class
__.TEXT_COUNT
1180 self
.__class
__.TEXT_COUNT
+= 1
1181 self
.__class
__.ATTRIB2NAME
[key
] = 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
,
1203 class ParagraphStyle(TextStyle
):
1211 FAMILY
= 'paragraph'
1212 STYLE_PROP
= 'style:paragraph-properties'
1216 class OdtCodeFormatter(formatter
.Formatter
):
1217 def __init__(self
, writable
, preso
):
1218 formatter
.Formatter
.__init
__(self
)
1219 self
.writable
= writable
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'')
1228 style_attrib
= self
.get_style(ttype
)
1229 tstyle
= TextStyle(**style_attrib
)
1230 self
.writable
.slide
.push_style(tstyle
)
1232 self
.writable
.slide
.insert_line_break
= 1
1233 self
.writable
.line_break()
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
1251 'fo:font-family':MONO_FONT
,
1252 'style:font-family-generic':"swiss",
1253 'style:font-pitch':"fixed"}
1255 results
['fo:color'] = '#' + value
['color']
1257 results
['fo:font-weight'] = 'bold'
1259 results
['fo:font-weight'] = 'italic'
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')
1271 >>> o.new_item('small')
1273 >>> o.new_item('weiner')
1274 >>> o.write(' - more junk about German dogs')
1275 >>> o.new_item('fido')
1278 >>> o.new_item('cats')
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>'
1283 http://books.evc-cit.info/odbook/ch03.html#bulleted-numbered-lists-section
1285 Bonafide OOo output looks like this:
1288 <text:list text:style-name="L2">
1290 <text:p text:style-name="P1">Foo</text:p>
1293 <text:p text:style-name="P1">Bar</text:p>
1295 <text:list-item> <!-- Important for indents!!! -->
1298 <text:p text:style-name="P1">barbie</text:p>
1301 <text:p text:style-name="P1">ken</text:p>
1306 <text:p text:style-name="P1">Baz</text:p>
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
1318 self
.parents
= [self
.node
]
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', {})
1332 li
= self
._add
_node
(self
.parents
[-1], 'text:list-item', {})
1333 l
= self
._add
_node
(li
, 'text:list', {})
1335 self
.parents
.append(self
.cur_node
)
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">
1391 <table:table-row table:style-name="ro1"
1392 table:default-cell-style-name="ce1">
1415 <draw:image xlink:href="Pictures/TablePreview1.svm"
1416 xlink:type="simple" xlink:show="embed"
1417 xlink:actuate="onLoad"/>
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',
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
)
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'):
1449 self
.add_node('table:table-cell', attrib
)
1455 if __name__
== '__main__':