Python-docx: feature: horz rule (paragraph border)

Created on 3 Nov 2014  路  13Comments  路  Source: python-openxml/python-docx

I'm wondering if there is a way to add a horizontal rule in a word document using python-docx?

If this is not possible is there a way you can add it or that I can add it myself.
Thanks.

text

Most helpful comment

Four years later, I find that the insert_element_before() is contained in docx/oxml/xmlchemy.py, so we can just code like this:

from docx.oxml.shared import OxmlElement
from docx.oxml.ns import qn

def insertHR(paragraph):
    p = paragraph._p  # p is the <w:p> XML element
    pPr = p.get_or_add_pPr()
    pBdr = OxmlElement('w:pBdr')
    pPr.insert_element_before(pBdr,
        'w:shd', 'w:tabs', 'w:suppressAutoHyphens', 'w:kinsoku', 'w:wordWrap',
        'w:overflowPunct', 'w:topLinePunct', 'w:autoSpaceDE', 'w:autoSpaceDN',
        'w:bidi', 'w:adjustRightInd', 'w:snapToGrid', 'w:spacing', 'w:ind',
        'w:contextualSpacing', 'w:mirrorIndents', 'w:suppressOverlap', 'w:jc',
        'w:textDirection', 'w:textAlignment', 'w:textboxTightWrap',
        'w:outlineLvl', 'w:divId', 'w:cnfStyle', 'w:rPr', 'w:sectPr',
        'w:pPrChange'
    )
    bottom = OxmlElement('w:bottom')
    bottom.set(qn('w:val'), 'single')
    bottom.set(qn('w:sz'), '6')
    bottom.set(qn('w:space'), '1')
    bottom.set(qn('w:color'), 'auto')
    pBdr.append(bottom)

All 13 comments

How is it done in Word?

Next step after that would be identifying the XML that corresponds to what you're after.

Then it should be pretty clear what the implementation would look like.

I normally do it in word by adding three '-' and pressing enter, How can I see the xml part of this operation and what do i need to call to implement this. Sorry if I am asking noob questions.

you can use opc-diag to quickly browse the XML that's produced:
http://opc-diag.readthedocs.org/en/latest/

The command would be something like:
opc diff-item before.docx after.docx document.xml

You would create an empty-ish document using Word, saving it as before.docx. Then add a horizontal rule and save that as after.docx. The opc-diag command will take care of unpacking the .docx file and doing the diff on the document.xml part, which is where this change would appear.

Ahh thank you for your response, I will try this later in the day.

Do I then need to code the xml in python-docx or could is there a method I could call with the raw xml.
Again thanks for your help

If you post the operative XML with a few lines of context above and below I can steer you in the right direction.

The general gist is that you would use existing internals to navigate as close in the XML as you can and then use the native lxml API methods to add elements from there.

UPDATE:
After looking some more i can see that this bit:

<w:pgSz w:w="11900" w:h="16840"/>
       <w:pgMar w:top="1417" w:right="1417" w:bottom="1417" w:left="1417" w:header="708" w:footer="708" w:gutter="0"/>
       <w:cols w:space="708"/>

is defined in the after document so the hr must be defined here.

ORIGINAL MESSAGE:
Alright I did a diff-item on a before and after document like you said and I'm get the following output. I cant really tell which line is the hr rule line. It seems that the hr line is established in a section or something?

--- before/word/document.xml

+++ after/word/document.xml

@@ -20,11 +20,29 @@

     mc:Ignorable="w14 wp14"
     >
   <w:body>
-    <w:p w:rsidR="008B1614" w:rsidRDefault="008B1614">
+    <w:p w:rsidR="008B1614" w:rsidRDefault="00DB792B">
+      <w:pPr>
+        <w:pBdr>
+          <w:bottom w:val="single" w:sz="6" w:space="1" w:color="auto"/>
+        </w:pBdr>
+      </w:pPr>
+      <w:r>
+        <w:t xml:space="preserve">This is before the </w:t>
+      </w:r>
+      <w:proofErr w:type="spellStart"/>
+      <w:r>
+        <w:t>hr</w:t>
+      </w:r>
+      <w:proofErr w:type="spellEnd"/>
+    </w:p>
+    <w:p w:rsidR="00DB792B" w:rsidRDefault="00DB792B">
+      <w:r>
+        <w:t>This is after the hr</w:t>
+      </w:r>
       <w:bookmarkStart w:id="0" w:name="_GoBack"/>
       <w:bookmarkEnd w:id="0"/>
     </w:p>
-    <w:sectPr w:rsidR="008B1614" w:rsidSect="008B1614">
+    <w:sectPr w:rsidR="00DB792B" w:rsidSect="008B1614">
       <w:pgSz w:w="11900" w:h="16840"/>
       <w:pgMar w:top="1417" w:right="1417" w:bottom="1417" w:left="1417" w:header="708" w:footer="708" w:gutter="0"/>
       <w:cols w:space="708"/>

