Skip to content

Commit 8610ece

Browse files
MHoroszowskiclaude
andcommitted
feature: add cap_style and join_style properties to LineFormat
Add line cap style (flat, round, square) and join style (round, bevel, miter) properties to LineFormat. Introduces MSO_LINE_CAP_STYLE and MSO_LINE_JOIN_STYLE enums and models the cap attribute and EG_LineJoinProperties choice group on CT_LineProperties. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1732961 commit 8610ece

File tree

4 files changed

+181
-3
lines changed

4 files changed

+181
-3
lines changed

src/pptx/dml/line.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55
from typing import TYPE_CHECKING
66

77
from pptx.dml.fill import FillFormat
8-
from pptx.enum.dml import MSO_FILL, MSO_LINE_END_SIZE, MSO_LINE_END_TYPE
8+
from pptx.enum.dml import (
9+
MSO_FILL,
10+
MSO_LINE_CAP_STYLE,
11+
MSO_LINE_END_SIZE,
12+
MSO_LINE_END_TYPE,
13+
MSO_LINE_JOIN_STYLE,
14+
)
915
from pptx.util import Emu, lazyproperty
1016

1117
if TYPE_CHECKING:
@@ -86,6 +92,23 @@ def begin_arrowhead_width(self, value: MSO_LINE_END_SIZE | None) -> None:
8692
return
8793
self._get_or_add_headEnd().w = value
8894

95+
@property
96+
def cap_style(self) -> MSO_LINE_CAP_STYLE | None:
97+
"""Cap style for this line.
98+
99+
Read/write. Returns a member of :ref:`MsoLineCapStyle` or |None| if no explicit value
100+
has been set. Assigning |None| removes any existing value.
101+
"""
102+
ln = self._ln
103+
if ln is None:
104+
return None
105+
return ln.cap
106+
107+
@cap_style.setter
108+
def cap_style(self, value: MSO_LINE_CAP_STYLE | None) -> None:
109+
ln = self._get_or_add_ln()
110+
ln.cap = value
111+
89112
@lazyproperty
90113
def color(self):
91114
"""
@@ -163,6 +186,36 @@ def end_arrowhead_width(self, value: MSO_LINE_END_SIZE | None) -> None:
163186
return
164187
self._get_or_add_tailEnd().w = value
165188

189+
@property
190+
def join_style(self) -> MSO_LINE_JOIN_STYLE | None:
191+
"""Join style for this line.
192+
193+
Read/write. Returns a member of :ref:`MsoLineJoinStyle` or |None| if no explicit value
194+
has been set. Assigning |None| removes any existing value.
195+
"""
196+
ln = self._ln
197+
if ln is None:
198+
return None
199+
join = ln.eg_lineJoinProperties
200+
if join is None:
201+
return None
202+
tag_name = join.tag.split("}")[-1]
203+
return {"round": MSO_LINE_JOIN_STYLE.ROUND, "bevel": MSO_LINE_JOIN_STYLE.BEVEL,
204+
"miter": MSO_LINE_JOIN_STYLE.MITER}[tag_name]
205+
206+
@join_style.setter
207+
def join_style(self, value: MSO_LINE_JOIN_STYLE | None) -> None:
208+
ln = self._get_or_add_ln()
209+
if value is None:
210+
ln._remove_eg_lineJoinProperties()
211+
return
212+
method_map = {
213+
MSO_LINE_JOIN_STYLE.ROUND: "get_or_change_to_round",
214+
MSO_LINE_JOIN_STYLE.BEVEL: "get_or_change_to_bevel",
215+
MSO_LINE_JOIN_STYLE.MITER: "get_or_change_to_miter",
216+
}
217+
getattr(ln, method_map[value])()
218+
166219
@property
167220
def dash_style(self):
168221
"""Return value indicating line style.

src/pptx/enum/dml.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,50 @@ class MSO_LINE_END_SIZE(BaseXmlEnum):
196196
"""A large arrowhead."""
197197

198198

