Skip to content

Commit 01ac5c0

Browse files
MHoroszowskiclaude
andcommitted
feature: add arrowhead style, width, and length properties to LineFormat
Add begin/end arrowhead properties to LineFormat for controlling arrow styles on lines and connectors. Introduces MSO_LINE_END_TYPE and MSO_LINE_END_SIZE enums and models CT_LineEndProperties for the a:headEnd and a:tailEnd XML elements. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 278b47b commit 01ac5c0

8 files changed

Lines changed: 468 additions & 3 deletions

File tree

features/dml-line.feature

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,30 @@ Feature: Get and change line properties
3333
| solid | None |
3434

3535

36+
Scenario Outline: LineFormat.end_arrowhead_style getter
37+
Given a LineFormat object as line having <arrowhead> end arrowhead
38+
Then line.end_arrowhead_style is <value>
39+
40+
Examples: End arrowhead styles
41+
| arrowhead | value |
42+
| no explicit | None |
43+
| triangle | MSO_LINE_END_TYPE.TRIANGLE |
44+
45+
Scenario: LineFormat.end_arrowhead_style setter
46+
Given a LineFormat object as line having no explicit end arrowhead
47+
When I assign MSO_LINE_END_TYPE.STEALTH to line.end_arrowhead_style
48+
Then line.end_arrowhead_style is MSO_LINE_END_TYPE.STEALTH
49+
50+
Scenario Outline: LineFormat.begin_arrowhead_style getter
51+
Given a LineFormat object as line having <arrowhead> begin arrowhead
52+
Then line.begin_arrowhead_style is <value>
53+
54+
Examples: Begin arrowhead styles
55+
| arrowhead | value |
56+
| no explicit | None |
57+
| stealth | MSO_LINE_END_TYPE.STEALTH |
58+
59+
3660
Scenario: LineFormat.fill
3761
Given a LineFormat object as line
3862
Then line.fill is a FillFormat object

features/steps/line.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from helpers import test_pptx
77

88
from pptx import Presentation
9-
from pptx.enum.dml import MSO_LINE
9+
from pptx.enum.dml import MSO_LINE, MSO_LINE_END_TYPE
1010
from pptx.util import Length, Pt
1111

1212
# given ===================================================
@@ -25,6 +25,20 @@ def given_a_LineFormat_object_as_line_having_dash_style(context, current):
2525
context.line = shape.line
2626

2727

28+
@given("a LineFormat object as line having {arrowhead} end arrowhead")
29+
def given_a_LineFormat_object_as_line_having_end_arrowhead(context, arrowhead):
30+
shape_idx = {"no explicit": 0, "triangle": 1}[arrowhead]
31+
shape = Presentation(test_pptx("dml-line-end")).slides[0].shapes[shape_idx]
32+
context.line = shape.line
33+
34+
35+
@given("a LineFormat object as line having {arrowhead} begin arrowhead")
36+
def given_a_LineFormat_object_as_line_having_begin_arrowhead(context, arrowhead):
37+
shape_idx = {"no explicit": 0, "stealth": 2}[arrowhead]
38+
shape = Presentation(test_pptx("dml-line-end")).slides[0].shapes[shape_idx]
39+
context.line = shape.line
40+
41+
2842
@given("a LineFormat object as line having {line_width} width")
2943
def given_a_LineFormat_object_as_line_having_width(context, line_width):
3044
shape_idx = {"no explicit": 0, "1 pt": 1}[line_width]
@@ -47,6 +61,16 @@ def when_I_assign_value_to_line_dash_style(context, value_key):
4761
context.line.dash_style = value
4862

4963

64+
@when("I assign {value_key} to line.end_arrowhead_style")
65+
def when_I_assign_value_to_line_end_arrowhead_style(context, value_key):
66+
value = {
67+
"None": None,
68+
"MSO_LINE_END_TYPE.TRIANGLE": MSO_LINE_END_TYPE.TRIANGLE,
69+
"MSO_LINE_END_TYPE.STEALTH": MSO_LINE_END_TYPE.STEALTH,
70+
}[value_key]
71+
context.line.end_arrowhead_style = value
72+
73+
5074
@when("I assign {line_width} to line.width")
5175
def when_I_assign_value_to_line_width(context, line_width):
5276
value = {"None": None, "1 pt": Pt(1), "2.34 pt": Pt(2.34)}[line_width]
@@ -81,6 +105,27 @@ def then_line_dash_style_is_value(context, dash_style):
81105
)
82106

