Search code examples
python-3.xpython-pptx

How to get the correct left position of a shape within a group when using python-pptx?


I am parsing a simple PowerPoint with three shapes. One shape is visibly to the left of the two other. But not when comparing it using python-pptx. The right side of that left shape (shape.left+shape.width) has a higher value than one of the other shapes left side (shape.left). The python-pptx result seem to indicate the right-hand shape starts within the left-hand shapes border. This seem to be caused by the group shape the right-hand shape is within.

What is the proper code to compare correctly that the right-hand shapes left side in fact is to the right of the left-hand shape?

I have tried removing the group, and then comparisons show expected values. I have tried creating new group shapes with shapes within, and again, they show expected values. However, the linked PowerPoint file at www.mibnet.se/LeftBoxIssue.pptx is an example where the group shape affects the normal result. When running the code, I do not know how the shapes were created. I need a generic way to test this special case correctly.

from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE

strStartPowerPoint=r".\LeftBoxIssue.pptx"
prs=Presentation(strStartPowerPoint)
slide=prs.slides[0]
for shpShape in slide.shapes:
    if shpShape.shape_type == MSO_SHAPE_TYPE.GROUP:
        print(shpShape.shapes[0].text+
             " has   Left="+str(shpShape.shapes[0].left)+
             " and right="+
             str(shpShape.shapes[0].left+shpShape.shapes[0].width))
    else:
        print(shpShape.text+" has Left="+str(shpShape.left)+
             " and right="+str(shpShape.left+shpShape.width))

I expect the right hand shape to have its "left" value greater than the left-hand shapes "right" value. But instead, it prints a smaller value:

Left has Left=160326 and right=6254527
Right has Left=3291751 and right=3846370

Solution

  • A good place to start in understanding this is inspecting the group-shape XML:

    print(group_shape._element.xml)
    

    There you will find a child element that looks like this:

    <p:grpSpPr>
      <a:xfrm>
        <a:off x="3347864" y="2204864"/>
        <a:ext cx="3506688" cy="2930624"/>
        <a:chOff x="3347864" y="2204864"/>
        <a:chExt cx="3506688" cy="2930624"/>
      </a:xfrm>
    </p:grpSpPr>
    

    The <a:chOff> element represents the "child-offset" of shapes within the group. In this case, which is typical of shapes grouped in python-pptx, note that the a:chOff values are exactly the same as the a:off values, which represent the top-left corner of the group-shape.

    Using these two sets of values, you can calculate some interesting positions.

    1. Absolute position of child shapes. This is child a:off plus group a:off minus group a:chOff.

    2. Relative position of child shapes (to the group-shape origin). This is child a:off minus group a:chOff.

    You can get these extra child-offset values from the group with:

    chOff = group_shape._element.xpath("./p:grpSpPr/a:xfrm/a:chOff")[0]
    chOff_x = int(chOff["x"])
    chOff_y = int(chOff["y"])
    

    These values are in English Metric Units (EMU) which are described here along with how you might conveniently manipulate them:
    https://python-pptx.readthedocs.io/en/latest/user/autoshapes.html#understanding-english-metric-units

    python-pptx always uses a child offset equal to the group shape position (a:off) because that is convenient. Other packages may use other group-shape offsets that are more convenient to their purposes. For example, if you were to move a group, you could accomplish that by changing a:off in the group only, without having to visit and update each of the child shape positions.