PPTX connectors: emit native bentConnector2/3/5 instead of disjoint lines

Spec authors hand the renderer a polyline like
``points: [[210,280],[400,280],[700,130]]``. The previous code emitted
one ``prst="line"`` shape per segment, so the elbow was two separate
straight lines that PowerPoint cannot recognise as a connected path.

The OCI template (kb/diagram/assets/OCI_Icons.pptx) uses native
PowerPoint bent connectors instead — 98 ``straightConnector1`` plus
22 ``bentConnector2/3/5`` plus 15 ``line`` across 48 slides. Match
that convention:

  • 2 points  → straightConnector1 (one segment, unchanged)
  • 3 points  → bentConnector2     (one elbow)
  • 4 points  → bentConnector3     (S-shape, two elbows)
  • 5+ points → bentConnector5     (four elbows)

The rasterizer (oci_pptx_render.py) now also understands bentConnectorN
so the local PNG preview matches what real PowerPoint draws — without
this it rendered every connector as one straight diagonal regardless of
the prst.

Persistent: applies to every absolute_layout connector for every spec,
not a per-case patch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-04-25 21:49:39 -03:00
parent e384e627d1
commit 3fb832e578
2 changed files with 125 additions and 6 deletions

View File

@@ -355,8 +355,60 @@ class PPTXSlideRenderer:
if not line:
return
xfrm = sp_pr.find("a:xfrm", namespaces=NS)
geom = sp_pr.find("a:prstGeom", namespaces=NS)
prst = geom.get("prst") if geom is not None else "line"
# bentConnectorN: PowerPoint draws an elbow path inside the
# bbox. Approximate visually so the rasterizer preview matches
# what real PowerPoint shows. (The previous code rendered every
# connector as a single diagonal which hid bent elbows during
# local visual review — Diego flagged 2026-04-25.)
if prst.startswith("bentConnector"):
self._render_bent_connector(xfrm, transform, line, prst)
return
self._render_line_like(xfrm, transform, line, arrow=True)
def _render_bent_connector(self, xfrm: etree._Element | None, transform: Transform,
line: dict, prst: str) -> None:
if xfrm is None:
return
off = xfrm.find("a:off", namespaces=NS)
ext = xfrm.find("a:ext", namespaces=NS)
if off is None or ext is None:
return
x = _safe_int(off.get("x"))
y = _safe_int(off.get("y"))
cx = _safe_int(ext.get("cx"))
cy = _safe_int(ext.get("cy"))
flip_h = xfrm.get("flipH") == "1"
flip_v = xfrm.get("flipV") == "1"
start_local = (cx if flip_h else 0, cy if flip_v else 0)
end_local = (0 if flip_h else cx, 0 if flip_v else cy)
sx_, sy_ = transform.map_point(x + start_local[0], y + start_local[1])
ex, ey = transform.map_point(x + end_local[0], y + end_local[1])
# Build the elbow waypoints. bentConnector2 = single bend at
# the source's projection on the target. bentConnector3 = two
# bends (S-shape). bentConnector5 = four bends.
if prst == "bentConnector2":
mid = (ex, sy_)
pts = [(sx_, sy_), mid, (ex, ey)]
elif prst == "bentConnector3":
half_x = (sx_ + ex) / 2
pts = [(sx_, sy_), (half_x, sy_), (half_x, ey), (ex, ey)]
else: # bentConnector5 or anything larger
half_x = (sx_ + ex) / 2
half_y = (sy_ + ey) / 2
pts = [(sx_, sy_), (half_x, sy_), (half_x, half_y),
(ex, half_y), (ex, ey)]
ipts = [(int(round(p[0])), int(round(p[1]))) for p in pts]
for i in range(len(ipts) - 1):
a, b = ipts[i], ipts[i + 1]
if line.get("dashed"):
self._draw_dashed_line(a, b, line["color"], line["width"])
else:
self.draw.line([a, b], fill=line["color"], width=line["width"])
if line.get("arrow"):
self._draw_arrowhead(ipts[-2], ipts[-1], line["color"], line["width"])
def _render_line_like(self, xfrm: etree._Element | None, transform: Transform, line: dict, arrow: bool) -> None:
if xfrm is None:
return