#!/usr/bin/env python # -*- coding: utf-8 -*- """ Object oriented lib to Open Office Presentations Copyright 2008-2009 Matt Harrison Licensed under Apache License, Version 2.0 (current) """ import copy import cStringIO as sio import xml.etree.ElementTree as et from xml.dom import minidom import os import sys import tempfile try: import pygments from pygments import formatter, lexers pygmentsAvail = True except: print 'Could not import pygments code highlighting will not work' pygmentsAvail = False import zipwrap import Image import imagescale DOC_CONTENT_ATTRIB = { 'office:version': '1.0', 'xmlns:anim':'urn:oasis:names:tc:opendocument:xmlns:animation:1.0', 'xmlns:chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0', 'xmlns:dc': 'http://purl.org/dc/elements/1.1/', 'xmlns:dom': 'http://www.w3.org/2001/xml-events', 'xmlns:dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0', 'xmlns:draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0', 'xmlns:fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0', 'xmlns:form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0', 'xmlns:math': 'http://www.w3.org/1998/Math/MathML', 'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0', 'xmlns:number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0', 'xmlns:office': 'urn:oasis:names:tc:opendocument:xmlns:office:1.0', 'xmlns:presentation': 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0', 'xmlns:ooo': 'http://openoffice.org/2004/office', 'xmlns:oooc': 'http://openoffice.org/2004/calc', 'xmlns:ooow': 'http://openoffice.org/2004/writer', 'xmlns:script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0', 'xmlns:smil':'urn:oasis:names:tc:opendocument:xmlns:smil-compatible:1.0', 'xmlns:style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'xmlns:svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0', 'xmlns:table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0', 'xmlns:text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0', 'xmlns:xforms': 'http://www.w3.org/2002/xforms', 'xmlns:xlink': 'http://www.w3.org/1999/xlink', 'xmlns:xsd': 'http://www.w3.org/2001/XMLSchema', 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', } NS2PREFIX = {} for key, value in DOC_CONTENT_ATTRIB.items(): NS2PREFIX[value] = key.split(':')[-1] TEXT_COUNT = 100 DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') MONO_FONT = 'Courier New' # I like 'Envy Code R' NORMAL_FONT = 'Arial' SLIDE_WIDTH = 30 # cm SLIDE_HEIGHT = 21 def cwd_decorator(func): """ decorator to change cwd to directory containing rst for this function """ def wrapper(*args, **kw): cur_dir = os.getcwd() found = False for arg in sys.argv: if arg.endswith(".rst"): found = arg break if found: directory = os.path.dirname(arg) if directory: os.chdir(directory) data = func(*args, **kw) os.chdir(cur_dir) return data return wrapper class PrefixedWriter(et.ElementTree): """ hacked to pass NS2PREFIX to _write """ def write(self, file, encoding="us-ascii"): assert self._root is not None if not hasattr(file, "write"): file = open(file, "wb") if not encoding: encoding = "us-ascii" elif encoding != "utf-8" and encoding != "us-ascii": file.write("\n" % encoding) self._write(file, self._root, encoding, NS2PREFIX) #self._write(file, self._root, encoding, {}) # Wrap etree elements to add parent attribute def el(tag, attrib=None): attrib = attrib or {} el = et.Element(tag, attrib) el.parent = None return el def sub_el(parent, tag, attrib=None): attrib = attrib or {} el = et.SubElement(parent, tag, attrib) el.parent = parent return el def to_xml(node): """ convert an etree node to xml """ fout = sio.StringIO() etree = PrefixedWriter(node) etree.write(fout) xml = fout.getvalue() return xml def pretty_xml(string_input, add_ns=False): """ pretty indent string_input """ if add_ns: elem = '' + string_input + '' doc = minidom.parseString(string_input) if add_ns: s1 = doc.childNodes[0].childNodes[0].toprettyxml(' ') else: s1 = doc.toprettyxml(' ') return s1 class Preso(object): mime_type = 'application/vnd.oasis.opendocument.presentation' def __init__(self): self.slides = [] self.limit_pages = [] # can be list of page numbers (not indexes to export) self._pictures = [] # list of Picture instances self._footer_count = 0 # xml elements self._root = None self._auto_styles = None self._presentation = None self._styles_added = {} self._init_xml() def _init_xml(self): self._root = el('office:document-content', attrib=DOC_CONTENT_ATTRIB) o_scripts = sub_el(self._root, 'office:scripts') self._auto_styles = sub_el(self._root, 'office:automatic-styles') o_body = sub_el(self._root, 'office:body') self._presentation = sub_el(o_body, 'office:presentation') def add_imported_auto_style(self, style_node): self._auto_styles.append(style_node) style_node.parent = self._auto_styles def import_slide(self, preso_file, page_num): odp = zipwrap.ZipWrap(preso_file) content = odp.cat('content.xml') content_tree = et.fromstring(content) 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') try: slide_xml = slides[page_num - 1] except IndexError, e: print "Can't find page_num %d only %d slides" %(page_num, len(slides)) raise if slide_xml: self.slides.append(XMLSlide(self, slide_xml, odp)) def get_data(self, style_file=None): fd, filename = tempfile.mkstemp() zip_odp = self.to_file() if style_file: self.add_otp_style(zip_odp, style_file) zip_odp.zipit(filename) data = open(filename).read() os.close(fd) os.remove(filename) return data def add_otp_style(self, zip_odp, style_file): """ takes the slide content and merges in the style_file """ style = zipwrap.ZipWrap(style_file) if 'Pictures' in style.cat(''): for p in style.cat('Pictures'): picture_file = 'Pictures/'+p zip_odp.touch(picture_file, style.cat(picture_file)) zip_odp.touch('styles.xml', style.cat('styles.xml')) return zip_odp def to_file(self, filename=None): """ >>> p = Preso() >>> z = p.to_file('/tmp/foo.odp') >>> z.cat('/') ['settings.xml', 'META-INF', 'styles.xml', 'meta.xml', 'content.xml', 'mimetype'] """ out = zipwrap.ZipWrap('') out.touch('mimetype', self.mime_type) for p in self._pictures: out.touch('Pictures/%s' % p.internal_name, p.get_data()) out.touch('content.xml', self.to_xml()) out.touch('styles.xml', self.styles_xml()) out.touch('meta.xml', self.meta_xml()) out.touch('settings.xml', self.settings_xml()) out.touch('META-INF/manifest.xml', self.manifest_xml(out)) if filename: out.zipit(filename) return out def manifest_xml(self, zip): content = """ """ files = zip.cat('/') try: files.extend(zip.cat('/Pictures')) except IOError, e: # it's ok to not have pictures ;) pass for filename in files: filetype = '' if filename.endswith('.xml'): filetype = 'text/xml' elif filename.endswith('.jpg'): filetype = 'image/jpeg' elif filename.endswith('.gif'): filetype = 'image/gif' elif filename.endswith('.png'): filetype = 'image/png' elif filename == 'Configurations2/': filetype = 'application/vnd.sun.xml.ui.configuration' content += """ """ % (filetype, filename) content += """""" return content def meta_xml(self): return """ odplib(python) 2008-09-15T11:12:02 2008-10-01T20:32:43 3 PT26M35S """ def settings_xml(self): filename = os.path.join(DATA_DIR, 'settings.xml') return open(filename).read() def styles_xml(self): filename = os.path.join(DATA_DIR, 'styles.xml') data = open(filename).read() if NORMAL_FONT != 'Arial': data = data.replace('fo:font-family="Arial"', 'fo:font-family="%s"' %NORMAL_FONT) return data def to_xml(self): for i, slide in enumerate(self.slides): if self.limit_pages and i+1 not in self.limit_pages: continue if slide.footer: footer_node = slide.footer.get_node() self._presentation.append(footer_node) footer_node.parent = self._presentation node = slide.get_node() self._presentation.append(node) node.parent = self._presentation return to_xml(self._root) def add_style(self, style): name = style.name node = style.style_node() if name not in self._styles_added: self._styles_added[name] = 1 self._auto_styles.append(node) def add_slide(self): pnum = len(self.slides)+1 s = Slide(self, page_number=pnum) self.slides.append(s) return s def copy_slide(self, s): new_s = s._copy() self.slides.append(new_s) return new_s def add_footer(self, f): f.name = 'ftr%d'%(self._footer_count) self._footer_count += 1 self.slides[-1].footer = f class Animation(object): ANIM_COUNT = 1 def __init__(self): self.id = self._get_id() def _get_id(self): my_id = "id%d" % self.__class__.ANIM_COUNT self.__class__.ANIM_COUNT += 1 return my_id def get_node(self): """ """ par = el('anim:par', attrib={'smil:begin':'next'}) par2 = sub_el(par, 'anim:par', attrib={'smil:begin':'0s'}) par3 = sub_el(par2, 'anim:par', attrib={'smil:begin':'0s', 'smil:fill':'hold', 'presentation:node-type':'on-click', 'presentation:preset-class':'entrance', 'presentation:preset-id':'ooo-entrance-appear'}) anim_set = sub_el(par3, 'anim:set', attrib={'smil:begin':'0s', 'smil:dur':'0.001s', 'smil:fill':'hold', 'smil:targetElement':self.id, 'anim:sub-item':'text', 'smil:attributeName':'visibility', 'smil:to':'visible'}) return par class ImportedPicture(object): """ Pictures used when importing slides """ def __init__(self, name, data): self.internal_name = name self.data = data def get_data(self): return self.data class Picture(object): """ Need to convert to use image scale:: im = imagescale.ImageScale(uri) x, y, w, h = im.adjust_size(WIDTH, HEIGHT) x_str = "%fcm" % x y_str = "%fcm" % y w_str = "%fcm" % w h_str = "%fcm" % h frame = self._create_frame(attrib={ "draw:style-name":style_name, "draw:text-style-name":"P6", "draw:layer":"layout", "svg:width":w_str, #"31.585cm", "svg:height":h_str, #"21cm", "svg:x":x_str, #"-1.781cm", "svg:y":y_str #"0cm" }) """ COUNT = 0 CM_SCALE = 30. def __init__(self, filepath, **kw): self.filepath = filepath image = Image.open(filepath) self.w, self.h = image.size self.internal_name = self._gen_name() self.user_defined = {} self._process_kw(kw) def update_frame_attributes(self, attrib): """ For positioning update the frame """ if 'align' in self.user_defined: align = self.user_defined['align'] if 'top' in align: attrib['style:vertical-pos'] = 'top' if 'right' in align: attrib['style:horizontal-pos'] = 'right' return attrib def _process_kw(self, kw): self.user_defined = kw def _gen_name(self): ext = os.path.splitext(self.filepath)[1] name = str(Picture.COUNT) + ext Picture.COUNT += 1 return name def get_xywh(self, measurement=None): if measurement is None or measurement == 'cm': measurement = 'cm' scale = Picture.CM_SCALE DPCM = 1 # dots per cm if 'crop' in self.user_defined.get('classes', []): x,y,w,h = imagescale.adjust_crop(SLIDE_WIDTH*DPCM, SLIDE_HEIGHT*DPCM,self.w, self.h) elif 'fit' in self.user_defined.get('classes', []): x,y,w,h = imagescale.adjust_fit(SLIDE_WIDTH*DPCM, SLIDE_HEIGHT*DPCM,self.w, self.h) elif 'fill' in self.user_defined.get('classes', []): x,y,w,h = 0,0,SLIDE_WIDTH,SLIDE_HEIGHT else: x,y,w,h = 1.4, 4.6, self.get_width(), self.get_height() return [str(foo)+measurement for foo in [x,y,w,h]] def get_width(self, measurement=None): if measurement is None or measurement == 'cm': measurement = 'cm' scale = Picture.CM_SCALE if 'width' in self.user_defined: return self.user_defined['width'] + 'pt' if 'scale' in self.user_defined: return '%spt' % (self.w * float(self.user_defined['scale'])/100) return str(self.w/scale) def get_height(self, measurement=None): if measurement is None: measurement = 'cm' if measurement == 'cm': scale = Picture.CM_SCALE if 'height' in self.user_defined: return self.user_defined['height'] + 'pt' if 'scale' in self.user_defined: return '%spt' % (self.h * float(self.user_defined['scale'])/100) return str(self.h/scale) def get_data(self): return open(self.filepath).read() class XMLSlide(object): PREFIX = 'IMPORT%d-%s' COUNT = 0 def __init__(self, preso, node, odp_zipwrap): self.preso = preso self.page_node = node self.footer = None self.mangled = self._mangle_name() self._init(odp_zipwrap) def page_num(self): """ not an int, usually 'Slide 1' or 'page1' """ name = self.page_node.attrib.get('{urn:oasis:names:tc:opendocument:xmlns:drawing:1.0}name', None) return name def _mangle_name(self): name = self.PREFIX%(self.COUNT, self.page_num()) self.COUNT += 1 return name def _init(self, odp_zipwrap): # pull pictures out of slide images = self.page_node.findall('*/{urn:oasis:names:tc:opendocument:xmlns:drawing:1.0}image') for image in images: path = image.attrib.get('{http://www.w3.org/1999/xlink}href') data = odp_zipwrap.cat(path) name = path.split('/')[1] self.preso._pictures.append(ImportedPicture(name, data)) # pull styles out of content.xml (draw:style-name, draw:text-style-name, text:style-name) styles_to_copy = {} #map of (attr_name, value) to value attr_names = ['{urn:oasis:names:tc:opendocument:xmlns:drawing:1.0}style-name', '{urn:oasis:names:tc:opendocument:xmlns:drawing:1.0}text-style-name', '{urn:oasis:names:tc:opendocument:xmlns:text:1.0}style-name'] for node in self.page_node.getiterator(): for attr_name in attr_names: style = node.attrib.get(attr_name, None) if style: styles_to_copy[style] = attr_name # mangle name node.attrib[attr_name] = self.mangled + style auto_attr_names = ['{urn:oasis:names:tc:opendocument:xmlns:style:1.0}name'] found = {} # get content.xml automatic-styles content = odp_zipwrap.cat('content.xml') content_node = et.fromstring(content) auto_node = content_node.findall('{urn:oasis:names:tc:opendocument:xmlns:office:1.0}automatic-styles')[0] for node in auto_node.getchildren(): for attr_name in auto_attr_names: attr_value = node.attrib.get(attr_name, None) if attr_value in styles_to_copy: found[attr_value] = 1 # mangle name node.attrib[attr_name] = self.mangled + attr_value self.preso.add_imported_auto_style(node) def get_node(self): return self.page_node class Slide(object): def __init__(self, preso, page_number=None): self.title_frame = None self.text_frames = [] self._cur_text_frame = None self.pic_frame = None self._preso = preso self.footer_frame = None self.notes_frame = None self.page_number = page_number self.bullet_list = None # current bullet list self.footer = None self.animations = [] self.paragraph_attribs = {} # used to mark id's for animations self.page_number_listeners = [self] self.pending_styles = [] self.element_stack = [] # allow us to push pop self.cur_element = None # if we write it could be to title, # text or notes (Subclass of # MixedContent) self.insert_line_break = 0 # xml elements self._page = None self._init_xml() def insert_line_breaks(self): """ If you want to write out existing line breaks, but don't have content to write call this """ if self.cur_element: #self.cur_element.line_break() self.cur_element.write('') def start_animation(self, anim): self.animations.append(anim) self.paragraph_attribs['text:id'] = anim.id def end_animation(self): # jump out of text:p self.parent_of('text:p') if 'text:id' in self.paragraph_attribs: del self.paragraph_attribs['text:id'] def push_pending_node(self, name, attr): """ pending nodes are for affecting type, such as wrapping content with text:a to make a hyperlink. Anything in pending nodes will be written before the actual text. User needs to remember to pop out of it. """ if self.cur_element is None: self.add_text_frame() self.cur_element.pending_nodes.append((name,attr)) def push_style(self, style): if self.cur_element is None: self.add_text_frame() self.pending_styles.append(style) def pop_style(self): popped = self.pending_styles.pop() def add_code(self, code, language): if self.cur_element is None: self.add_text_frame() style = ParagraphStyle(**{'fo:text-align':'start'}) self.push_style(style) output = pygments.highlight(code, lexers.get_lexer_by_name(language, stripall=True), OdtCodeFormatter(self.cur_element, self._preso)) self.pop_style() self.pop_node() def add_picture(self, p): """ needs to look like this (under draw:page) """ # pictures should be added the the draw:frame element self.pic_frame = PictureFrame(self, p) self.pic_frame.add_node('draw:image', attrib={'xlink:href': 'Pictures/' + p.internal_name, 'xlink:type':'simple', 'xlink:show':'embed', 'xlink:actuate':'onLoad' }) self._preso._pictures.append(p) node = self.pic_frame.get_node() self._page.append(node) node.parent = self._page def push_element(self): """ element push/pop is used to remember previous cur_elem, since lists might need to mess with that""" self.element_stack.append(self.cur_element) def pop_element(self): self.cur_element = self.element_stack.pop() def to_xml(self): node = self.get_node() return to_xml(node) def _fire_page_number(self, new_num): for listener in self.page_number_listeners: listener.new_page_num(new_num) def new_page_num(self, new_num): self._page.attrib['draw:name'] = 'page%d' % self.page_number def _copy(self): ''' needs to update page numbers ''' ins = copy.copy(self) ins._fire_page_number(self.page_number+1) return ins def _init_xml(self): self._page = el('draw:page', attrib={ 'draw:name':'page%d' % self.page_number, 'draw:style-name':'dp1', 'draw:master-page-name':'Default', 'presentation:presentation-page-layout-name':'AL1T0' }) office_forms = sub_el(self._page, 'office:forms', attrib={'form:automatic-focus':'false', 'form:apply-design-mode':'false'}) def get_node(self): """return etree Element representing this slide""" # already added title, text frames # add animation chunks if self.animations: anim_par = el('anim:par', attrib={'presentation:node-type':'timing-root'}) self._page.append(anim_par) anim_par.parent = self._page anim_seq = sub_el(anim_par, 'anim:seq', attrib={'presentation:node-type':'main-sequence'}) for a in self.animations: a_node = a.get_node() anim_seq.append(a_node) a_node.parent = anim_seq # add notes now (so they are last) if self.notes_frame: notes = self.notes_frame.get_node() self._page.append(notes) notes.parent = self._page if self.footer: self._page.attrib['presentation:use-footer-name'] = self.footer.name return self._page def add_text_frame(self, attrib=None): # should adjust width, x based on if existing boxes self.text_frames.append(TextFrame(self, attrib)) node = self.text_frames[-1].get_node() self._page.append(node) node.parent = self._page self.cur_element = self.text_frames[-1] return self.text_frames[-1] def add_title_frame(self): self.title_frame = TitleFrame(self) node = self.title_frame.get_node() self._page.append(node) node.parent = self._page self.cur_element = self.title_frame return self.title_frame def add_notes_frame(self): self.notes_frame = NotesFrame(self) self.page_number_listeners.append(self.notes_frame) self.cur_element = self.notes_frame return self.notes_frame def add_list(self, bl): """ note that this pushes the cur_element, but doesn't pop it. You'll need to do that """ # text:list doesn't like being a child of text:p if self.cur_element is None: self.add_text_frame() self.push_element() self.cur_element._text_box.append(bl.node) bl.node.parent = self.cur_element._text_box style = bl.style_name if style not in self._preso._styles_added: self._preso._styles_added[style] = 1 self._preso._auto_styles.append(et.fromstring(bl.default_styles())[0]) self.cur_element = bl def add_table(self, t): """ remember to call pop_element after done with table """ self.push_element() self._page.append(t.node) t.node.parent = self._page self.cur_element = t def write(self, text, **kw): if self.cur_element is None: self.add_text_frame() self.cur_element.write(text, **kw) def add_node(self, node, attrib=None): attrib = attrib or {} if self.cur_element is None: self.add_text_frame() self.cur_element.add_node(node, attrib) def pop_node(self): self.cur_element.pop_node() def parent_of(self, name): """ like pop_node, but traverse up parents. When you find a node with name, set cur_node to that """ if self.cur_element: self.cur_element.parent_of(name) class MixedContent(object): def __init__(self, slide, name, attrib=None): self._default_align = 'start' self.slide = slide if attrib is None: attrib = {} self.node = el(name, attrib) self.cur_node = self.node # store nodes that affect output (such as text:a) self.pending_nodes = [] # typles of (name, attr) self.dirty = False # keep track if we have been written to def parent_of(self, name): """ go to parent of node with name, and set as cur_node. Useful for creating new paragraphs """ if not self._in_tag(name): return node = self.cur_node while node.tag != name: node = node.parent self.cur_node = node.parent def _in_p(self): """ Determine if we are already in a text:p, odp doesn't like nested ones too much """ return self._in_tag('text:p') def _is_last_child(self, tagname, attributes=None): """ Check if last child of cur_node is tagname with attributes """ children = self.cur_node.getchildren() if children: result = self._is_node(tagname, attributes, node=children[-1]) return result return False def _is_node(self, tagname, attributes=None, node=None): if node is None: node = self.cur_node if attributes: return node.tag == tagname and node.attrib == attributes else: return node.tag == tagname def _in_tag(self, tagname, attributes=None): """ Determine if we are already in a certain tag. If we give attributes, make sure they match. """ node = self.cur_node while not node is None: if node.tag == tagname: if attributes and node.attrib == attributes: return True elif attributes: return False return True node = node.parent return False def to_xml(self): return to_xml(self.node) def get_node(self): return self.node def append(self, node): self.cur_node.append(node) node.parent = self.cur_node def _check_add_node(self, parent, name): ''' Returns False if bad to make name a child of parent ''' if name == 'text:a': if parent.tag == 'draw:text-box': return False return True def _add_node(self, parent, name, attrib): if not self._check_add_node(parent, name): raise Exception, 'Bad child (%s) for %s)' %(name, parent.tag) new_node = sub_el(parent, name, attrib) return new_node def add_node(self, node_name, attrib=None): if attrib is None: attrib = {} new_node = self._add_node(self.cur_node, node_name, attrib) self.cur_node = new_node return self.cur_node def pop_node(self): if self.cur_node.parent == self.node: # Don't pop too far !! return if self.cur_node.parent is None: return self.cur_node = self.cur_node.parent def _add_styles(self, add_paragraph=True, add_text=True): p_styles = {'fo:text-align':self._default_align} t_styles = {} for s in self.slide.pending_styles: if isinstance(s, ParagraphStyle): p_styles.update(s.styles) elif isinstance(s, TextStyle): t_styles.update(s.styles) para = ParagraphStyle(**p_styles) if add_paragraph or self.slide.paragraph_attribs: p_attrib = {'text:style-name':para.name} p_attrib.update(self.slide.paragraph_attribs) if self._is_last_child('text:p', p_attrib): children = self.cur_node.getchildren() self.cur_node = children[-1] elif not self._in_tag('text:p', p_attrib): self.parent_of('text:p') # Create paragraph style first self.slide._preso.add_style(para) self.add_node('text:p', attrib=p_attrib) # span is only necessary if style changes if add_text and t_styles: text = TextStyle(**t_styles) children = self.cur_node.getchildren() if children: # if we already are using this text style, reuse the last one last = children[-1] if last.tag == 'text:span' and \ last.attrib['text:style-name'] == text.name and \ last.tail is None: # if we have a tail, we can't reuse self.cur_node = children[-1] return if not self._is_node('text:span', {'text:style-name':text.name}): # Create text style self.slide._preso.add_style(text) self.add_node('text:span', attrib={'text:style-name':text.name}) def _add_pending_nodes(self): for node, attr in self.pending_nodes: self.add_node(node, attr) def line_break(self): """insert as many line breaks as the insert_line_break variable says """ for i in range(self.slide.insert_line_break): # needs to be inside text:p if not self._in_tag('text:p'): # we can just add a text:p and no line-break # Create paragraph style first self.add_node('text:p') #else: self.add_node('text:line-break') self.pop_node() if self.cur_node.tag == 'text:p': return if self.cur_node.parent.tag != 'text:p': self.pop_node() self.slide.insert_line_break = 0 def write(self, text, add_p_style=True, add_t_style=True): """ see mixed content http://effbot.org/zone/element-infoset.htm#mixed-content Writing is complicated by requirements of odp to ignore duplicate spaces together. Deal with this by splitting on white spaces then dealing with the '' (empty strings) which would be the extra spaces """ self._add_styles(add_p_style, add_t_style) self.line_break() self._add_pending_nodes() spaces = [] for i, letter in enumerate(text): if letter == ' ': spaces.append(letter) continue elif len(spaces) == 1: self._write(' ') self._write(letter) spaces = [] continue elif spaces: num_spaces = len(spaces) - 1 # write just a plain space at the start self._write(' ') if num_spaces > 1: # write the attrib only if more than one space self.add_node('text:s', {'text:c':str(num_spaces)}) else: self.add_node('text:s') self.pop_node() self._write(letter) spaces = [] continue self._write(letter) # might have dangling spaces # if len(spaces) == 1: # self._write(' ') #elif spaces: if spaces: num_spaces = len(spaces) ##num_spaces = len(spaces) - 1 # write space ##self._write(' ') if num_spaces > 1: self.add_node('text:s', {'text:c':str(num_spaces)}) else: self.add_node('text:s') self.pop_node() def _write(self, letter): children = self.cur_node.getchildren() if children: child = children[-1] cur_text = child.tail or '' child.tail = cur_text + letter else: cur_text = self.cur_node.text or '' self.cur_node.text = cur_text + letter self.dirty = True class Footer(MixedContent): def __init__(self, slide): self._default_align = 'center' MixedContent.__init__(self, slide, 'presentation:footer-decl') self.name = None def get_node(self): if self.name is None: raise Exception("set footer name") self.node.attrib['presentation:name'] = self.name return self.node class PictureFrame(MixedContent): def __init__(self, slide, picture, attrib=None): x,y,w,h = picture.get_xywh() attrib = attrib or { 'presentation:style-name':'pr2', 'draw:style-name':'gr2', 'draw:layer':'layout', 'svg:width':w, #picture.get_width(), 'svg:height':h, #picture.get_height(), 'svg:x':x, #'1.4cm', 'svg:y':y, #'4.577cm', } attrib = picture.update_frame_attributes(attrib) MixedContent.__init__(self, slide, 'draw:frame', attrib=attrib) class TextFrame(MixedContent): def __init__(self, slide, attrib=None): attrib = attrib or { 'presentation:style-name':'pr2', 'draw:layer':'layout', 'svg:width':'25.199cm', 'svg:height':'13.86cm', 'svg:x':'1.4cm', 'svg:y':'4.577cm', 'presentation:class':'subtitle' } MixedContent.__init__(self, slide, 'draw:frame', attrib=attrib) self._text_box = sub_el(self.node, 'draw:text-box') self.cur_node = self._text_box self.text_styles = ['P1'] self.cur_node = self._text_box def to_xml(self): return to_xml(self.get_node()) def _in_bullet(self): return self._in_tag('text:list') class TitleFrame(TextFrame): def __init__(self, slide, attrib=None): attrib = attrib or { 'presentation:style-name':'Default-title', 'draw:layer':'layout', 'svg:width':'25.199cm', 'svg:height':'1.737cm', 'svg:x':'1.4cm', 'svg:y':'1.721cm', 'presentation:class':'title' } TextFrame.__init__(self, slide, attrib) self._default_align = 'center' class NotesFrame(TextFrame): def __init__(self, slide, attrib=None): attrib = attrib or { 'presentation:style-name':'pr1', 'draw:layer':'layout', 'svg:width':'17.271cm', 'svg:height':'12.322cm', 'svg:x':'2.159cm', 'svg:y':'13.271cm', 'presentation:class':'notes', 'presentation:placeholder':'true' } TextFrame.__init__(self, slide, attrib) self._preso_notes = el('presentation:notes', attrib={'draw:style-name':'dp2'}) self._page_thumbnail = sub_el(self._preso_notes, 'draw:page-thumbnail', attrib={ 'presentation:style-name':'gr1', 'draw:layer':'layout', 'svg:width':'13.968cm', 'svg:height':'10.476cm', 'svg:x':'3.81cm', 'svg:y':'2.123cm', 'draw:page-number':'%d'%slide.page_number, 'presentation:class':'page'}) self._preso_notes.append(self.node) self.node.parent = self._preso_notes def new_page_num(self, new_num): self._page_thumbnail.attrib['draw:page-number']='%d'%new_num def get_node(self): return self._preso_notes class TextStyle(object): """ based on http://books.evc-cit.info/odbook/ch03.html#char-para-styling-section """ font_weight = dict( BOLD = 'bold', NORMAL = 'normal' ) font_style = dict( ITALIC = 'italic', NORMAL = 'normal' ) text_underline_style = dict( NONE = 'none', SOLID = 'solid', DOTTED = 'dotted', DASH = 'dash', LONG_DASH = 'long-dash', DOT_DASH = 'dot-dash', DOT_DOT_DASH = 'dot-dot-dash', WAVE = 'wave' ) text_underline_type = dict( NONE = 'none', SINGLE = 'single', #default DOUBLE = 'double' ) text_underline_width = dict( AUTO = 'auto', NORMAL = 'normal', BOLD = 'bold', THIN = 'thin', DASH = 'dash', MEDIUM = 'medium', THICK = 'thick' ) text_underline_mode = dict( SKIP_WHITE_SPACE = 'skip-white-space' ) font_variant = dict( NORMAL = 'normal', SMALL_CAPS = 'small-caps' ) text_transform = dict( NONE = 'none', LOWERCASE = 'lowercase', UPPERCASE = 'uppercase', CAPITALIZE = 'capitalize', SMALL_CAPS = 'small-caps' ) text_outline = dict( TRUE = 'true' ) text_rotation_angle = dict( ZERO = '0', NINETY = '90', TWOSEVENTY = '270' ) text_rotation_scale = dict( LINE_HEIGHT = 'line-height', FIXED = 'fixed' ) FAMILY = 'text' STYLE_PROP = 'style:text-properties' PREFIX = 'T%d' ATTRIB2NAME = {} TEXT_COUNT = 0 def __init__(self, **kw): ''' pass in a dictionary containing the style attributes you want for your text ''' self.styles = kw self.name = self._gen_name() def _gen_name(self): key = self.styles.items() key.sort() key = tuple(key) if key in self.__class__.ATTRIB2NAME: return self.__class__.ATTRIB2NAME[key] else: name = self.PREFIX % self.__class__.TEXT_COUNT self.__class__.TEXT_COUNT += 1 self.__class__.ATTRIB2NAME[key] = name return name def style_node(self, additional_style_attrib=None): """ generate a style node (for automatic-styles) could specify additional attributes such as 'style:parent-style-name' or 'style:list-style-name' """ style_attrib = {'style:name':self.name, 'style:family':self.FAMILY} if additional_style_attrib: style_attrib.update(additional_style_attrib) node = el('style:style', attrib=style_attrib) props = sub_el(node, self.STYLE_PROP, attrib=self.styles) return node class ParagraphStyle(TextStyle): text_align = dict( START = 'start', END = 'end', CENTER = 'center', JUSTIFY = 'justify' ) FAMILY = 'paragraph' STYLE_PROP = 'style:paragraph-properties' PREFIX = 'P%d' if pygmentsAvail: class OdtCodeFormatter(formatter.Formatter): def __init__(self, writable, preso): formatter.Formatter.__init__(self) self.writable = writable self.preso = preso self.seen = [] def format(self, source, outfile): tclass = pygments.token.Token # push default style default_style_attrib = self.get_style(tclass.Text) self.writable.slide.push_style(TextStyle(**default_style_attrib)) for ttype, value in source: self.seen.append(value) # getting ttype, values like (Token.Keyword.Namespace, u'') if value == '': continue style_attrib = self.get_style(ttype) tstyle = TextStyle(**style_attrib) self.writable.slide.push_style(tstyle) if value == '\n': self.writable.slide.insert_line_break = 1 self.writable.write('') # will insert break/formatting else: parts = value.split('\n') for part in parts[:-1]: self.writable.write(part) self.writable.slide.insert_line_break = 1 self.writable.write('') #insert break self.writable.write(parts[-1]) self.writable.slide.pop_style() self.writable.pop_node() self.writable.slide.pop_style() def get_style(self, tokentype): while not self.style.styles_token(tokentype): tokentype = tokentype.parent value = self.style.style_for_token(tokentype) # default to monospace results = { 'fo:font-family':MONO_FONT, 'style:font-family-generic':"swiss", 'style:font-pitch':"fixed"} if value['color']: results['fo:color'] = '#' + value['color'] if value['bold']: results['fo:font-weight'] = 'bold' if value['italic']: results['fo:font-weight'] = 'italic' return results class OutlineList(MixedContent): """ see the following for lists http://books.evc-cit.info/odbook/ch03.html#list-spec-fig >>> o = OutlineList() >>> o.new_item('dogs') >>> o.indent() >>> o.new_item('small') >>> o.indent() >>> o.new_item('weiner') >>> o.write(' - more junk about German dogs') >>> o.new_item('fido') >>> o.dedent() >>> o.dedent() >>> o.new_item('cats') >>> o.to_xml() 'dogssmallweiner - more junk about German dogsfidocats' See also: http://books.evc-cit.info/odbook/ch03.html#bulleted-numbered-lists-section Bonafide OOo output looks like this: Foo Bar barbie ken Baz """ def __init__(self, slide, attrib=None): self._default_align = 'start' self.attrib = attrib or {'text:style-name':'L2'} MixedContent.__init__(self, slide, 'text:list', attrib=self.attrib) self.slide.insert_line_break = 0 self.parents = [self.node] self.level = 0 self.style_file = 'auto_list.xml' self.style_name = 'default-list' self.slide.pending_styles.append(ParagraphStyle(**{'text:enable-numbering':'true'})) def new_item(self, text=None): li = self._add_node(self.parents[-1], 'text:list-item', {}) self.cur_node = li if text: self.write(text) def indent(self): self.level += 1 li = self._add_node(self.parents[-1], 'text:list-item', {}) l = self._add_node(li, 'text:list', {}) self.cur_node = l self.parents.append(self.cur_node) def dedent(self): self.level -= 1 self.parents.pop() self.cur_node = self.parents[-1] def default_styles(self): filename = os.path.join(DATA_DIR, self.style_file) return open(filename).read() class NumberList(OutlineList): def __init__(self, slide): self.attrib = {'text:style-name':'L3'} OutlineList.__init__(self, slide, self.attrib) self.style_file = 'number_list.xml' self.style_name = 'number-list' class TableFrame(MixedContent): """ Tables look like this: Header 2 3 4 5 row1 2 3 4 5 """ def __init__(self, slide, frame_attrib=None, table_attrib=None): self.frame_attrib = frame_attrib or {'draw:style-name':'standard', 'draw:layer':'layout', 'svg:width':'25.199cm',#'14.098cm', #'svg:height':'13.86cm', #'''1.943cm', 'svg:x':'1.4cm', 'svg:y':'147pt'#'24.577m' } MixedContent.__init__(self, slide, 'draw:frame', attrib=self.frame_attrib) self.attrib = table_attrib or {'table:template-name':'default', 'table:use-first-row-styles':'true', 'table:use-banding-rows-styles':'true'} self.table = self.add_node('table:table', attrib=self.attrib) self.row = None def add_row(self, attrib=None): # rows always go on the table:table elem self.cur_node = self.table attrib = attrib or {'table:style-name':'ro1', 'table:default-cell-style-name':'ce1'} self.add_node( 'table:table-row', attrib) def add_cell(self, attrib=None): self.slide.insert_line_break = 0 if self._in_tag('table:table-cell'): self.parent_of('table:table-cell') elif not self._in_tag('table:table-row'): self.add_row() self.add_node('table:table-cell', attrib) def _test(): import doctest doctest.testmod() if __name__ == '__main__': _test()