class odf_tracked_changes(odf_element): def get_changed_regions(self, creator=None, date=None, content=None): return _get_elements(self, 'text:changed-region', dc_creator=creator, dc_date=date, content=content) get_changed_region_list = obsolete('get_changed_region_list', get_changed_regions) def get_changed_region(self, position=0, text_id=None, creator=None, date=None, content=None): return _get_element(self, 'text:changed-region', position, text_id=text_id, dc_creator=creator, dc_date=date, content=content)
def odf_get_container(path_or_file): """Return an odf_container instance of the ODF document stored at the given local path or in the given (open) file-like object. """ return odf_container(path_or_file) def odf_new_container(path_or_file): """Return an odf_container instance based on the given template. """ if path_or_file in ODF_TYPES: path_or_file = _get_abspath(ODF_TYPES[path_or_file]) template_container = odf_get_container(path_or_file) # Return a copy of the template container clone = template_container.clone() # Change type from template to regular mimetype = clone.get_part('mimetype').replace('-template', '') clone.set_part('mimetype', mimetype) # Update the manifest manifest = odf_manifest(ODF_MANIFEST, clone) manifest.set_media_type('/', mimetype) clone.set_part(ODF_MANIFEST, manifest.serialize()) return clone odf_new_document_from_template = obsolete('odf_new_document_from_template', odf_new_container) odf_new_document_from_type = obsolete('odf_new_document_from_template', odf_new_container)
class odf_frame(odf_element): def get_name(self): return self.get_attribute('draw:name') def set_name(self, name): return self.set_attribute('draw:name', name) def get_id(self): return self.get_attribute('draw:id') def set_id(self, frame_id): return self.set_attribute('draw:id', frame_id) def get_style(self): return self.get_attribute('draw:style-name') def set_style(self, name): return self.set_style_attribute('draw:style-name', name) def get_position(self): """Get the position of the frame relative to its anchor point. Position is a (left, top) tuple with items including the unit, e.g. ('10cm', '15cm'). Return: (str, str) """ get_attr = self.get_attribute return get_attr('svg:x'), get_attr('svg:y') def set_position(self, position): """Set the position of the frame relative to its anchor point. Position is a (left, top) tuple with items including the unit, e.g. ('10cm', '15cm'). Arguments: position -- (str, str) """ self.set_attribute('svg:x', str(position[0])) self.set_attribute('svg:y', str(position[1])) def get_size(self): """Get the size of the frame. Size is a (width, height) tuple with items including the unit, e.g. ('10cm', '15cm'). Return: (str, str) """ get_attr = self.get_attribute return get_attr('svg:width'), get_attr('svg:height') def set_size(self, size): """Set the size of the frame. Size is a (width, height) tuple with items including the unit, e.g. ('10cm', '15cm'). The dimensions can be None. Arguments: size -- (str, str) """ self.set_attribute('svg:width', str(size[0])) self.set_attribute('svg:height', str(size[1])) def get_z_index(self): z_index = self.get_attribute('draw:z-index') if z_index is None: return None return int(z_index) def set_z_index(self, z_index): if z_index is None: self.set_attribute('draw:z-index', z_index) return self.set_attribute('draw:z-index', str(z_index)) def get_anchor_type(self): """Get how the frame is attached to its environment. Return: 'page', 'frame', 'paragraph', 'char' or 'as-char' """ return self.get_attribute('text:anchor-type') def set_anchor_type(self, anchor_type, page_number=None): """Set how the frame is attached to its environment. When the type is 'page', you can give the number of the page where to attach. Arguments: anchor_type -- 'page', 'frame', 'paragraph', 'char' or 'as-char' page_number -- int (when anchor_type == 'page') """ self.set_attribute('text:anchor-type', anchor_type) if anchor_type == 'page' and page_number: self.set_page_number(page_number) set_frame_anchor_type = obsolete('set_frame_anchor_type', set_anchor_type) def get_page_number(self): """Get the number of the page where the frame is attached when the anchor type is 'page'. Return: int """ page_number = self.get_attribute('text:anchor-page-number') if page_number is None: return None return int(page_number) def set_page_number(self, page_number): """Set the number of the page where the frame is attached when the anchor type is 'page', or None to delete it Arguments: page_number -- int or None """ if page_number is None: self.set_attribute('text:anchor-page-number', None) self.set_attribute('text:anchor-page-number', str(page_number)) def get_layer(self): return self.get_attribute('draw:layer') def set_layer(self, layer): return self.set_attribute('draw:layer', layer) def get_text_content(self): text_box = self.get_element('draw:text-box') if text_box is None: return None return text_box.get_text_content() def set_text_content(self, text_or_element): text_box = self.get_element('draw:text-box') if text_box is None: text_box = odf_create_element('draw:text-box') self.append(text_box) if isinstance(text_or_element, odf_element): text_box.clear() return text_box.append(text_or_element) return text_box.set_text_content(text_or_element) def get_presentation_class(self): return self.get_attribute('presentation:class') def set_presentation_class(self, presentation_class): return self.set_attribute('presentation:class', presentation_class) def get_presentation_style(self): return self.get_attribute('presentation:style-name') def set_presentation_style(self, name): return self.set_style_attribute('presentation:style-name', name) def get_image(self): return self.get_element('draw:image') def set_image(self, url_or_element, text=None): image = self.get_image() if image is None: if isinstance(url_or_element, odf_element): image = url_or_element self.append(image) else: image = odf_create_image(url_or_element) self.append(image) else: if isinstance(url_or_element, odf_element): image.delete() image = url_or_element self.append(image) else: image.set_url(url_or_element) return image def get_text_box(self): return self.get_element('draw:text-box') def set_text_box(self, text_or_element=None, text_style=None): text_box = self.get_text_box() if text_box is None: text_box = odf_create_element('draw:text-box') self.append(text_box) else: text_box.clear() if not isiterable(text_or_element): text_or_element = [text_or_element] for item in text_or_element: if isinstance(item, unicode): item = odf_create_paragraph(item, style=text_style) text_box.append(item) return text_box def get_formatted_text(self, context): result = [] for element in self.get_children(): tag = element.get_tag() if tag == 'draw:image': if context['rst_mode']: filename = element.get_attribute('xlink:href') # Compute width and height width, height = self.get_size() if width is not None: width = Unit(width) width = width.convert('px', DPI) if height is not None: height = Unit(height) height = height.convert('px', DPI) # Insert or not ? if context['no_img_level']: context['img_counter'] += 1 ref = u'|img%d|' % context['img_counter'] result.append(ref) context['images'].append( (ref, filename, (width, height))) else: result.append(u'\n.. image:: %s\n' % filename) if width is not None: result.append(u' :width: %s\n' % width) if height is not None: result.append(u' :height: %s\n' % height) else: result.append(u'[Image %s]\n' % element.get_attribute('xlink:href')) elif tag == 'draw:text-box': subresult = [u' '] for element in element.get_children(): subresult.append(element.get_formatted_text(context)) subresult = u''.join(subresult) subresult = subresult.replace(u'\n', u'\n ') subresult.rstrip(' ') result.append(subresult) else: result.append(element.get_formatted_text(context)) result.append(u'\n') return u''.join(result)
class odf_manifest(odf_xmlpart): # # Public API # def get_paths(self): """Return the list of full paths in the manifest. Return: list of unicode """ expr = '//manifest:file-entry/attribute::manifest:full-path' return self.xpath(expr) get_path_list = obsolete('get_path_list', get_paths) def get_path_medias(self): """Return the list of (full_path, media_type) pairs in the manifest. Return: list of (unicode, str) tuples """ expr = '//manifest:file-entry' result = [] for file_entry in self.xpath(expr): result.append((file_entry.get_attribute('manifest:full-path'), file_entry.get_attribute('manifest:media-type'))) return result get_path_media_list = obsolete('get_path_media_list', get_path_medias) def get_media_type(self, full_path): """Get the media type of an existing path. Return: str """ expr = ('//manifest:file-entry[attribute::manifest:full-path="%s"]' '/attribute::manifest:media-type') result = self.xpath(expr % full_path) if not result: return None return result[0] def set_media_type(self, full_path, media_type): """Set the media type of an existing path. Arguments: full_path -- unicode media_type -- str """ expr = '//manifest:file-entry[attribute::manifest:full-path="%s"]' result = self.xpath(expr % full_path) if not result: raise KeyError, 'path "%s" not found' % full_path file_entry = result[0] file_entry.set_attribute('manifest:media-type', str(media_type)) def add_full_path(self, full_path, media_type=''): # Existing? existing = self.get_media_type(full_path) if existing is not None: self.set_media_type(full_path, media_type) root = self.get_root() file_entry = odf_create_file_entry(full_path, media_type) root.append(file_entry) def del_full_path(self, full_path): expr = '//manifest:file-entry[attribute::manifest:full-path="%s"]' result = self.xpath(expr % full_path) if not result: raise KeyError, 'path "%s" not found' % full_path file_entry = result[0] root = self.get_root() root.delete(file_entry)
class odf_content(odf_xmlpart): def get_body(self): return self.get_root().get_document_body() # The following two seem useless but they match styles API def _get_style_contexts(self, family): if family == 'font-face': return (self.get_element('//office:font-face-decls'), ) return (self.get_element('//office:font-face-decls'), self.get_element('//office:automatic-styles')) # # Public API # def get_styles(self, family=None): """Return the list of styles in the Content part, optionally limited to the given family. Arguments: family -- str Return: list of odf_style """ result = [] for context in self._get_style_contexts(family): if context is None: continue result.extend(context.get_styles(family=family)) return result get_style_list = obsolete('get_style_list', get_styles) def get_style(self, family, name_or_element=None, display_name=None): """Return the style uniquely identified by the name/family pair. If the argument is already a style object, it will return it. If the name is None, the default style is fetched. If the name is not the internal name but the name you gave in the desktop application, use display_name instead. Arguments: family -- 'paragraph', 'text', 'graphic', 'table', 'list', 'number' name_or_element -- unicode or odf_style display_name -- unicode Return: odf_style or None if not found """ for context in self._get_style_contexts(family): if context is None: continue style = context.get_style(family, name_or_element=name_or_element, display_name=display_name) if style is not None: return style return None
class odf_styles(odf_xmlpart): def _get_style_contexts(self, family, automatic=False): if automatic is True: return (self.get_element('//office:automatic-styles'), ) elif family is None: # All possibilities return (self.get_element('//office:automatic-styles'), self.get_element('//office:styles'), self.get_element('//office:master-styles'), self.get_element('//office:font-face-decls')) queries = context_mapping.get(family) if queries is None: raise ValueError, "unknown family: " + family return [self.get_element(query) for query in queries] def get_styles(self, family=None, automatic=False): """Return the list of styles in the Content part, optionally limited to the given family. Arguments: family -- str Return: list of odf_style """ result = [] for context in self._get_style_contexts(family, automatic=automatic): if context is None: continue result.extend(context.get_styles(family=family)) return result get_style_list = obsolete('get_style_list', get_styles) def get_style(self, family, name_or_element=None, display_name=None): """Return the style uniquely identified by the name/family pair. If the argument is already a style object, it will return it. If the name is None, the default style is fetched. If the name is not the internal name but the name you gave in the desktop application, use display_name instead. Arguments: name_or_element -- unicode, odf_style or None family -- 'paragraph', 'text', 'graphic', 'table', 'list', 'number', 'page-layout', 'master-page' display_name -- unicode Return: odf_style or None if not found """ for context in self._get_style_contexts(family): if context is None: continue style = context.get_style(family, name_or_element=name_or_element, display_name=display_name) if style is not None: return style return None def get_master_pages(self): return _get_elements(self, 'descendant::style:master-page') def get_master_page(self, position=0): return _get_element(self, 'descendant::style:master-page', position)
def odf_new_document(path_or_file): """Return an "odf_document" instance using the given template or the template found at the given path. Examples:: >>> document = odf_new_document(template) >>> path = 'models/invoice.ott' >>> document = odf_new_document(path) if "path" is one of 'text', 'spreadsheet', 'presentation', 'drawing' or 'graphics', then the lpOD default template is used. Examples:: >>> document = odf_new_document('text') >>> document = odf_new_document('spreadsheet') """ container = odf_new_container(path_or_file) return odf_document(container) odf_new_document_from_template = obsolete('odf_new_document_from_template', odf_new_document) odf_new_document_from_type = obsolete('odf_new_document_from_type', odf_new_document)
class odf_list(odf_element): """Specialised element for lists. """ def get_style(self): return self.get_attribute('text:style-name') def set_style(self, name): return self.set_style_attribute('text:style-name', name) def get_items(self, content=None): """Return all the list items that match the criteria. Arguments: style -- unicode content -- unicode regex Return: list of odf_paragraph """ return _get_elements(self, 'text:list-item', content=content) get_item_list = obsolete('get_item_list', get_items) def get_item(self, position=0, content=None): """Return the list item that matches the criteria. In nested lists, return the list item that really contains that content. Arguments: position -- int content -- unicode regex Return: odf_element or None if not found """ # Custom implementation because of nested lists if content: # Don't search recursively but on the very own paragraph(s) of # each list item for paragraph in self.get_elements('descendant::text:p'): if paragraph.match(content): return paragraph.get_element('parent::text:list-item') return None return _get_element(self, 'text:list-item', position) def set_header(self, text_or_element): if not isiterable(text_or_element): text_or_element = [text_or_element] # Remove existing header for element in self.get_elements('text:p'): self.delete(element) for paragraph in reversed(text_or_element): if type(paragraph) is unicode: paragraph = odf_create_paragraph(paragraph) self.insert(paragraph, FIRST_CHILD) def insert_item(self, item, position=None, before=None, after=None): # Check if the item is already a list-item tag_name = item.get_tag() if isinstance(item, odf_element) else None if tag_name != 'text:list-item': item = odf_create_list_item(item) if before is not None: before.insert(item, xmlposition=PREV_SIBLING) elif after is not None: after.insert(item, xmlposition=NEXT_SIBLING) elif position is not None: self.insert(item, position=position) else: raise ValueError, "position must be defined" def append_item(self, item): # Check if the item is already a list-item tag_name = item.get_tag() if isinstance(item, odf_element) else None if tag_name != 'text:list-item': item = odf_create_list_item(item) self.append(item) def get_formatted_text(self, context): rst_mode = context["rst_mode"] result = [] if rst_mode: result.append('\n') for list_item in self.get_elements('text:list-item'): textbuf = [] for child in list_item.get_children(): text = child.get_formatted_text(context) tag = child.get_tag() if tag == 'text:h': # A title in a list is a bug return text elif tag == 'text:list': if not text.lstrip().startswith(u'-'): # If the list didn't indent, don't either # (inner title) return text textbuf.append(text) textbuf = u''.join(textbuf) textbuf = textbuf.strip('\n') # Indent the text textbuf = u'- %s\n' % textbuf.replace(u'\n', u'\n ') result.append(textbuf) if rst_mode: result.append('\n') return u''.join(result)
class odf_xmlpart(object): """Representation of an XML part. Abstraction of the XML library behind. """ def __init__(self, part_name, container): self.part_name = part_name self.container = container # Internal state self.__tree = None self.__root = None def __get_tree(self): if self.__tree is None: container = self.container part = container.get_part(self.part_name) self.__tree = parse(StringIO(part)) return self.__tree # # Public API # def get_root(self): if self.__root is None: tree = self.__get_tree() self.__root = _make_odf_element(tree.getroot()) return self.__root def get_elements(self, xpath_query): root = self.get_root() return root.xpath(xpath_query) get_element_list = obsolete('get_element_list', get_elements) def get_element(self, xpath_query): result = self.get_elements(xpath_query) if not result: return None return result[0] def delete_element(self, child): child.delete() def xpath(self, xpath_query): """Apply XPath query to the XML part. Return list of odf_element or odf_text instances translated from the nodes found. """ root = self.get_root() return root.xpath(xpath_query) def clone(self): clone = object.__new__(self.__class__) for name in self.__dict__: if name == 'container': setattr(clone, name, self.container.clone()) elif name in ('_odf_xmlpart__tree', ): setattr(clone, name, None) else: value = getattr(self, name) value = deepcopy(value) setattr(clone, name, value) return clone def serialize(self, pretty=False): tree = self.__get_tree() # Lxml declaration is too exotic to me data = ['<?xml version="1.0" encoding="UTF-8"?>'] tree = tostring(tree, encoding='UTF-8', pretty_print=pretty) # Lxml with pretty_print is adding a empty line if pretty: tree = tree.strip() data.append(tree) return '\n'.join(data)
class odf_document(object): """Abstraction of the ODF document. """ def __init__(self, container): if not isinstance(container, odf_container): raise TypeError, "container is not an ODF container" self.container = container # Cache of XML parts self.__xmlparts = {} # Cache of the body self.__body = None # # Public API # def get_parts(self): """Return available part names with path inside the archive, e.g. ['content.xml', ..., 'Pictures/100000000000032000000258912EB1C3.jpg'] """ return self.container.get_parts() def get_part(self, path): """Return the bytes of the given part. The path is relative to the archive, e.g. "Pictures/100000000000032000000258912EB1C3.jpg". "content", "meta", "settings", "styles" and "manifest" are shortcuts to the real path, e.g. content.xml, and return a dedicated object with its own API. """ # "./ObjectReplacements/Object 1" path = path.lstrip('./') path = _get_part_path(path) cls = _get_part_class(path) container = self.container # Raw bytes if cls is None: return container.get_part(path) # XML part xmlparts = self.__xmlparts part = xmlparts.get(path) if part is None: xmlparts[path] = part = cls(path, container) return part get_content = obsolete('get_content', get_part, ODF_CONTENT) get_meta = obsolete('get_meta', get_part, ODF_META) get_styles = obsolete('get_styles', get_part, ODF_STYLES) get_manifest = obsolete('get_manifest', get_part, ODF_MANIFEST) def set_part(self, path, data): """Set the bytes of the given part. The path is relative to the archive, e.g. "Pictures/100000000000032000000258912EB1C3.jpg". """ # "./ObjectReplacements/Object 1" path = path.lstrip('./') path = _get_part_path(path) cls = _get_part_class(path) # XML part overwritten if cls is not None: del self.__xmlparts[path] return self.container.set_part(path, data) def del_part(self, path): path = _get_part_path(path) cls = _get_part_class(path) if path == ODF_MANIFEST or cls is not None: raise ValueError, 'part "%s" is mandatory' % path return self.container.del_part(path) def get_mimetype(self): return self.get_part('mimetype') def get_type(self): """ Get the ODF type (also called class) of this document. Return: 'chart', 'database', 'formula', 'graphics', 'graphics-template', 'image', 'presentation', 'presentation-template', 'spreadsheet', 'spreadsheet-template', 'text', 'text-master', 'text-template' or 'text-web' """ # The mimetype must be with the form: # application/vnd.oasis.opendocument.text mimetype = self.get_mimetype() # Isolate and return the last part return mimetype.rsplit('.', 1)[-1] def get_body(self): """Return the body element of the content part, where actual content is inserted. """ if self.__body is None: content = self.get_part(ODF_CONTENT) self.__body = content.get_body() return self.__body def get_formatted_text(self, rst_mode=False): # For the moment, only "type='text'" type = self.get_type() if type not in ('text', 'text-template', 'presentation', 'presentation-template'): raise NotImplementedError, ('Type of document "%s" not ' 'supported yet' % type) # Initialize an empty context context = { 'document': self, 'footnotes': [], 'endnotes': [], 'annotations': [], 'rst_mode': rst_mode, 'img_counter': 0, 'images': [], 'no_img_level': 0 } body = self.get_body() # Get the text result = [] for element in body.get_children(): if element.get_tag() == 'table:table': result.append(element.get_formatted_text(context)) else: result.append(element.get_formatted_text(context)) # Insert the notes footnotes = context['footnotes'] # Separate text from notes if footnotes: if rst_mode: result.append(u'\n') else: result.append(u'----\n') for citation, body in footnotes: if rst_mode: result.append(u'.. [#] %s\n' % body) else: result.append(u'[%s] %s\n' % (citation, body)) # Append a \n after the notes result.append(u'\n') # Reset for the next paragraph context['footnotes'] = [] # Insert the annotations annotations = context['annotations'] # With a separation if annotations: if rst_mode: result.append(u'\n') else: result.append(u'----\n') for annotation in annotations: if rst_mode: result.append('.. [#] %s\n' % annotation) else: result.append('[*] %s\n' % annotation) context['annotations'] = [] # Insert the images ref, only in rst mode images = context['images'] if images: result.append(u'\n') for ref, filename, (width, height) in images: result.append(u'.. %s image:: %s\n' % (ref, filename)) if width is not None: result.append(u' :width: %s\n' % width) if height is not None: result.append(u' :height: %s\n' % height) result.append(u'\n') context['images'] = [] # Append the end notes endnotes = context['endnotes'] if endnotes: if rst_mode: result.append(u'\n\n') else: result.append(u'\n========\n') for citation, body in endnotes: if rst_mode: result.append(u'.. [*] %s\n' % body) else: result.append(u'(%s) %s\n' % (citation, body)) return u''.join(result) def get_formated_meta(self): result = [] meta = self.get_part(ODF_META) # Simple values def print_info(name, value): if value: result.append("%s: %s" % (name, value)) print_info("Title", meta.get_title()) print_info("Subject", meta.get_subject()) print_info("Language", meta.get_language()) print_info("Modification date", meta.get_modification_date()) print_info("Creation date", meta.get_creation_date()) print_info("Initial creator", meta.get_initial_creator()) print_info("Keyword", meta.get_keywords()) print_info("Editing duration", meta.get_editing_duration()) print_info("Editing cycles", meta.get_editing_cycles()) print_info("Generator", meta.get_generator()) # Statistic result.append("Statistic:") statistic = meta.get_statistic() for name, value in statistic.iteritems(): result.append(" - %s: %s" % (name[5:].replace('-', ' ').capitalize(), value)) # User defined metadata result.append("User defined metadata:") user_metadata = meta.get_user_defined_metadata() for name, value in user_metadata.iteritems(): result.append(" - %s: %s" % (name, value)) # And the description print_info("Description", meta.get_description()) return u"\n".join(result) + '\n' def add_file(self, path_or_file): """Insert a file from a path or a fike-like object in the container. Return the full path to reference it in the content. Arguments: path_or_file -- str or file-like Return: str """ name = None close_after = False # Folder for added files (FIXME hard-coded and copied) manifest = self.get_part(ODF_MANIFEST) medias = manifest.get_paths() if type(path_or_file) is unicode or type(path_or_file) is str: path_or_file = path_or_file.encode('utf_8') file = open(path_or_file, 'rb') name = path_or_file close_after = True else: file = path_or_file # XXX Bug à corriger #name = getattr(_file, 'name') name = 'image' name = name.count('./') and name.split('./')[-1] or name # Generate a safe portable name uuid = str(uuid4()) if name is None: name = uuid media_type = '' else: basename, extension = splitext(name) name = basename + extension.lower() media_type, encoding = guess_type(name) # Check this name is already used in the document fullpath = 'Pictures/%s' % name if fullpath in medias: _, extension = splitext(name) basename = '%s_%s' % (basename, uuid) name = basename + extension.lower() media_type, encoding = guess_type(name) if manifest.get_media_type('Pictures/') is None: manifest.add_full_path('Pictures/') full_path = 'Pictures/%s' % (name) self.container.set_part(full_path, file.read()) # Update manifest manifest.add_full_path(full_path, media_type) # Close file if close_after: file.close() return full_path def clone(self): """Return an exact copy of the document. Return: odf_document """ clone = object.__new__(self.__class__) for name in self.__dict__: if name == 'container': setattr(clone, name, self.container.clone()) elif name == '_odf_document__xmlparts': xmlparts = {} for key, value in self.__xmlparts.iteritems(): xmlparts[key] = value.clone() setattr(clone, name, xmlparts) else: value = getattr(self, name) value = deepcopy(value) setattr(clone, name, value) return clone def save(self, target=None, packaging=None, pretty=False): """Save the document, at the same place it was opened or at the given target path. Target can also be a file-like object. It can be saved as a Zip file or as a flat XML file. XML parts can be pretty printed. Arguments: target -- str or file-like object packaging -- 'zip' or 'flat' pretty -- bool """ # Some advertising meta = self.get_part(ODF_META) if not meta._generator_modified: meta.set_generator(u"lpOD Python %s" % __version__) # Synchronize data with container container = self.container for path, part in self.__xmlparts.iteritems(): if part is not None: container.set_part(path, part.serialize(pretty)) # Save the container container.save(target, packaging) # # Styles over several parts # # TODO rename to get_styles in next version def get_style_list(self, family=None, automatic=False): content = self.get_part(ODF_CONTENT) styles = self.get_part(ODF_STYLES) return (content.get_styles(family=family) + styles.get_styles(family=family, automatic=automatic)) def get_style(self, family, name_or_element=None, display_name=None): """Return the style uniquely identified by the name/family pair. If the argument is already a style object, it will return it. If the name is None, the default style is fetched. If the name is not the internal name but the name you gave in a desktop application, use display_name instead. Arguments: family -- 'paragraph', 'text', 'graphic', 'table', 'list', 'number', 'page-layout', 'master-page' name -- unicode or odf_element or None display_name -- unicode Return: odf_style or None if not found. """ # 1. content.xml content = self.get_part(ODF_CONTENT) element = content.get_style(family, name_or_element=name_or_element, display_name=display_name) if element is not None: return element # 2. styles.xml styles = self.get_part(ODF_STYLES) return styles.get_style(family, name_or_element=name_or_element, display_name=display_name) def insert_style(self, style, name=None, automatic=False, default=False): """Insert the given style object in the document, as required by the style family and type. The style is expected to be a common style with a name. In case it was created with no name, the given can be set on the fly. If automatic is True, the style will be inserted as an automatic style. If default is True, the style will be inserted as a default style and would replace any existing default style of the same family. Any name or display name would be ignored. Automatic and default arguments are mutually exclusive. All styles can’t be used as default styles. Default styles are allowed for the following families: paragraph, text, section, table, table-column, table-row, table-cell, table-page, chart, drawing-page, graphic, presentation, control and ruby. Arguments: style -- odf_style name -- unicode automatic -- bool default -- bool """ # Get family and name family = style.get_family() if name is None: name = style.get_name() # Master page style if isinstance(style, odf_master_page): part = self.get_part(ODF_STYLES) container = part.get_element("office:master-styles") existing = part.get_style(family, name) # Font face declarations elif isinstance(style, odf_font_style): # XXX If inserted in styles.xml => It doesn't work, it's normal? part = self.get_part(ODF_CONTENT) container = part.get_element("office:font-face-decls") existing = part.get_style(family, name) # Common style elif isinstance(style, odf_style): # Common style if name and automatic is False and default is False: part = self.get_part(ODF_STYLES) container = part.get_element("office:styles") existing = part.get_style(family, name) # Automatic style elif automatic is True and default is False: part = self.get_part(ODF_CONTENT) container = part.get_element("office:automatic-styles") # A name ? if name is None: # Make a beautiful name # TODO: Use prefixes of Ooo: Mpm1, ... prefix = 'lpod_auto_' styles = self.get_style_list(family=family, automatic=True) names = [s.get_name() for s in styles] numbers = [ int(name[len(prefix):]) for name in names if name and name.startswith(prefix) ] if numbers: number = max(numbers) + 1 else: number = 1 name = prefix + str(number) # And set it style.set_name(name) existing = None else: existing = part.get_style(family, name) # Default style elif automatic is False and default is True: part = self.get_part(ODF_STYLES) container = part.get_element("office:styles") # Force default style style.set_tag("style:default-style") if name is not None: style.del_attribute("style:name") existing = part.get_style(family) # Error else: raise AttributeError, "invalid combination of arguments" # Invalid style else: raise ValueError, "invalid style" # Insert it! if existing is not None: container.delete(existing) container.append(style) def get_styled_elements(self, name=True): """Brute-force to find paragraphs, tables, etc. using the given style name (or all by default). Arguments: name -- unicode Return: list """ content = self.get_part(ODF_CONTENT) # Header, footer, etc. have styles too styles = self.get_part(ODF_STYLES) return (content.get_root().get_styled_elements(name) + styles.get_root().get_styled_elements(name)) def show_styles(self, automatic=True, common=True, properties=False): infos = [] for style in self.get_style_list(): name = style.get_name() is_auto = ( style.get_parent().get_tag() == 'office:automatic-styles') if (is_auto and automatic is False or not is_auto and common is False): continue is_used = bool(self.get_styled_elements(name)) infos.append({ 'type': u"auto " if is_auto else u"common", 'used': u"y" if is_used else u"n", 'family': style.get_family() or u"", 'parent': style.get_parent_style() or u"", 'name': name or u"", 'display_name': style.get_display_name(), 'properties': style.get_properties() if properties else None }) if not infos: return u"" # Sort by family and name infos.sort(key=itemgetter('family', 'name')) # Show common and used first infos.sort(key=itemgetter('type', 'used'), reverse=True) max_family = unicode(max([len(x['family']) for x in infos])) max_parent = unicode(max([len(x['parent']) for x in infos])) format = (u"%(type)s used:%(used)s family:%(family)-0" + max_family + u"s parent:%(parent)-0" + max_parent + u"s name:%(name)s") output = [] for info in infos: line = format % info if info['display_name']: line += u' display_name:' + info['display_name'] output.append(line) if info['properties']: for name, value in info['properties'].iteritems(): output.append(" - %s: %s" % (name, value)) output.append(u"") return u"\n".join(output) def delete_styles(self): """Remove all style information from content and all styles. Return: number of deleted styles """ # First remove references to styles for element in self.get_styled_elements(): for attribute in ('text:style-name', 'draw:style-name', 'draw:text-style-name', 'table:style-name', 'style:page-layout-name'): try: element.del_attribute(attribute) except KeyError: continue # Then remove supposedly orphaned styles i = 0 for style in self.get_style_list(): if style.get_name() is None: # Don't delete default styles continue #elif type(style) is odf_master_page: # # Don't suppress header and footer, just styling was removed # continue style.delete() i += 1 return i def merge_styles_from(self, document): """Copy all the styles of a document into ourself. Styles with the same type and name will be replaced, so only unique styles will be preserved. """ styles = self.get_part(ODF_STYLES) content = self.get_part(ODF_CONTENT) manifest = self.get_part(ODF_MANIFEST) document_manifest = document.get_part(ODF_MANIFEST) for style in document.get_style_list(): tagname = style.get_tag() family = style.get_family() stylename = style.get_name() container = style.get_parent() container_name = container.get_tag() partname = container.get_parent().get_tag() # The destination part if partname == "office:document-styles": part = styles elif partname == "office:document-content": part = content else: raise NotImplementedError, partname # Implemented containers if container_name not in ('office:styles', 'office:automatic-styles', 'office:master-styles', 'office:font-face-decls'): raise NotImplementedError, container_name dest = part.get_element('//%s' % container_name) # Implemented style types if tagname not in registered_styles: raise NotImplementedError, tagname duplicate = part.get_style(family, stylename) if duplicate is not None: duplicate.delete() dest.append(style) # Copy images from the header/footer if tagname == 'style:master-page': query = 'descendant::draw:image' for image in style.get_elements(query): url = image.get_url() part = document.get_part(url) # Manually add the part to keep the name self.set_part(url, part) media_type = document_manifest.get_media_type(url) manifest.add_full_path(url, media_type) # Copy images from the fill-image elif tagname == 'draw:fill-image': url = style.get_url() part = document.get_part(url) self.set_part(url, part) media_type = document_manifest.get_media_type(url) manifest.add_full_path(url, media_type)
class odf_style(odf_element): """Specialised element for styles, yet generic to all style types. """ def get_name(self): return self.get_attribute('style:name') get_style_name = obsolete('get_style_name', get_name) def set_name(self, name): self.set_attribute('style:name', name) def get_display_name(self): return self.get_attribute('style:display-name') def set_display_name(self, name): return self.set_style_attribute('style:display-name', name) def get_family(self): family = self.get_attribute('style:family') # Where the family is known from the tag, it must be defined if family is None: raise ValueError, 'family undefined in %s "%s"' % (self, self.get_name()) return family def set_family(self, family): return self.set_attribute('style:family', family) def get_parent_style(self): """Will only return a name, not an object, because we don't have access to the XML part from here. See odf_styles.get_parent_style """ return self.get_attribute('style:parent-style-name') get_parent_style_name = obsolete('get_parent_style_name', get_parent_style) def set_parent_style(self, name): self.set_style_attribute('style:parent-style-name', name) def get_properties(self, area=None): """Get the mapping of all properties of this style. By default the properties of the same family, e.g. a paragraph style and its paragraph properties. Specify the area to get the text properties of a paragraph style for example. Arguments: area -- str Return: dict """ if area is None: area = self.get_family() element = self.get_element('style:%s-properties' % area) if element is None: return None properties = element.get_attributes() # Nested properties are nested dictionaries for child in element.get_children(): properties[child.get_tag()] = child.get_attributes() return properties def set_properties(self, properties={}, style=None, area=None, **kw): """Set the properties of the "area" type of this style. Properties are given either as a dict or as named arguments (or both). The area is identical to the style family by default. If the properties element is missing, it is created. Instead of properties, you can pass a style with properties of the same area. These will be copied. Arguments: properties -- dict style -- odf_style area -- 'paragraph', 'text'... """ if area is None: area = self.get_family() element = self.get_element('style:%s-properties' % area) if element is None: element = odf_create_element('style:%s-properties' % area) self.append(element) if properties or kw: properties = _expand_properties(_merge_dicts(properties, kw)) elif style is not None: properties = style.get_properties(area=area) if properties is None: return for key, value in properties.iteritems(): if value is None: element.del_attribute(key) else: element.set_attribute(key, value) set_style_properties = obsolete('set_style_properties', set_properties) def del_properties(self, properties=[], area=None, *args): """Delete the given properties, either by list argument or positional argument (or both). Remove only from the given area, identical to the style family by default. Arguments: properties -- list area -- str """ if area is None: area = self.get_family() element = self.get_element('style:%s-properties' % area) if element is None: raise ValueError, "properties element is inexistent" for key in _expand_properties(properties): element.del_attribute(key) def set_background(self, color=None, url=None, position='center', repeat=None, opacity=None, filter=None): """Set the background color of a text style, or the background color or image of a paragraph style or page layout. With no argument, remove any existing background. The position is one or two of 'center', 'left', 'right', 'top' or 'bottom'. The repeat is 'no-repeat', 'repeat' or 'stretch'. The opacity is a percentage integer (not a string with the '%s' sign) The filter is an application-specific filter name defined elsewhere. Though this method is defined on the base style class, it will raise an error if the style type is not compatible. Arguments: color -- '#rrggbb' url -- str position -- str repeat -- str opacity -- int filter -- str """ family = self.get_family() if family not in ('text', 'paragraph', 'page-layout', 'section', 'table', 'table-row', 'table-cell', 'graphic'): raise TypeError, 'no background support for this family' if url is not None and family == 'text': raise TypeError, 'no background image for text styles' properties = self.get_element('style:%s-properties' % family) if properties is None: bg_image = None else: bg_image = properties.get_element('style:background-image') # Erasing if color is None and url is None: if properties is None: return properties.del_attribute('fo:background-color') if bg_image is not None: properties.delete(bg_image) return # Add the properties if necessary if properties is None: properties = odf_create_element('style:%s-properties' % family) self.append(properties) # Add the color... if color: properties.set_attribute('fo:background-color', color) if bg_image is not None: properties.delete(bg_image) # ... or the background elif url: properties.set_attribute('fo:background-color', 'transparent') if bg_image is None: bg_image = odf_create_element('style:background-image') properties.append(bg_image) bg_image.set_url(url) if position: bg_image.set_position(position) if repeat: bg_image.set_repeat(repeat) if opacity: bg_image.set_opacity(opacity) if filter: bg_image.set_filter(filter) def get_master_page(self): return self.get_attributes('style:master-page-name') def set_master_page(self, name): return self.set_style_attribute('style:master-page-name', name)
class odf_toc(odf_element): def get_formatted_text(self, context): index_body = self.get_element('text:index-body') if index_body is None: return u'' if context["rst_mode"]: return u"\n.. contents::\n\n" result = [] for element in index_body.get_children(): if element.get_tag() == 'text:index-title': for element in element.get_children(): result.append(element.get_formatted_text(context)) result.append(u'\n') else: result.append(element.get_formatted_text(context)) result.append('\n') return u''.join(result) def get_name(self): return self.get_attribute('text:name') def set_name(self, name): self.set_attribute('text:name', name) def get_outline_level(self): source = self.get_element('text:table-of-content-source') if source is None: return None return source.get_outline_level() def set_outline_level(self, level): source = self.get_element('text:table-of-content-source') if source is None: source = odf_create_element('text:table-of-content-source') self.insert(source, FIRST_CHILD) source.set_outline_level(level) def get_protected(self): return self.get_attribute('text:protected') def set_protected(self, protected): self.set_attribute('text:protected', protected) def get_style(self): return self.get_attribute('text:style-name') def set_style(self, name): return self.set_style_attribute('text:style-name', name) def get_body(self): return self.get_element('text:index-body') def set_body(self, body=None): old_body = self.get_body() if old_body is not None: self.delete(old_body) if body is None: body = odf_create_index_body() self.append(body) return body def get_title(self): index_body = self.get_body() if index_body is None: return None index_title = index_body.get_element('text:index-title') if index_title is None: return None return index_title.get_text_content() def set_title(self, title, style=None, text_style=None): index_body = self.get_body() if index_body is None: index_body = self.set_body() index_title = index_body.get_element('text:index-title') if index_title is None: name = u"%s_Head" % self.get_name() index_title = odf_create_index_title(title, name=name, style=style, text_style=text_style) index_body.append(index_title) else: if style: index_title.set_text_style(style) paragraph = index_title.get_paragraph() if paragraph is None: paragraph = odf_create_paragraph() index_title.append(paragraph) if text_style: paragraph.set_text_style(text_style) paragraph.set_text(title) def fill(self, document=None, use_default_styles=True): """Fill the TOC with the titles found in the document. A TOC is not contextual so it will catch all titles before and after its insertion. If the TOC is not attached to a document, attach it beforehand or provide one as argument. For having a pretty TOC, let use_default_styles by default. Arguments: document -- odf_document use_default_styles -- bool """ # Find the body if document is not None: body = document.get_body() else: body = self.get_document_body() if body is None: raise ValueError, "the TOC must be related to a document somehow" # Save the title index_body = self.get_body() title = index_body.get_element('text:index-title') # Clean the old index-body index_body = self.set_body() # Restore the title index_body.insert(title, position=0) # Insert default TOC style if use_default_styles: automatic_styles = body.get_element('//office:automatic-styles') for level in range(1, 11): if automatic_styles.get_style('paragraph', TOC_ENTRY_STYLE_PATTERN % level) is None: level_style = odf_create_toc_level_style(level) automatic_styles.append(level_style) # Auto-fill the index outline_level = self.get_outline_level() or 10 level_indexes = {} for heading in body.get_headings(): level = heading.get_outline_level() if level > outline_level: continue number = [] # 1. l < level for l in range(1, level): index = level_indexes.setdefault(l, 1) number.append(unicode(index)) # 2. l == level index = level_indexes.setdefault(level, 0) + 1 level_indexes[level] = index number.append(unicode(index)) # 3. l > level for l in range(level + 1, 11): if level_indexes.has_key(l): del level_indexes[l] number = u'.'.join(number) + u'.' # Make the title with "1.2.3. Title" format title = u"%s %s" % (number, heading.get_text()) paragraph = odf_create_paragraph(title) if use_default_styles: paragraph.set_text_style(TOC_ENTRY_STYLE_PATTERN % level) index_body.append(paragraph) toc_fill = obsolete('toc_fill', fill)