How do I Add Custom XML to An Element Using Python-Docx?

Viewed 551

I've been trying to get text wrapping for my table which isn't implemented in python-docx.

enter image description here

So far so good, I've isolated that MS Word adds <w:tblpPr w:leftFromText="180" w:rightFromText="180" w:vertAnchor="text" w:tblpY="1"/> as a child to the table properties element so I just need to inject that into my table in python-docx.

enter image description here

I got real close. Because I was able to find the element class in the library to add a child to. However my problem lays in the w: namespace because it won't let me construct an Element tag with the ':' character in it. I tried two methods, both failed.

tblPrElement = Element('w:tblpPr ', {'w:leftFromText': '180', 'w:rightFromText': '180', 'w:vertAnchor': 'text', 'w:tblpY': '1' })

tblPrElement = parse_xml('<w:tblpPr w:leftFromText="180" w:rightFromText="180" w:vertAnchor="text" w:tblpY="1"/>')

If I try it omitting the w: ...

document = Document()
table = document.add_table(rows=1, cols=3)
tblPrElement = parse_xml('<tblpPr leftFromText="180" rightFromText="180" vertAnchor="text" tblpY="1"/>')
table._tblPr.append(tblPrElement)

...then the document builds but ms word can't open it because the xml then looks like this:

enter image description here

3 Answers

If you want to use lxml to append the XML using an Element you will have to provide python-docx's namespace map (docx.oxml.nsmap) when creating the Element.

The syntax for inserting a namespace in a tag name is {namespacevalue}tagname.

For example, <{http://schemas.openxmlformats.org/wordprocessingml/2006/main}tblpPr/>

Here is a method you can use to set the table to text wrapping using your XML.

def set_text_wrap_around_table(table):
    w_namespace = '{' + nsmap['w'] + '}'
    tblPrElement = Element('{w}tblpPr'.format(w=w_namespace),
                           {'{w}leftFromText'.format(w=w_namespace): '180',
                            '{w}rightFromText'.format(w=w_namespace): '180',
                            '{w}vertAnchor'.format(w=w_namespace): 'text',
                            '{w}tblpY'.format(w=w_namespace): '1'},
                           nsmap)
    table._tblPr.append(tblPrElement)

Consider lxml's QName() for a functional version to avoid string formatting:

from lxml import etree
...


def set_text_wrap_around_table(table): 
     tblPrElement = etree.Element(
         etree.QName(nsmap["w"], "tblpPr"),
         {
              etree.QName(nsmap["w"], "leftFromText"): '180', 
              etree.QName(nsmap["w"], "rightFromText"): '180', 
              etree.QName(nsmap["w"], "vertAnchor"): 'text', 
              etree.QName(nsmap["w"], "tblpY"): '1'
         },
         nsmap
     )

     table._tblPr.append(tblPrElement)

python-docx has a qn method for using with element creation.

def qn(tag):
    """
    Stands for "qualified name", a utility function to turn a namespace
    prefixed tag name into a Clark-notation qualified tag name for lxml. For
    example, ``qn('p:cSld')`` returns ``'{http://schemas.../main}cSld'``.
    """

It allows you to type XML with namespace prefixes.

from docx.oxml.ns import qn

def set_text_wrap_around_table(table):
    tbl_properties_element = Element(qn('w:tblpPr'),
                           {
                              qn('w:leftFromText'): '180',
                              qn('w:rightFromText'): '180',
                              qn('w:vertAnchor'): 'text',
                              qn('w:tblpY'): '1'
                           })
    table._tblPr.append(tbl_properties_element)

If the element you'd like to create already has a class defined in python-docx you can create it using docx.oxml.OxmlElement for example, you can create a paragraph run element class like so:

from docx.oxml import OxmlElement

run_element = OxmlElement('w:r')

This has the added benefits of having its possible children defined as attributes and more.

Related