199+
class MSO_LINE_CAP_STYLE(BaseXmlEnum):
200+
"""Specifies the cap style for a line.
201+
202+
Example::
203+
204+
from pptx.enum.dml import MSO_LINE_CAP_STYLE
205+
206+
shape.line.cap_style = MSO_LINE_CAP_STYLE.ROUND
207+
208+
MS API name: `MsoLineCap` (not in MS API, maps to DrawingML `ST_LineCap`)
209+
"""
210+
211+
FLAT = (1, "flat", "A flat cap at the end of a line.")
212+
"""A flat cap at the end of a line."""
213+
214+
ROUND = (2, "rnd", "A round cap at the end of a line.")
215+
"""A round cap at the end of a line."""
216+
217+
SQUARE = (3, "sq", "A square cap at the end of a line.")
218+
"""A square cap at the end of a line."""
219+
220+
221+
class MSO_LINE_JOIN_STYLE(BaseXmlEnum):
222+
"""Specifies the join style for a line.
223+
224+
Example::
225+
226+
from pptx.enum.dml import MSO_LINE_JOIN_STYLE
227+
228+
shape.line.join_style = MSO_LINE_JOIN_STYLE.MITER
229+
230+
MS API name: Not directly in MS API, maps to DrawingML `EG_LineJoinProperties`.
231+
"""
232+
233+
ROUND = (1, "round", "A round join between two lines.")
234+
"""A round join between two lines."""
235+
236+
BEVEL = (2, "bevel", "A bevel join between two lines.")
237+
"""A bevel join between two lines."""
238+
239+
MITER = (3, "miter", "A miter join between two lines.")
240+
"""A miter join between two lines."""
241+
242+
199243
class MSO_PATTERN_TYPE(BaseXmlEnum):
200244
"""Specifies the fill pattern used in a shape.
201245

src/pptx/oxml/shapes/shared.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import TYPE_CHECKING, Callable
66

77
from pptx.dml.fill import CT_GradientFillProperties
8-
from pptx.enum.dml import MSO_LINE_END_SIZE, MSO_LINE_END_TYPE
8+
from pptx.enum.dml import MSO_LINE_CAP_STYLE, MSO_LINE_END_SIZE, MSO_LINE_END_TYPE
99
from pptx.enum.shapes import PP_PLACEHOLDER
1010
from pptx.oxml.ns import qn
1111
from pptx.oxml.simpletypes import (
@@ -290,8 +290,19 @@ class CT_LineProperties(BaseOxmlElement):
290290
tailEnd: CT_LineEndProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
291291
"a:tailEnd", successors=_tag_seq[11:]
292292
)
293+
eg_lineJoinProperties = ZeroOrOneChoice(
294+
(
295+
Choice("a:round"),
296+
Choice("a:bevel"),
297+
Choice("a:miter"),
298+
),
299+
successors=_tag_seq[9:],
300+
)
293301
del _tag_seq
294302
w = OptionalAttribute("w", ST_LineWidth, default=Emu(0))
303+
cap: MSO_LINE_CAP_STYLE | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
304+
"cap", MSO_LINE_CAP_STYLE
305+
)
295306

296307
@property
297308
def eg_fillProperties(self):

tests/dml/test_line.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@
77
from pptx.dml.color import ColorFormat
88
from pptx.dml.fill import FillFormat
99
from pptx.dml.line import LineFormat
10-
from pptx.enum.dml import MSO_FILL, MSO_LINE, MSO_LINE_END_SIZE, MSO_LINE_END_TYPE
10+
from pptx.enum.dml import (
11+
MSO_FILL,
12+
MSO_LINE,
13+
MSO_LINE_CAP_STYLE,
14+
MSO_LINE_END_SIZE,
15+
MSO_LINE_END_TYPE,
16+
MSO_LINE_JOIN_STYLE,
17+
)
1118
from pptx.oxml.shapes.shared import CT_LineProperties
1219
from pptx.shapes.autoshape import Shape
1320

@@ -172,6 +179,69 @@ def it_can_change_its_end_arrowhead_length(
172179
LineFormat(spPr).end_arrowhead_length = value
173180
assert spPr.xml == xml(expected_cxml)
174181

182+
@pytest.mark.parametrize(
183+
("spPr_cxml", "expected_value"),
184+
[
185+
("p:spPr", None),
186+
("p:spPr/a:ln", None),
187+
("p:spPr/a:ln{cap=rnd}", MSO_LINE_CAP_STYLE.ROUND),
188+
("p:spPr/a:ln{cap=sq}", MSO_LINE_CAP_STYLE.SQUARE),
189+
("p:spPr/a:ln{cap=flat}", MSO_LINE_CAP_STYLE.FLAT),
190+
],
191+
)
192+
def it_knows_its_cap_style(
193+
self, spPr_cxml: str, expected_value: MSO_LINE_CAP_STYLE | None
194+
):
195+
line = LineFormat(element(spPr_cxml))
196+
assert line.cap_style == expected_value
197+
198+
@pytest.mark.parametrize(
199+
("spPr_cxml", "value", "expected_cxml"),
200+
[
201+
("p:spPr{a:b=c}", MSO_LINE_CAP_STYLE.ROUND, "p:spPr{a:b=c}/a:ln{cap=rnd}"),
202+
("p:spPr/a:ln{cap=rnd}", MSO_LINE_CAP_STYLE.SQUARE, "p:spPr/a:ln{cap=sq}"),
203+
("p:spPr/a:ln{cap=sq}", None, "p:spPr/a:ln"),
204+
],
205+
)
206+
def it_can_change_its_cap_style(
207+
self, spPr_cxml: str, value: MSO_LINE_CAP_STYLE | None, expected_cxml: str
208+
):
209+
spPr = element(spPr_cxml)
210+
LineFormat(spPr).cap_style = value
211+
assert spPr.xml == xml(expected_cxml)
212+
213+
@pytest.mark.parametrize(
214+
("spPr_cxml", "expected_value"),
215+
[
216+
("p:spPr", None),
217+
("p:spPr/a:ln", None),
218+
("p:spPr/a:ln/a:round", MSO_LINE_JOIN_STYLE.ROUND),
219+
("p:spPr/a:ln/a:bevel", MSO_LINE_JOIN_STYLE.BEVEL),
220+
("p:spPr/a:ln/a:miter", MSO_LINE_JOIN_STYLE.MITER),
221+
],
222+
)
223+
def it_knows_its_join_style(
224+
self, spPr_cxml: str, expected_value: MSO_LINE_JOIN_STYLE | None
225+
):
226+
line = LineFormat(element(spPr_cxml))
227+
assert line.join_style == expected_value
228+
229+
@pytest.mark.parametrize(
230+
("spPr_cxml", "value", "expected_cxml"),
231+
[
232+
("p:spPr{a:b=c}", MSO_LINE_JOIN_STYLE.ROUND, "p:spPr{a:b=c}/a:ln/a:round"),
233+
("p:spPr/a:ln", MSO_LINE_JOIN_STYLE.BEVEL, "p:spPr/a:ln/a:bevel"),
234+
("p:spPr/a:ln/a:round", MSO_LINE_JOIN_STYLE.MITER, "p:spPr/a:ln/a:miter"),
235+
("p:spPr/a:ln/a:miter", None, "p:spPr/a:ln"),
236+
],
237+
)
238+
def it_can_change_its_join_style(
239+
self, spPr_cxml: str, value: MSO_LINE_JOIN_STYLE | None, expected_cxml: str
240+
):
241+
spPr = element(spPr_cxml)
242+
LineFormat(spPr).join_style = value
243+
assert spPr.xml == xml(expected_cxml)
244+
175245
def it_knows_its_dash_style(self, dash_style_get_fixture):
176246
line, expected_value = dash_style_get_fixture
177247
assert line.dash_style == expected_value

0 commit comments

Comments
 (0)