Ejemplo n.º 1
0
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)
Ejemplo n.º 2
0
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)
Ejemplo n.º 3
0
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)
Ejemplo n.º 4
0
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)
Ejemplo n.º 5
0
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
Ejemplo n.º 6
0
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)
Ejemplo n.º 7
0


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)
Ejemplo n.º 8
0
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)
Ejemplo n.º 9
0
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)
Ejemplo n.º 10
0
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)
Ejemplo n.º 11
0

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)
Ejemplo n.º 12
0
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)
Ejemplo n.º 13
0
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)