Skip to content

Commit 26a75f9

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 278b47b commit 26a75f9

4 files changed

Lines changed: 168 additions & 2 deletions

File tree

src/pptx/dml/line.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from pptx.dml.fill import FillFormat
6-
from pptx.enum.dml import MSO_FILL
6+
from pptx.enum.dml import MSO_FILL, MSO_LINE_CAP_STYLE, MSO_LINE_JOIN_STYLE
77
from pptx.util import Emu, lazyproperty
88

99

@@ -18,6 +18,23 @@ def __init__(self, parent):
1818
super(LineFormat, self).__init__()
1919
self._parent = parent
2020

21+
@property
22+
def cap_style(self) -> MSO_LINE_CAP_STYLE | None:
23+
"""Cap style for this line.
24+
25+
Read/write. Returns a member of :ref:`MsoLineCapStyle` or |None| if no explicit value
26+
has been set. Assigning |None| removes any existing value.
27+
"""
28+
ln = self._ln
29+
if ln is None:
30+
return None
31+
return ln.cap
32+
33+
@cap_style.setter
34+
def cap_style(self, value: MSO_LINE_CAP_STYLE | None) -> None:
35+
ln = self._get_or_add_ln()
36+
ln.cap = value
37+
2138
@lazyproperty
2239
def color(self):
2340
"""
@@ -32,6 +49,36 @@ def color(self):
3249
self.fill.solid()
3350
return self.fill.fore_color
3451

52+
@property
53+
def join_style(self) -> MSO_LINE_JOIN_STYLE | None:
54+
"""Join style for this line.
55+
56+
Read/write. Returns a member of :ref:`MsoLineJoinStyle` or |None| if no explicit value
57+
has been set. Assigning |None| removes any existing value.
58+
"""
59+
ln = self._ln
60+
if ln is None:
61+
return None
62+
join = ln.eg_lineJoinProperties
63+
if join is None:
64+
return None
65+
tag_name = join.tag.split("}")[-1]
66+
return {"round": MSO_LINE_JOIN_STYLE.ROUND, "bevel": MSO_LINE_JOIN_STYLE.BEVEL,
67+
"miter": MSO_LINE_JOIN_STYLE.MITER}[tag_name]
68+
69+
@join_style.setter
70+
def join_style(self, value: MSO_LINE_JOIN_STYLE | None) -> None:
71+
ln = self._get_or_add_ln()
72+
if value is None:
73+
ln._remove_eg_lineJoinProperties()
74+
return
75+
method_map = {
76+
MSO_LINE_JOIN_STYLE.ROUND: "get_or_change_to_round",
77+
MSO_LINE_JOIN_STYLE.BEVEL: "get_or_change_to_bevel",
78+
MSO_LINE_JOIN_STYLE.MITER: "get_or_change_to_miter",
79+
}
80+
getattr(ln, method_map[value])()
81+
3582
@property
3683
def dash_style(self):
3784
"""Return value indicating line style.

src/pptx/enum/dml.py

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

141141