83107

108+
@then("line.end_arrowhead_style is {value_key}")
109+
def then_line_end_arrowhead_style_is_value(context, value_key):
110+
expected_value = {
111+
"None": None,
112+
"MSO_LINE_END_TYPE.TRIANGLE": MSO_LINE_END_TYPE.TRIANGLE,
113+
"MSO_LINE_END_TYPE.STEALTH": MSO_LINE_END_TYPE.STEALTH,
114+
}[value_key]
115+
actual_value = context.line.end_arrowhead_style
116+
assert actual_value == expected_value, "expected %s, got %s" % (expected_value, actual_value)
117+
118+
119+
@then("line.begin_arrowhead_style is {value_key}")
120+
def then_line_begin_arrowhead_style_is_value(context, value_key):
121+
expected_value = {
122+
"None": None,
123+
"MSO_LINE_END_TYPE.STEALTH": MSO_LINE_END_TYPE.STEALTH,
124+
}[value_key]
125+
actual_value = context.line.begin_arrowhead_style
126+
assert actual_value == expected_value, "expected %s, got %s" % (expected_value, actual_value)
127+
128+
84129
@then("line.fill is a FillFormat object")
85130
def then_line_fill_is_a_FillFormat_object(context):
86131
class_name = context.line.fill.__class__.__name__
27.7 KB
Binary file not shown.

src/pptx/dml/line.py

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22

33
from __future__ import annotations
44

5+
from typing import TYPE_CHECKING
6+
57
from pptx.dml.fill import FillFormat
6-
from pptx.enum.dml import MSO_FILL
8+
from pptx.enum.dml import MSO_FILL, MSO_LINE_END_SIZE, MSO_LINE_END_TYPE
79
from pptx.util import Emu, lazyproperty
810

11+
if TYPE_CHECKING:
12+
from pptx.oxml.shapes.shared import CT_LineEndProperties
13+
914

1015
class LineFormat(object):
1116
"""Provides access to line properties such as color, style, and width.
@@ -18,6 +23,69 @@ def __init__(self, parent):
1823
super(LineFormat, self).__init__()
1924
self._parent = parent
2025

26+
@property
27+
def begin_arrowhead_length(self) -> MSO_LINE_END_SIZE | None:
28+
"""Size of the arrowhead at the beginning of the line.
29+
30+
Read/write. Returns a member of :ref:`MsoArrowheadSize` or |None| if no explicit value
31+
has been set. Assigning |None| removes any existing value.
32+
"""
33+
headEnd = self._headEnd
34+
if headEnd is None:
35+
return None
36+
return headEnd.len
37+
38+
@begin_arrowhead_length.setter
39+
def begin_arrowhead_length(self, value: MSO_LINE_END_SIZE | None) -> None:
40+
if value is None:
41+
headEnd = self._headEnd
42+
if headEnd is not None:
43+
del headEnd.attrib["len"]
44+
return
45+
self._get_or_add_headEnd().len = value
46+
47+
@property
48+
def begin_arrowhead_style(self) -> MSO_LINE_END_TYPE | None:
49+
"""Type of arrowhead at the beginning of the line.
50+
51+
Read/write. Returns a member of :ref:`MsoArrowheadStyle` or |None| if no explicit value
52+
has been set. Assigning |None| removes any existing value.
53+
"""
54+
headEnd = self._headEnd
55+
if headEnd is None:
56+
return None
57+
return headEnd.type
58+
59+
@begin_arrowhead_style.setter
60+
def begin_arrowhead_style(self, value: MSO_LINE_END_TYPE | None) -> None:
61+
if value is None:
62+
headEnd = self._headEnd
63+
if headEnd is not None:
64+
del headEnd.attrib["type"]
65+
return
66+
self._get_or_add_headEnd().type = value
67+
68+
@property
69+
def begin_arrowhead_width(self) -> MSO_LINE_END_SIZE | None:
70+
"""Width of the arrowhead at the beginning of the line.
71+
72+
Read/write. Returns a member of :ref:`MsoArrowheadSize` or |None| if no explicit value
73+
has been set. Assigning |None| removes any existing value.
74+
"""
75+
headEnd = self._headEnd
76+
if headEnd is None:
77+
return None
78+
return headEnd.w
79+
80+
@begin_arrowhead_width.setter
81+
def begin_arrowhead_width(self, value: MSO_LINE_END_SIZE | None) -> None:
82+
if value is None:
83+
headEnd = self._headEnd
84+
if headEnd is not None:
85+
del headEnd.attrib["w"]
86+
return
87+
self._get_or_add_headEnd().w = value
88+
2189
@lazyproperty
2290
def color(self):
2391
"""
@@ -32,6 +100,69 @@ def color(self):
32100
self.fill.solid()
33101
return self.fill.fore_color
34102

