pythonpython-docx

python-docx header set anchor "to page"


Problem: put a logo image at top left position of the page.

I've tried the following to accomplish the same but it doesn't work as expected:

from docx import Document
from docx.shared import Mm, Pt
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
doc = Document()

# A4 vertical
section = doc.sections[0]
section.page_height = Mm(297)
section.page_width = Mm(210)

# tried to remove margin to position logo
section.top_margin = Mm(0)
section.bottom_margin = Mm(0)
section.left_margin = Mm(0)
section.right_margin = Mm(0)

# placed logo but still got a left margin
header = section.header
p = header.paragraphs[0]
p.paragraph_format.space_before = 0
p.paragraph_format.space_after = 0
p.alignment = WD_PARAGRAPH_ALIGNMENT.LEFT
p.add_run().add_picture("logo.png", width=Mm(210))

# restored margins for next content but trigger a break of the page
section.top_margin = Mm(20)
section.bottom_margin = Mm(20)
section.left_margin = Mm(25)
section.right_margin = Mm(20)

Manually editing the generated docx file I've found out that with image properties anchor to page it would fix.

But this option seems missing in docx code.


Solution

  • As python-docx follows the approach to hold the Office Open XML in memory and write it while Document.save one can manipulate the XML directly. So if one knows what the XML must look like and what XML elements needed one can extend python-docx whthout the need to change the methods deep inside of the python-docx source code.

    The following complete example shows this by creating a function add_image_with_anchor which adds an inline picture to the text run. This is what python-docx does by default. Then it changes the XML of the complex type graphical object from inline to anchor.

    from docx import Document
    from docx.shared import Inches
    from docx.oxml import parse_xml
    
    def add_image_with_anchor(run, image, drawing_descr, image_width, image_height, behind_text, rel_from_h, align_h, rel_from_v, align_v):
        inline_shape = run.add_picture(image, width=image_width, height=image_height)
        ct_inline = inline_shape._inline
        ct_graphicalobjects = ct_inline.xpath("./a:graphic")
        if len(ct_graphicalobjects) > 0:
            ct_graphicalobject = ct_graphicalobjects[0]
            anchor_XML =  '<wp:anchor xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"'
            anchor_XML += ' simplePos="0" relativeHeight="0" behindDoc="' + ('1' if behind_text else '0') + '"'
            anchor_XML += ' locked="0" layoutInCell="1" allowOverlap="1">'
            anchor_XML += '<wp:simplePos x="0" y="0"/>'
            anchor_XML += '<wp:positionH relativeFrom="' + rel_from_h + '"><wp:align>' + align_h + '</wp:align></wp:positionH>'
            anchor_XML += '<wp:positionV relativeFrom="' + rel_from_v + '"><wp:align>' + align_v + '</wp:align></wp:positionV>'
            anchor_XML += '<wp:extent cx="' + str(image_width) + '" cy="' + str(image_height) + '"/>'
            anchor_XML += '<wp:effectExtent l="0" t="0" r="0" b="0"/><wp:wrapNone/>'
            anchor_XML += '<wp:docPr id="1" name="Drawing 0" descr="' + drawing_descr + '"/>'
            anchor_XML += '<wp:cNvGraphicFramePr/>'
            anchor_XML += '</wp:anchor>'
            ct_anchor = parse_xml(anchor_XML)
            ct_anchor.append(ct_graphicalobject)
            ct_drawing = ct_inline.getparent()
            ct_drawing.remove_all("wp:inline")
            ct_drawing.append(ct_anchor)
            
        
    document = Document()
        
    document.add_paragraph('Title Text', style='Title')
    document.add_paragraph('Heading 1', style='Heading 1')
    
    run = document.add_paragraph().add_run()
    add_image_with_anchor(run, './acer01.jpg', "acer picture", Inches(2), Inches(1.5), True, "page", "center", "page", "center")
    
    add_image_with_anchor(run, './stackoverflowLogo.jpg', "stackoverflow logo", Inches(3), Inches(1), True, "page", "left", "page", "top")
    
    document.save('output.docx')