142+
class MSO_LINE_CAP_STYLE(BaseXmlEnum):
143+
"""Specifies the cap style for a line.
144+
145+
Example::
146+
147+
from pptx.enum.dml import MSO_LINE_CAP_STYLE
148+
149+
shape.line.cap_style = MSO_LINE_CAP_STYLE.ROUND
150+
151+
MS API name: `MsoLineCap` (not in MS API, maps to DrawingML `ST_LineCap`)
152+
"""
153+
154+
FLAT = (1, "flat", "A flat cap at the end of a line.")
155+
"""A flat cap at the end of a line."""
156+
157+
ROUND = (2, "rnd", "A round cap at the end of a line.")
158+
"""A round cap at the end of a line."""
159+
160+
SQUARE = (3, "sq", "A square cap at the end of a line.")
161+
"""A square cap at the end of a line."""
162+
163+
164+
class MSO_LINE_JOIN_STYLE(BaseXmlEnum):
165+
"""Specifies the join style for a line.
166+
167+
Example::
168+
169+
from pptx.enum.dml import MSO_LINE_JOIN_STYLE
170+
171+
shape.line.join_style = MSO_LINE_JOIN_STYLE.MITER
172+
173+
MS API name: Not directly in MS API, maps to DrawingML `EG_LineJoinProperties`.
174+
"""
175+
176+
ROUND = (1, "round", "A round join between two lines.")
177+
"""A round join between two lines."""
178+
179+
BEVEL = (2, "bevel", "A bevel join between two lines.")
180+
"""A bevel join between two lines."""
181+
182+
MITER = (3, "miter", "A miter join between two lines.")
183+
"""A miter join between two lines."""
184+
185+
142186
class MSO_PATTERN_TYPE(BaseXmlEnum):
143187
"""Specifies the fill pattern used in a shape.
144188

src/pptx/oxml/shapes/shared.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +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_CAP_STYLE
89
from pptx.enum.shapes import PP_PLACEHOLDER
910
from pptx.oxml.ns import qn
1011
from pptx.oxml.simpletypes import (
@@ -264,8 +265,19 @@ class CT_LineProperties(BaseOxmlElement):
264265
)
265266
prstDash = ZeroOrOne("a:prstDash", successors=_tag_seq[5:])
266267
custDash = ZeroOrOne("a:custDash", successors=_tag_seq[6:])
268+
eg_lineJoinProperties = ZeroOrOneChoice(
269+
(
270+
Choice("a:round"),
271+
Choice("a:bevel"),
272+
Choice("a:miter"),
273+
),
274+
successors=_tag_seq[9:],
275+
)
267276
del _tag_seq
268277
w = OptionalAttribute("w", ST_LineWidth, default=Emu(0))
278+
cap: MSO_LINE_CAP_STYLE | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
279+
"cap", MSO_LINE_CAP_STYLE
280+
)
269281

270282
@property
271283
def eg_fillProperties(self):

tests/dml/test_line.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
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
10+
from pptx.enum.dml import MSO_FILL, MSO_LINE, MSO_LINE_CAP_STYLE, MSO_LINE_JOIN_STYLE
1111
from pptx.oxml.shapes.shared import CT_LineProperties
1212
from pptx.shapes.autoshape import Shape
1313

@@ -17,6 +17,69 @@
1717

1818

1919
class DescribeLineFormat(object):
20+
@pytest.mark.parametrize(
21+
("spPr_cxml", "expected_value"),
22+
[
23+
("p:spPr", None),
24+
("p:spPr/a:ln", None),
25+
("p:spPr/a:ln{cap=rnd}", MSO_LINE_CAP_STYLE.ROUND),
26+
("p:spPr/a:ln{cap=sq}", MSO_LINE_CAP_STYLE.SQUARE),
27+
("p:spPr/a:ln{cap=flat}", MSO_LINE_CAP_STYLE.FLAT),
28+
],
29+
)
30+
def it_knows_its_cap_style(
31+
self, spPr_cxml: str, expected_value: MSO_LINE_CAP_STYLE | None
32+
):
33+
line = LineFormat(element(spPr_cxml))
34+
assert line.cap_style == expected_value
35+
36+
@pytest.mark.parametrize(
37+
("spPr_cxml", "value", "expected_cxml"),
38+
[
39+
("p:spPr{a:b=c}", MSO_LINE_CAP_STYLE.ROUND, "p:spPr{a:b=c}/a:ln{cap=rnd}"),
40+
("p:spPr/a:ln{cap=rnd}", MSO_LINE_CAP_STYLE.SQUARE, "p:spPr/a:ln{cap=sq}"),
41+
("p:spPr/a:ln{cap=sq}", None, "p:spPr/a:ln"),
42+
],
43+
)
44+
def it_can_change_its_cap_style(
45+
self, spPr_cxml: str, value: MSO_LINE_CAP_STYLE | None, expected_cxml: str
46+
):
47+
spPr = element(spPr_cxml)
48+
LineFormat(spPr).cap_style = value
49+
assert spPr.xml == xml(expected_cxml)
50+
51+
@pytest.mark.parametrize(
52+
("spPr_cxml", "expected_value"),
53+
[
54+
("p:spPr", None),
55+
("p:spPr/a:ln", None),
56+
("p:spPr/a:ln/a:round", MSO_LINE_JOIN_STYLE.ROUND),
57+
("p:spPr/a:ln/a:bevel", MSO_LINE_JOIN_STYLE.BEVEL),
58+
("p:spPr/a:ln/a:miter", MSO_LINE_JOIN_STYLE.MITER),
59+
],
60+
)
61+
def it_knows_its_join_style(
62+
self, spPr_cxml: str, expected_value: MSO_LINE_JOIN_STYLE | None
63+
):
64+
line = LineFormat(element(spPr_cxml))
65+
assert line.join_style == expected_value
66+
67+
@pytest.mark.parametrize(
68+
("spPr_cxml", "value", "expected_cxml"),
69+
[
70+
("p:spPr{a:b=c}", MSO_LINE_JOIN_STYLE.ROUND, "p:spPr{a:b=c}/a:ln/a:round"),
71+
("p:spPr/a:ln", MSO_LINE_JOIN_STYLE.BEVEL, "p:spPr/a:ln/a:bevel"),
72+
("p:spPr/a:ln/a:round", MSO_LINE_JOIN_STYLE.MITER, "p:spPr/a:ln/a:miter"),
73+
("p:spPr/a:ln/a:miter", None, "p:spPr/a:ln"),
74+
],
75+
)
76+
def it_can_change_its_join_style(
77+
self, spPr_cxml: str, value: MSO_LINE_JOIN_STYLE | None, expected_cxml: str
78+
):
79+
spPr = element(spPr_cxml)
80+
LineFormat(spPr).join_style = value
81+
assert spPr.xml == xml(expected_cxml)
82+
2083
def it_knows_its_dash_style(self, dash_style_get_fixture):
2184
line, expected_value = dash_style_get_fixture
2285
assert line.dash_style == expected_value

0 commit comments

Comments
 (0)