103+
@property
104+
def end_arrowhead_length(self) -> MSO_LINE_END_SIZE | None:
105+
"""Size of the arrowhead at the end of the line.
106+
107+
Read/write. Returns a member of :ref:`MsoArrowheadSize` or |None| if no explicit value
108+
has been set. Assigning |None| removes any existing value.
109+
"""
110+
tailEnd = self._tailEnd
111+
if tailEnd is None:
112+
return None
113+
return tailEnd.len
114+
115+
@end_arrowhead_length.setter
116+
def end_arrowhead_length(self, value: MSO_LINE_END_SIZE | None) -> None:
117+
if value is None:
118+
tailEnd = self._tailEnd
119+
if tailEnd is not None:
120+
del tailEnd.attrib["len"]
121+
return
122+
self._get_or_add_tailEnd().len = value
123+
124+
@property
125+
def end_arrowhead_style(self) -> MSO_LINE_END_TYPE | None:
126+
"""Type of arrowhead at the end of the line.
127+
128+
Read/write. Returns a member of :ref:`MsoArrowheadStyle` or |None| if no explicit value
129+
has been set. Assigning |None| removes any existing value.
130+
"""
131+
tailEnd = self._tailEnd
132+
if tailEnd is None:
133+
return None
134+
return tailEnd.type
135+
136+
@end_arrowhead_style.setter
137+
def end_arrowhead_style(self, value: MSO_LINE_END_TYPE | None) -> None:
138+
if value is None:
139+
tailEnd = self._tailEnd
140+
if tailEnd is not None:
141+
del tailEnd.attrib["type"]
142+
return
143+
self._get_or_add_tailEnd().type = value
144+
145+
@property
146+
def end_arrowhead_width(self) -> MSO_LINE_END_SIZE | None:
147+
"""Width of the arrowhead at the end of the line.
148+
149+
Read/write. Returns a member of :ref:`MsoArrowheadSize` or |None| if no explicit value
150+
has been set. Assigning |None| removes any existing value.
151+
"""
152+
tailEnd = self._tailEnd
153+
if tailEnd is None:
154+
return None
155+
return tailEnd.w
156+
157+
@end_arrowhead_width.setter
158+
def end_arrowhead_width(self, value: MSO_LINE_END_SIZE | None) -> None:
159+
if value is None:
160+
tailEnd = self._tailEnd
161+
if tailEnd is not None:
162+
del tailEnd.attrib["w"]
163+
return
164+
self._get_or_add_tailEnd().w = value
165+
35166
@property
36167
def dash_style(self):
37168
"""Return value indicating line style.
@@ -88,13 +219,37 @@ def width(self, emu):
88219
ln = self._get_or_add_ln()
89220
ln.w = emu
90221

222+
def _get_or_add_headEnd(self) -> CT_LineEndProperties:
223+
"""Return the `a:headEnd` element, creating it if not present."""
224+
return self._get_or_add_ln().get_or_add_headEnd()
225+
91226
def _get_or_add_ln(self):
92227
"""
93228
Return the ``<a:ln>`` element containing the line format properties
94229
in the XML.
95230
"""
96231
return self._parent.get_or_add_ln()
97232

233+
def _get_or_add_tailEnd(self) -> CT_LineEndProperties:
234+
"""Return the `a:tailEnd` element, creating it if not present."""
235+
return self._get_or_add_ln().get_or_add_tailEnd()
236+
237+
@property
238+
def _headEnd(self) -> CT_LineEndProperties | None:
239+
"""Return `a:headEnd` element or None if not present."""
240+
ln = self._ln
241+
if ln is None:
242+
return None
243+
return ln.headEnd
244+
98245
@property
99246
def _ln(self):
100247
return self._parent.ln
248+
249+
@property
250+
def _tailEnd(self) -> CT_LineEndProperties | None:
251+
"""Return `a:tailEnd` element or None if not present."""
252+
ln = self._ln
253+
if ln is None:
254+
return None
255+
return ln.tailEnd

src/pptx/enum/dml.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,63 @@ class MSO_LINE_DASH_STYLE(BaseXmlEnum):
139139
MSO_LINE = MSO_LINE_DASH_STYLE
140140

141141

142+
class MSO_LINE_END_TYPE(BaseXmlEnum):
143+
"""Specifies the style of arrowhead at the end of a line.
144+
145+
Example::
146+
147+
from pptx.enum.dml import MSO_LINE_END_TYPE
148+
149+
shape.line.end_arrowhead_style = MSO_LINE_END_TYPE.TRIANGLE
150+
151+
MS API name: `MsoArrowheadStyle`
152+
153+
https://learn.microsoft.com/en-us/office/vba/api/Office.MsoArrowheadStyle
154+
"""
155+
156+
NONE = (1, "none", "No arrowhead.")
157+
"""No arrowhead."""
158+
159+
TRIANGLE = (2, "triangle", "A triangular arrowhead.")
160+
"""A triangular arrowhead."""
161+
162+
STEALTH = (3, "stealth", "A stealth-shaped arrowhead.")
163+
"""A stealth-shaped arrowhead."""
164+
165+
DIAMOND = (4, "diamond", "A diamond-shaped arrowhead.")
166+
"""A diamond-shaped arrowhead."""
167+
168+
OVAL = (5, "oval", "An oval arrowhead.")
169+
"""An oval arrowhead."""
170+
171+
OPEN = (6, "arrow", "An open arrowhead.")
172+
"""An open arrowhead."""
173+
174+
175+
class MSO_LINE_END_SIZE(BaseXmlEnum):
176+
"""Specifies the width or length of an arrowhead at the end of a line.
177+
178+
Example::
179+
180+
from pptx.enum.dml import MSO_LINE_END_SIZE
181+
182+
shape.line.end_arrowhead_width = MSO_LINE_END_SIZE.MEDIUM
183+
184+
MS API name: `MsoArrowheadWidth` / `MsoArrowheadLength`
185+
186+
https://learn.microsoft.com/en-us/office/vba/api/Office.MsoArrowheadWidth
187+
"""
188+
189+
SMALL = (1, "sm", "A small arrowhead.")
190+
"""A small arrowhead."""
191+
192+
MEDIUM = (2, "med", "A medium arrowhead.")
193+
"""A medium arrowhead."""
194+
195+
LARGE = (3, "lg", "A large arrowhead.")
196+
"""A large arrowhead."""
197+
198+
142199
class MSO_PATTERN_TYPE(BaseXmlEnum):
143200
"""Specifies the fill pattern used in a shape.
144201

src/pptx/oxml/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]):
370370

371371
from pptx.oxml.shapes.shared import ( # noqa: E402
372372
CT_ApplicationNonVisualDrawingProps,
373+
CT_LineEndProperties,
373374
CT_LineProperties,
374375
CT_NonVisualDrawingProps,
375376
CT_Placeholder,
@@ -382,8 +383,10 @@ def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]):
382383
register_element_cls("a:chExt", CT_PositiveSize2D)
383384
register_element_cls("a:chOff", CT_Point2D)
384385
register_element_cls("a:ext", CT_PositiveSize2D)
386+
register_element_cls("a:headEnd", CT_LineEndProperties)
385387
register_element_cls("a:ln", CT_LineProperties)
386388
register_element_cls("a:off", CT_Point2D)
389+
register_element_cls("a:tailEnd", CT_LineEndProperties)
387390
register_element_cls("a:xfrm", CT_Transform2D)
388391
register_element_cls("c:spPr", CT_ShapeProperties)
389392
register_element_cls("p:cNvPr", CT_NonVisualDrawingProps)

0 commit comments

Comments
 (0)