Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connector between shape doesn't match how powerpoint would draw it manually #946

Open
jwhendy opened this issue Feb 15, 2024 · 6 comments
Open

Comments

@jwhendy
Copy link

jwhendy commented Feb 15, 2024

Caveat: I'm pretty new to python-pptx and could be easily messing up.

I approached connecting shapes naively, per the approach in this SO post answered by @scanny .

I have this image saved as box.png:
box

Here is my code:

import pptx
from pptx.enum.shapes import MSO_CONNECTOR_TYPE
from pptx.util import Cm

prs = pptx.Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[6]) # this yields a blank slide in my default template

s1 = slide.shapes.add_picture('./box.png', left=Cm(2), top=Cm(2), height=pptx.util.Inches(1))
s2 = slide.shapes.add_picture('./box.png', left=Cm(4), top=Cm(5), height=pptx.util.Inches(1))

line = slide.shapes.add_connector(MSO_CONNECTOR_TYPE.ELBOW, Cm(2), Cm(2), Cm(2), Cm(2))
line.begin_connect(s1, 3)
line.end_connect(s2, 0)

prs.save('test.pptx')

I'm getting the right connection points, but I don't understand why the connector comes in from the right vs. down?
image

If I go to re-connect the line manually, it does the expected thing, and I'm not sure how I could force the as-is behavior from within powerpoint even if I wanted to?
image

Is this a bug or user error on my part? I admit I just put in dummy coordinates; it was not clear to me why I need to initialize the coordinates of a connector when we'll just be accessing shape built-ins to accomplish this. I did play with them a bit, but saw no effect. Just in case, I implemented the method of also determining the connection points from that same SO post and tried this with the same result:

cx1 = s1.left + s1.width
cy1 = s1.top + int(s1.height/2)
cx2 = s2.left + int(s2.width/2)
cy2 = s2.top

line = slide.shapes.add_connector(MSO_CONNECTOR_TYPE.ELBOW, cx1, cy1, cx2, cy2)
line.begin_connect(s1, 3)
line.end_connect(s2, 0)

I do see that connectors are experimental, so if that's why this isn't perfect, could the docs be updated to say what, exactly, isn't working how you want? At the moment, it just seems like it only works with certain shapes, and I believe I'm using ones that ShouldWork. Thanks for any input!

@scanny
Copy link
Owner

scanny commented Feb 15, 2024

Hmm, dunno. I think you'd have to analyze the change to the XML that happened between the before and after XML for the connector.

Might be worth trying it for two rectangles where it comes out of a side and goes into a side of the other or out of the bottom of one and into the top of the other, just to see if that works. I think you're right the initial location of the connector endpoints is arbitrary if you're going to connect it.

I don't recall much about this feature, I expect it was sponsored and this was good enough behavior for the sponsor and better-than-nothing behavior for everyone else :)

I kind of suppose there is some "egress vector/angle/orientation" attribute that appears on either the connector or the shape and is not or cannot reliably be computed by python-pptx. It's possible it depends on the connector type, that would explain some things. Definitely analysis of the XML would be the next step if you wanted to understand it.

@jwhendy
Copy link
Author

jwhendy commented Feb 15, 2024

I've tried a bunch of entry ids (0 through 3) and the end always wants to go down, then bend right. Not sure.

Forgive the formatting, but here's the extracted connector info (I ditched the <style> part) to show the difference. In both cases I'm connecting from the top (0) to left side (1).

As exported from python-pptx

<p:cxnSp>
	<p:nvCxnSpPr>
	  <p:cNvPr id="4" name="Connector 3"/>
	  <p:cNvCxnSpPr>
	    <a:stCxn id="2" idx="0"/>
	    <a:endCxn id="3" idx="1"/>
	  </p:cNvCxnSpPr>
	  <p:nvPr/>
	</p:nvCxnSpPr>
	<p:spPr>
	  <a:xfrm flipH="1">
	    <a:off x="360000" y="360000"/>
	    <a:ext cx="370046" cy="1260000"/>
	  </a:xfrm>
	  <a:prstGeom prst="bentConnector3">
	    <a:avLst/>
	  </a:prstGeom>
	  <a:ln>
	    <a:solidFill>
	      <a:srgbClr val="000000"/>
	    </a:solidFill>
	  </a:ln>
	</p:spPr>
</p:cxnSp>

After tweaking one side of the connector and it 'fixes' itself

<p:cxnSp>
	<p:nvCxnSpPr>
	  <p:cNvPr id="4" name="Connector 3"/>
	  <p:cNvCxnSpPr>
	    <a:cxnSpLocks/>
	    <a:stCxn id="2" idx="0"/>
	    <a:endCxn id="3" idx="1"/>
	  </p:cNvCxnSpPr>
	  <p:nvPr/>
	</p:nvCxnSpPr>
	<p:spPr>
	  <a:xfrm rot="16200000" flipH="1" flipV="1">
	    <a:off x="-84976" y="804976"/>
	    <a:ext cx="1260000" cy="370047"/>
	  </a:xfrm>
	  <a:prstGeom prst="bentConnector4">
	    <a:avLst>
	      <a:gd name="adj1" fmla="val -18143"/>
	      <a:gd name="adj2" fmla="val 161776"/>
	    </a:avLst>
	  </a:prstGeom>
	  <a:ln>
	    <a:solidFill>
	      <a:srgbClr val="000000"/>
	    </a:solidFill>
	  </a:ln>
	</p:spPr>
      </p:cxnSp>

I'm pretty out of my depth here on how one could fix this, but at least this gives some data that might be helpful.

I expect it was sponsored and this was good enough behavior for the sponsor and better-than-nothing behavior for everyone else :)

Ha! Perfectly said, and understood :) Thanks for taking a look.

@scanny
Copy link
Owner

scanny commented Feb 16, 2024

Ah, okay, so definitely the <a:avLst> element (and its children) then. av stands for adjustment-value. Those are those little yellow boxes you can drag around to adjust a shape or in this case the position of the line relative to the elbows.

There's not much out there on those, just that the fmla attribute is the business-end and that those are a string: ST_GeomGuideFormula

<xsd:simpleType name="ST_GeomGuideFormula">

I think you'd be reduced to experimentation to sort out how those behave in this case. Good news is there is some interface for it in python-pptx, code here:

class Adjustment(object):

and docs here: https://python-pptx.readthedocs.io/en/latest/user/autoshapes.html#adjusting-an-autoshape

Haven't looked at that part of the docs for a long time, looks like when I wrote it I knew more about it than I now remember :)

@scanny
Copy link
Owner

scanny commented Feb 16, 2024

Actually, reading that part of the docs more closely, the example there seems like a close fit for what you're trying to do, worth a read I'd say :)

@Ozymansour
Copy link

@jwhendy did you figure this out? it seems that the adjustments property doesn't apply to connectors?

@jwhendy
Copy link
Author

jwhendy commented Oct 11, 2024

@Ozymansour : I think I gave up, sorry :( I did take a moment to revisit this, but am not getting @scanny 's suggestion to work. Referring to my repro code in the first post and the input on using adjustments, I assumed I should do this:

adjs = line.adjustments

I get an error, however:

AttributeError: 'Connector' object has no attribute 'adjustments'

At that point I'm stumped, as the avLst section referenced applies to the bent connector, which would seem to be the line object... so I'm not sure how to access it's adjustments and see which ones control removing that spurious bend right at the end.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants