Skip to content

Commit 296ca19

Browse files
authored
Merge pull request #4 from MHoroszowski/feature/table-cell-borders
feature: add per-edge border styling for table cells
2 parents 1d2e194 + ed97f6a commit 296ca19

File tree

4 files changed

+138
-1
lines changed

4 files changed

+138
-1
lines changed

src/pptx/oxml/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,10 @@ def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]):
385385
register_element_cls("a:ext", CT_PositiveSize2D)
386386
register_element_cls("a:headEnd", CT_LineEndProperties)
387387
register_element_cls("a:ln", CT_LineProperties)
388+
register_element_cls("a:lnB", CT_LineProperties)
389+
register_element_cls("a:lnL", CT_LineProperties)
390+
register_element_cls("a:lnR", CT_LineProperties)
391+
register_element_cls("a:lnT", CT_LineProperties)
388392
register_element_cls("a:off", CT_Point2D)
389393
register_element_cls("a:tailEnd", CT_LineEndProperties)
390394
register_element_cls("a:xfrm", CT_Transform2D)

src/pptx/oxml/table.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,32 @@ def _set_marX(self, marX: str, value: Length | None) -> None:
363363
class CT_TableCellProperties(BaseOxmlElement):
364364
"""`a:tcPr` custom element class"""
365365

366+
get_or_add_lnL: Callable[..., BaseOxmlElement]
367+
get_or_add_lnR: Callable[..., BaseOxmlElement]
368+
get_or_add_lnT: Callable[..., BaseOxmlElement]
369+
get_or_add_lnB: Callable[..., BaseOxmlElement]
370+
371+
_tag_seq = (
372+
"a:lnL",
373+
"a:lnR",
374+
"a:lnT",
375+
"a:lnB",
376+
"a:lnTlToBr",
377+
"a:lnBlToTr",
378+
"a:cell3D",
379+
"a:noFill",
380+
"a:solidFill",
381+
"a:gradFill",
382+
"a:blipFill",
383+
"a:pattFill",
384+
"a:grpFill",
385+
"a:headers",
386+
"a:extLst",
387+
)
388+
lnL = ZeroOrOne("a:lnL", successors=_tag_seq[1:])
389+
lnR = ZeroOrOne("a:lnR", successors=_tag_seq[2:])
390+
lnT = ZeroOrOne("a:lnT", successors=_tag_seq[3:])
391+
lnB = ZeroOrOne("a:lnB", successors=_tag_seq[4:])
366392
eg_fillProperties = ZeroOrOneChoice(
367393
(
368394
Choice("a:noFill"),
@@ -372,8 +398,9 @@ class CT_TableCellProperties(BaseOxmlElement):
372398
Choice("a:pattFill"),
373399
Choice("a:grpFill"),
374400
),
375-
successors=("a:headers", "a:extLst"),
401+
successors=_tag_seq[13:],
376402
)
403+
del _tag_seq
377404
anchor: MSO_VERTICAL_ANCHOR | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
378405
"anchor", MSO_VERTICAL_ANCHOR
379406
)

src/pptx/table.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import TYPE_CHECKING, Iterator
66

77
from pptx.dml.fill import FillFormat
8+
from pptx.dml.line import LineFormat
89
from pptx.oxml.table import TcRange
910
from pptx.shapes import Subshape
1011
from pptx.text.text import TextFrame
@@ -165,6 +166,62 @@ def vert_banding(self, value: bool):
165166
self._tbl.bandCol = value
166167

167168

169+
class _BorderEdge:
170+
"""Adapter providing a `LineFormat`-compatible interface for one edge of a cell border.
171+
172+
`LineFormat` requires a parent with `.ln` and `.get_or_add_ln()`. This adapter delegates those
173+
to the appropriate border element (`a:lnL`, `a:lnR`, `a:lnT`, `a:lnB`) on `a:tcPr`.
174+
"""
175+
176+
def __init__(self, tc: CT_TableCell, edge_attr: str):
177+
self._tc = tc
178+
self._edge_attr = edge_attr # e.g. "lnL", "lnR", "lnT", "lnB"
179+
180+
@property
181+
def ln(self):
182+
"""Return the `a:lnX` element or None."""
183+
tcPr = self._tc.tcPr
184+
if tcPr is None:
185+
return None
186+
return getattr(tcPr, self._edge_attr)
187+
188+
def get_or_add_ln(self):
189+
"""Return the `a:lnX` element, creating `a:tcPr` and the element if not present."""
190+
tcPr = self._tc.get_or_add_tcPr()
191+
return getattr(tcPr, f"get_or_add_{self._edge_attr}")()
192+
193+
194+
class _CellBorders:
195+
"""Provides access to border line formatting for each edge of a table cell.
196+
197+
Accessed via `cell.borders`. Each edge (`.left`, `.right`, `.top`, `.bottom`) returns a
198+
|LineFormat| object that controls the border's color, width, and dash style.
199+
"""
200+
201+
def __init__(self, tc: CT_TableCell):
202+
self._tc = tc
203+
204+
@lazyproperty
205+
def bottom(self) -> LineFormat:
206+
"""|LineFormat| for the bottom border of this cell."""
207+
return LineFormat(_BorderEdge(self._tc, "lnB"))
208+
209+
@lazyproperty
210+
def left(self) -> LineFormat:
211+
"""|LineFormat| for the left border of this cell."""
212+
return LineFormat(_BorderEdge(self._tc, "lnL"))
213+
214+
@lazyproperty
215+
def right(self) -> LineFormat:
216+
"""|LineFormat| for the right border of this cell."""
217+
return LineFormat(_BorderEdge(self._tc, "lnR"))
218+
219+
@lazyproperty
220+
def top(self) -> LineFormat:
221+
"""|LineFormat| for the top border of this cell."""
222+
return LineFormat(_BorderEdge(self._tc, "lnT"))
223+
224+
168225
class _Cell(Subshape):
169226
"""Table cell"""
170227

@@ -187,6 +244,21 @@ def __ne__(self, other: object) -> bool:
187244
return True
188245
return self._tc is not other._tc
189246

247+
@lazyproperty
248+
def borders(self) -> _CellBorders:
249+
"""|_CellBorders| instance for this cell.
250+
251+
Provides access to the line formatting for each border edge. Each edge (`.left`, `.right`,
252+
`.top`, `.bottom`) is a |LineFormat| object.
253+
254+
Example::
255+
256+
cell.borders.top.width = Pt(2)
257+
cell.borders.top.color.rgb = RGBColor(0xFF, 0x00, 0x00)
258+
cell.borders.bottom.dash_style = MSO_LINE.DASH
259+
"""
260+
return _CellBorders(self._tc)
261+
190262
@lazyproperty
191263
def fill(self) -> FillFormat:
192264
"""|FillFormat| instance for this cell.

tests/test_table.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
import pytest
88

99
from pptx.dml.fill import FillFormat
10+
from pptx.dml.line import LineFormat
1011
from pptx.enum.text import MSO_ANCHOR
1112
from pptx.oxml.ns import qn
1213
from pptx.oxml.table import CT_Table, CT_TableCell, TcRange
1314
from pptx.shapes.graphfrm import GraphicFrame
1415
from pptx.table import (
1516
Table,
1617
_Cell,
18+
_CellBorders,
1719
_CellCollection,
1820
_Column,
1921
_ColumnCollection,
@@ -207,6 +209,38 @@ def it_is_equal_to_other_instance_having_same_tc(self):
207209
assert cell == cell_with_same_tc
208210
assert cell != cell_with_other_tc
209211

212+
def it_provides_access_to_its_borders(self):
213+
tc = element("a:tc/a:tcPr")
214+
cell = _Cell(tc, None)
215+
borders = cell.borders
216+
assert isinstance(borders, _CellBorders)
217+
assert isinstance(borders.left, LineFormat)
218+
assert isinstance(borders.right, LineFormat)
219+
assert isinstance(borders.top, LineFormat)
220+
assert isinstance(borders.bottom, LineFormat)
221+
222+
@pytest.mark.parametrize(
223+
("tc_cxml", "edge", "expected_width"),
224+
[
225+
("a:tc/a:tcPr", "left", 0),
226+
("a:tc/a:tcPr/a:lnL{w=25400}", "left", 25400),
227+
("a:tc/a:tcPr/a:lnR{w=12700}", "right", 12700),
228+
("a:tc/a:tcPr/a:lnT{w=38100}", "top", 38100),
229+
("a:tc/a:tcPr/a:lnB{w=6350}", "bottom", 6350),
230+
],
231+
)
232+
def it_can_read_border_width(self, tc_cxml: str, edge: str, expected_width: int):
233+
cell = _Cell(element(tc_cxml), None)
234+
border_line = getattr(cell.borders, edge)
235+
assert border_line.width == expected_width
236+
237+
def it_can_set_a_border_width(self):
238+
tc = element("a:tc/a:tcPr")
239+
cell = _Cell(tc, None)
240+
cell.borders.top.width = Pt(2)
241+
assert cell._tc.tcPr.lnT is not None
242+
assert cell._tc.tcPr.lnT.w == Pt(2)
243+
210244
def it_has_a_fill(self, fill_fixture):
211245
cell = fill_fixture
212246
assert isinstance(cell.fill, FillFormat)

0 commit comments

Comments
 (0)