This is the operative set of lines:

+      <w:pPr>
+        <w:pBdr>
+          <w:bottom w:val="single" w:sz="6" w:space="1" w:color="auto"/>
+        </w:pBdr>
+      </w:pPr>

It places a bottom border under the first paragraph.

So that's pretty straightforward then. There's no dedicated horizontal rule element, which means this one is a twofer; implement paragraph border and get horizontal rule free with purchase :)

I'll see if I can get back to this after work today and provide a snippet you can use to insert these temporarily until we get it into the build.

This should get it done:

from docx.oxml import OxmlElement
from docx.oxml.ns import qn

def first_child_found_in(parent, tagnames):
    """
    Return the first child of parent with tag in *tagnames*, or None if
    not found.
    """
    for tagname in tagnames:
        child = parent.find(qn(tagname))
        if child is not None:
            return child
    return None

def insert_element_before(parent, elm, successors):
    """
    Insert *elm* as child of *parent* before any existing child having
    tag name found in *successors*.
    """
    successor = first_child_found_in(parent, successors)
    if successor is not None:
        successor.addprevious(elm)
    else:
        parent.append(elm)
    return elm

p = paragraph._p  # p is the <w:p> XML element
pPr = p.get_or_add_pPr()
pBdr = OxmlElement('w:pBdr')
insert_element_before(pPr, pBdr, successors=(
    'w:shd', 'w:tabs', 'w:suppressAutoHyphens', 'w:kinsoku', 'w:wordWrap',
    'w:overflowPunct', 'w:topLinePunct', 'w:autoSpaceDE', 'w:autoSpaceDN',
    'w:bidi', 'w:adjustRightInd', 'w:snapToGrid', 'w:spacing', 'w:ind',
    'w:contextualSpacing', 'w:mirrorIndents', 'w:suppressOverlap', 'w:jc',
    'w:textDirection', 'w:textAlignment', 'w:textboxTightWrap',
    'w:outlineLvl', 'w:divId', 'w:cnfStyle', 'w:rPr', 'w:sectPr',
    'w:pPrChange'
))
bottom = OxmlElement('w:bottom')
bottom.set(qn('w:val'), 'single')
bottom.set(qn('w:sz'), '6')
bottom.set(qn('w:space'), '1')
bottom.set(qn('w:color'), 'auto')
pBdr.append(bottom)

Let me know if you need help working out what this is doing.

First: Thank you so much for helping me with the code and I understand the gist of it, although I don't really know what the methods do (in detail I mean).
One question though: In the method insert_element_before in the else clause it says self.append(elm), what does self references to?

Oops, that should be docx, not pptx. And the self should be parent. I've updated the code to reflect :)

Alright, this works. Thank you so much for your help ;)

Hi @Steefkroon, let's keep this issue open as a feature request, we'll get around to adding it as a built-in feature in the fullness of time :)

Four years later, I find that the insert_element_before() is contained in docx/oxml/xmlchemy.py, so we can just code like this:

from docx.oxml.shared import OxmlElement
from docx.oxml.ns import qn

def insertHR(paragraph):
    p = paragraph._p  # p is the <w:p> XML element
    pPr = p.get_or_add_pPr()
    pBdr = OxmlElement('w:pBdr')
    pPr.insert_element_before(pBdr,
        'w:shd', 'w:tabs', 'w:suppressAutoHyphens', 'w:kinsoku', 'w:wordWrap',
        'w:overflowPunct', 'w:topLinePunct', 'w:autoSpaceDE', 'w:autoSpaceDN',
        'w:bidi', 'w:adjustRightInd', 'w:snapToGrid', 'w:spacing', 'w:ind',
        'w:contextualSpacing', 'w:mirrorIndents', 'w:suppressOverlap', 'w:jc',
        'w:textDirection', 'w:textAlignment', 'w:textboxTightWrap',
        'w:outlineLvl', 'w:divId', 'w:cnfStyle', 'w:rPr', 'w:sectPr',
        'w:pPrChange'
    )
    bottom = OxmlElement('w:bottom')
    bottom.set(qn('w:val'), 'single')
    bottom.set(qn('w:sz'), '6')
    bottom.set(qn('w:space'), '1')
    bottom.set(qn('w:color'), 'auto')
    pBdr.append(bottom)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

JiyhonLiu picture JiyhonLiu  路  3Comments

floatc picture floatc  路  4Comments

evbo picture evbo  路  3Comments

avvRobertoAlma picture avvRobertoAlma  路  7Comments

Tores76 picture Tores76  路  6Comments