python-pptx2 2.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pptx2/__init__.py +152 -0
- pptx2/_color.py +75 -0
- pptx2/_slide_importer.py +597 -0
- pptx2/_svg.py +155 -0
- pptx2/_template_applier.py +292 -0
- pptx2/_textstyle.py +187 -0
- pptx2/accessibility.py +365 -0
- pptx2/action.py +270 -0
- pptx2/animation.py +2237 -0
- pptx2/api.py +49 -0
- pptx2/audit.py +258 -0
- pptx2/chart/__init__.py +0 -0
- pptx2/chart/analytics.py +381 -0
- pptx2/chart/axis.py +543 -0
- pptx2/chart/category.py +200 -0
- pptx2/chart/chart.py +670 -0
- pptx2/chart/data.py +864 -0
- pptx2/chart/datalabel.py +406 -0
- pptx2/chart/legend.py +86 -0
- pptx2/chart/marker.py +70 -0
- pptx2/chart/palettes.py +129 -0
- pptx2/chart/plot.py +462 -0
- pptx2/chart/point.py +101 -0
- pptx2/chart/quick_layouts.py +325 -0
- pptx2/chart/series.py +334 -0
- pptx2/chart/xlsx.py +272 -0
- pptx2/chart/xmlwriter.py +1845 -0
- pptx2/compose/__init__.py +28 -0
- pptx2/compose/from_spec.py +1094 -0
- pptx2/design/__init__.py +8 -0
- pptx2/design/components.py +607 -0
- pptx2/design/figures.py +389 -0
- pptx2/design/layout.py +370 -0
- pptx2/design/recipes.py +1967 -0
- pptx2/design/style.py +209 -0
- pptx2/design/tokens.py +915 -0
- pptx2/diagrams.py +754 -0
- pptx2/dml/__init__.py +0 -0
- pptx2/dml/chtfmt.py +40 -0
- pptx2/dml/color.py +496 -0
- pptx2/dml/effect.py +909 -0
- pptx2/dml/fill.py +691 -0
- pptx2/dml/line.py +287 -0
- pptx2/dml/picture.py +212 -0
- pptx2/dml/three_d.py +381 -0
- pptx2/enum/__init__.py +0 -0
- pptx2/enum/action.py +71 -0
- pptx2/enum/animation.py +31 -0
- pptx2/enum/base.py +218 -0
- pptx2/enum/chart.py +574 -0
- pptx2/enum/dml.py +740 -0
- pptx2/enum/lang.py +685 -0
- pptx2/enum/presentation.py +133 -0
- pptx2/enum/shapes.py +1029 -0
- pptx2/enum/text.py +230 -0
- pptx2/exc.py +42 -0
- pptx2/formats.py +139 -0
- pptx2/geometry.py +420 -0
- pptx2/inherit.py +109 -0
- pptx2/lint.py +2256 -0
- pptx2/math.py +177 -0
- pptx2/media.py +197 -0
- pptx2/opc/__init__.py +0 -0
- pptx2/opc/constants.py +332 -0
- pptx2/opc/oxml.py +188 -0
- pptx2/opc/package.py +762 -0
- pptx2/opc/packuri.py +109 -0
- pptx2/opc/serialized.py +296 -0
- pptx2/opc/shared.py +20 -0
- pptx2/opc/spec.py +45 -0
- pptx2/oxml/__init__.py +555 -0
- pptx2/oxml/action.py +53 -0
- pptx2/oxml/chart/__init__.py +0 -0
- pptx2/oxml/chart/axis.py +337 -0
- pptx2/oxml/chart/chart.py +481 -0
- pptx2/oxml/chart/datalabel.py +253 -0
- pptx2/oxml/chart/legend.py +72 -0
- pptx2/oxml/chart/marker.py +61 -0
- pptx2/oxml/chart/plot.py +365 -0
- pptx2/oxml/chart/series.py +425 -0
- pptx2/oxml/chart/shared.py +220 -0
- pptx2/oxml/coreprops.py +288 -0
- pptx2/oxml/dml/__init__.py +0 -0
- pptx2/oxml/dml/color.py +135 -0
- pptx2/oxml/dml/effect.py +213 -0
- pptx2/oxml/dml/fill.py +316 -0
- pptx2/oxml/dml/line.py +12 -0
- pptx2/oxml/dml/three_d.py +110 -0
- pptx2/oxml/ns.py +135 -0
- pptx2/oxml/presentation.py +313 -0
- pptx2/oxml/shapes/__init__.py +19 -0
- pptx2/oxml/shapes/autoshape.py +467 -0
- pptx2/oxml/shapes/connector.py +107 -0
- pptx2/oxml/shapes/graphfrm.py +347 -0
- pptx2/oxml/shapes/groupshape.py +329 -0
- pptx2/oxml/shapes/picture.py +270 -0
- pptx2/oxml/shapes/shared.py +577 -0
- pptx2/oxml/simpletypes.py +1027 -0
- pptx2/oxml/slide.py +563 -0
- pptx2/oxml/table.py +650 -0
- pptx2/oxml/text.py +815 -0
- pptx2/oxml/theme.py +36 -0
- pptx2/oxml/xmlchemy.py +717 -0
- pptx2/package.py +222 -0
- pptx2/parts/__init__.py +0 -0
- pptx2/parts/chart.py +95 -0
- pptx2/parts/coreprops.py +167 -0
- pptx2/parts/diagram.py +37 -0
- pptx2/parts/embeddedpackage.py +93 -0
- pptx2/parts/image.py +275 -0
- pptx2/parts/media.py +37 -0
- pptx2/parts/presentation.py +136 -0
- pptx2/parts/slide.py +371 -0
- pptx2/presentation.py +408 -0
- pptx2/py.typed +0 -0
- pptx2/render.py +586 -0
- pptx2/section.py +272 -0
- pptx2/shapes/__init__.py +26 -0
- pptx2/shapes/autoshape.py +442 -0
- pptx2/shapes/base.py +1078 -0
- pptx2/shapes/connector.py +297 -0
- pptx2/shapes/freeform.py +337 -0
- pptx2/shapes/graphfrm.py +316 -0
- pptx2/shapes/group.py +264 -0
- pptx2/shapes/picture.py +422 -0
- pptx2/shapes/placeholder.py +468 -0
- pptx2/shapes/shapetree.py +2027 -0
- pptx2/shared.py +82 -0
- pptx2/skill/SKILL.md +450 -0
- pptx2/skill/__init__.py +78 -0
- pptx2/skill/__main__.py +64 -0
- pptx2/skill/references/animations.md +189 -0
- pptx2/skill/references/basics.md +421 -0
- pptx2/skill/references/charts.md +254 -0
- pptx2/skill/references/compose.md +234 -0
- pptx2/skill/references/design.md +366 -0
- pptx2/skill/references/effects.md +249 -0
- pptx2/skill/references/end-to-end-deck.md +231 -0
- pptx2/skill/references/geometry-and-arrows.md +334 -0
- pptx2/skill/references/lint.md +275 -0
- pptx2/skill/references/math.md +86 -0
- pptx2/skill/references/picture-effects.md +129 -0
- pptx2/skill/references/render.md +151 -0
- pptx2/skill/references/smart-art.md +75 -0
- pptx2/skill/references/space-aware-authoring.md +249 -0
- pptx2/skill/references/tables.md +244 -0
- pptx2/skill/references/theme.md +127 -0
- pptx2/skill/references/three-d.md +109 -0
- pptx2/skill/references/transitions.md +100 -0
- pptx2/slide.py +1244 -0
- pptx2/smart_art.py +220 -0
- pptx2/spec.py +633 -0
- pptx2/table.py +1181 -0
- pptx2/table_styles.py +184 -0
- pptx2/templates/default.pptx +0 -0
- pptx2/templates/docx-icon.emf +0 -0
- pptx2/templates/generic-icon.emf +0 -0
- pptx2/templates/notes.xml +23 -0
- pptx2/templates/notesMaster.xml +352 -0
- pptx2/templates/pptx-icon.emf +0 -0
- pptx2/templates/theme.xml +321 -0
- pptx2/templates/xlsx-icon.emf +0 -0
- pptx2/text/__init__.py +0 -0
- pptx2/text/fonts.py +482 -0
- pptx2/text/layout.py +374 -0
- pptx2/text/text.py +1272 -0
- pptx2/theme.py +721 -0
- pptx2/types.py +36 -0
- pptx2/util.py +263 -0
- python_pptx2-2.13.0.dist-info/METADATA +351 -0
- python_pptx2-2.13.0.dist-info/RECORD +175 -0
- python_pptx2-2.13.0.dist-info/WHEEL +5 -0
- python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
- python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
- python_pptx2-2.13.0.dist-info/top_level.txt +1 -0
pptx2/table.py
ADDED
|
@@ -0,0 +1,1181 @@
|
|
|
1
|
+
"""Table-related objects such as Table and Cell."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Iterable, Iterator, Sequence, Union
|
|
6
|
+
|
|
7
|
+
from pptx2._color import coerce_color
|
|
8
|
+
from pptx2._textstyle import (
|
|
9
|
+
apply_body_defaults,
|
|
10
|
+
apply_text_style,
|
|
11
|
+
coerce_anchor,
|
|
12
|
+
coerce_length,
|
|
13
|
+
)
|
|
14
|
+
from pptx2.dml.fill import FillFormat
|
|
15
|
+
from pptx2.dml.line import LineFormat
|
|
16
|
+
from pptx2.oxml.table import TcRange
|
|
17
|
+
from pptx2.shapes import Subshape
|
|
18
|
+
from pptx2.text.text import TextFrame
|
|
19
|
+
from pptx2.util import Emu, lazyproperty
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from pptx2.dml.color import RGBColor
|
|
23
|
+
from pptx2.enum.text import MSO_VERTICAL_ANCHOR
|
|
24
|
+
from pptx2.oxml.shapes.shared import CT_LineProperties
|
|
25
|
+
from pptx2.oxml.table import CT_Table, CT_TableCell, CT_TableCellProperties, CT_TableCol, CT_TableRow
|
|
26
|
+
from pptx2.parts.slide import BaseSlidePart
|
|
27
|
+
from pptx2.shapes.graphfrm import GraphicFrame
|
|
28
|
+
from pptx2.types import ProvidesPart
|
|
29
|
+
from pptx2.util import Length
|
|
30
|
+
|
|
31
|
+
# A colour accepted anywhere in the library: hex string, (r, g, b) tuple,
|
|
32
|
+
# or RGBColor. Matches what LineFormat.color.rgb coercion accepts.
|
|
33
|
+
_ColorLike = str | tuple[int, int, int] | RGBColor
|
|
34
|
+
else:
|
|
35
|
+
# Runtime fallback so ``typing.get_type_hints()`` on the public border
|
|
36
|
+
# helpers resolves ``_ColorLike`` instead of raising ``NameError`` (the
|
|
37
|
+
# precise union above is type-checker-only, since RGBColor is not imported
|
|
38
|
+
# at runtime here).
|
|
39
|
+
_ColorLike = Any
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
#: What `Table.format_cells` accepts for its `rows` / `cols` arguments.
|
|
43
|
+
_CellSelector = Union[None, int, slice, Iterable[int]]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _resolve_selector(selector: _CellSelector, count: int, name: str) -> list[int]:
|
|
47
|
+
"""Return the concrete indices `selector` picks out of `count` rows/columns.
|
|
48
|
+
|
|
49
|
+
``None`` means all of them; an ``int`` picks one (negative counts from the
|
|
50
|
+
end); an iterable of ints picks several. An out-of-range index raises
|
|
51
|
+
:class:`IndexError` rather than silently selecting nothing — a typo'd row
|
|
52
|
+
number should not read as "styled zero cells, all good".
|
|
53
|
+
|
|
54
|
+
A ``slice`` is the exception: it follows ordinary Python slicing, so
|
|
55
|
+
``slice(1, None)`` on a one-row table yields no cells rather than raising,
|
|
56
|
+
the same as ``rows[1:]`` would.
|
|
57
|
+
"""
|
|
58
|
+
if selector is None:
|
|
59
|
+
return list(range(count))
|
|
60
|
+
if isinstance(selector, slice):
|
|
61
|
+
return list(range(count))[selector]
|
|
62
|
+
idxs = [selector] if isinstance(selector, int) else list(selector)
|
|
63
|
+
out: list[int] = []
|
|
64
|
+
for idx in idxs:
|
|
65
|
+
i = int(idx)
|
|
66
|
+
if i < 0:
|
|
67
|
+
i += count
|
|
68
|
+
if not 0 <= i < count:
|
|
69
|
+
raise IndexError(f"{name} index {idx} out of range for table with {count} {name}")
|
|
70
|
+
out.append(i)
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _apply_cell_margins(
|
|
75
|
+
cell: "_Cell", margin: "float | Length | Sequence[float | Length]"
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Set a cell's four insets from a scalar or ``(top, right, bottom, left)``."""
|
|
78
|
+
if isinstance(margin, (tuple, list)):
|
|
79
|
+
if len(margin) != 4:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
"margin tuple must have 4 elements (top, right, bottom, left); "
|
|
82
|
+
f"got {len(margin)}"
|
|
83
|
+
)
|
|
84
|
+
top, right, bottom, left = (coerce_length(v) for v in margin)
|
|
85
|
+
else:
|
|
86
|
+
top = right = bottom = left = coerce_length(margin)
|
|
87
|
+
cell.margin_top, cell.margin_right = top, right
|
|
88
|
+
cell.margin_bottom, cell.margin_left = bottom, left
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Table(object):
|
|
92
|
+
"""A DrawingML table object.
|
|
93
|
+
|
|
94
|
+
Not intended to be constructed directly, use
|
|
95
|
+
:meth:`.Slide.shapes.add_table` to add a table to a slide.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, tbl: CT_Table, graphic_frame: GraphicFrame):
|
|
99
|
+
super(Table, self).__init__()
|
|
100
|
+
self._tbl = tbl
|
|
101
|
+
self._graphic_frame = graphic_frame
|
|
102
|
+
|
|
103
|
+
def cell(self, row_idx: int, col_idx: int) -> _Cell:
|
|
104
|
+
"""Return cell at `row_idx`, `col_idx`.
|
|
105
|
+
|
|
106
|
+
Return value is an instance of |_Cell|. `row_idx` and `col_idx` are zero-based, e.g.
|
|
107
|
+
cell(0, 0) is the top, left cell in the table.
|
|
108
|
+
"""
|
|
109
|
+
return _Cell(self._tbl.tc(row_idx, col_idx), self)
|
|
110
|
+
|
|
111
|
+
@lazyproperty
|
|
112
|
+
def columns(self) -> _ColumnCollection:
|
|
113
|
+
"""|_ColumnCollection| instance for this table.
|
|
114
|
+
|
|
115
|
+
Provides access to |_Column| objects representing the table's columns. |_Column| objects
|
|
116
|
+
are accessed using list notation, e.g. `col = tbl.columns[0]`.
|
|
117
|
+
"""
|
|
118
|
+
return _ColumnCollection(self._tbl, self)
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def first_col(self) -> bool:
|
|
122
|
+
"""When `True`, indicates first column should have distinct formatting.
|
|
123
|
+
|
|
124
|
+
Read/write. Distinct formatting is used, for example, when the first column contains row
|
|
125
|
+
headings (is a side-heading column).
|
|
126
|
+
"""
|
|
127
|
+
return self._tbl.firstCol
|
|
128
|
+
|
|
129
|
+
@first_col.setter
|
|
130
|
+
def first_col(self, value: bool):
|
|
131
|
+
self._tbl.firstCol = value
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def first_row(self) -> bool:
|
|
135
|
+
"""When `True`, indicates first row should have distinct formatting.
|
|
136
|
+
|
|
137
|
+
Read/write. Distinct formatting is used, for example, when the first row contains column
|
|
138
|
+
headings.
|
|
139
|
+
"""
|
|
140
|
+
return self._tbl.firstRow
|
|
141
|
+
|
|
142
|
+
@first_row.setter
|
|
143
|
+
def first_row(self, value: bool):
|
|
144
|
+
self._tbl.firstRow = value
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def horz_banding(self) -> bool:
|
|
148
|
+
"""When `True`, indicates rows should have alternating shading.
|
|
149
|
+
|
|
150
|
+
Read/write. Used to allow rows to be traversed more easily without losing track of which
|
|
151
|
+
row is being read.
|
|
152
|
+
"""
|
|
153
|
+
return self._tbl.bandRow
|
|
154
|
+
|
|
155
|
+
@horz_banding.setter
|
|
156
|
+
def horz_banding(self, value: bool):
|
|
157
|
+
self._tbl.bandRow = value
|
|
158
|
+
|
|
159
|
+
# Friendlier aliases — match the OOXML ``bandRow`` / ``bandCol``
|
|
160
|
+
# vocabulary that PowerPoint's UI uses ("banded rows / columns").
|
|
161
|
+
@property
|
|
162
|
+
def banded_rows(self) -> bool:
|
|
163
|
+
"""Alias for :attr:`horz_banding` — alternating row shading."""
|
|
164
|
+
return self._tbl.bandRow
|
|
165
|
+
|
|
166
|
+
@banded_rows.setter
|
|
167
|
+
def banded_rows(self, value: bool):
|
|
168
|
+
self._tbl.bandRow = value
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def banded_cols(self) -> bool:
|
|
172
|
+
"""Alias for :attr:`vert_banding` — alternating column shading."""
|
|
173
|
+
return self._tbl.bandCol
|
|
174
|
+
|
|
175
|
+
@banded_cols.setter
|
|
176
|
+
def banded_cols(self, value: bool):
|
|
177
|
+
self._tbl.bandCol = value
|
|
178
|
+
|
|
179
|
+
def iter_cells(self) -> Iterator[_Cell]:
|
|
180
|
+
"""Generate _Cell object for each cell in this table.
|
|
181
|
+
|
|
182
|
+
Each grid cell is generated in left-to-right, top-to-bottom order.
|
|
183
|
+
"""
|
|
184
|
+
return (_Cell(tc, self) for tc in self._tbl.iter_tcs())
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def last_col(self) -> bool:
|
|
188
|
+
"""When `True`, indicates the rightmost column should have distinct formatting.
|
|
189
|
+
|
|
190
|
+
Read/write. Used, for example, when a row totals column appears at the far right of the
|
|
191
|
+
table.
|
|
192
|
+
"""
|
|
193
|
+
return self._tbl.lastCol
|
|
194
|
+
|
|
195
|
+
@last_col.setter
|
|
196
|
+
def last_col(self, value: bool):
|
|
197
|
+
self._tbl.lastCol = value
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def last_row(self) -> bool:
|
|
201
|
+
"""When `True`, indicates the bottom row should have distinct formatting.
|
|
202
|
+
|
|
203
|
+
Read/write. Used, for example, when a totals row appears as the bottom row.
|
|
204
|
+
"""
|
|
205
|
+
return self._tbl.lastRow
|
|
206
|
+
|
|
207
|
+
@last_row.setter
|
|
208
|
+
def last_row(self, value: bool):
|
|
209
|
+
self._tbl.lastRow = value
|
|
210
|
+
|
|
211
|
+
def notify_height_changed(self) -> None:
|
|
212
|
+
"""Called by a row when its height changes.
|
|
213
|
+
|
|
214
|
+
Triggers the graphic frame to recalculate its total height (as the sum of the row
|
|
215
|
+
heights).
|
|
216
|
+
"""
|
|
217
|
+
new_table_height = Emu(sum([row.height for row in self.rows]))
|
|
218
|
+
self._graphic_frame.height = new_table_height
|
|
219
|
+
|
|
220
|
+
def notify_width_changed(self) -> None:
|
|
221
|
+
"""Called by a column when its width changes.
|
|
222
|
+
|
|
223
|
+
Triggers the graphic frame to recalculate its total width (as the sum of the column
|
|
224
|
+
widths).
|
|
225
|
+
"""
|
|
226
|
+
new_table_width = Emu(sum([col.width for col in self.columns]))
|
|
227
|
+
self._graphic_frame.width = new_table_width
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def part(self) -> BaseSlidePart:
|
|
231
|
+
"""The package part containing this table."""
|
|
232
|
+
return self._graphic_frame.part
|
|
233
|
+
|
|
234
|
+
@lazyproperty
|
|
235
|
+
def rows(self):
|
|
236
|
+
"""|_RowCollection| instance for this table.
|
|
237
|
+
|
|
238
|
+
Provides access to |_Row| objects representing the table's rows. |_Row| objects are
|
|
239
|
+
accessed using list notation, e.g. `col = tbl.rows[0]`.
|
|
240
|
+
"""
|
|
241
|
+
return _RowCollection(self._tbl, self)
|
|
242
|
+
|
|
243
|
+
def fit_to_box(
|
|
244
|
+
self,
|
|
245
|
+
*,
|
|
246
|
+
font_family: str = "Calibri",
|
|
247
|
+
max_font_pt: int = 18,
|
|
248
|
+
min_font_pt: int = 8,
|
|
249
|
+
bold: bool = False,
|
|
250
|
+
italic: bool = False,
|
|
251
|
+
font_file: str | None = None,
|
|
252
|
+
) -> int:
|
|
253
|
+
"""Shrink cell text font size until every cell fits within its bounds.
|
|
254
|
+
|
|
255
|
+
Walks every populated cell, computes the per-cell best-fit font
|
|
256
|
+
size against the cell's *own* width and row height (margins
|
|
257
|
+
respected), and applies the **smallest** of those sizes uniformly
|
|
258
|
+
to every cell — so the table reads as a single coherent grid
|
|
259
|
+
rather than each cell at its own size.
|
|
260
|
+
|
|
261
|
+
Returns the chosen size in points (clamped to ``min_font_pt``).
|
|
262
|
+
|
|
263
|
+
Useful for runtime-driven tables where row counts and string
|
|
264
|
+
lengths aren't known up front.
|
|
265
|
+
|
|
266
|
+
Parameters mirror :meth:`TextFrame.fit_text`.
|
|
267
|
+
"""
|
|
268
|
+
from pptx2.text.fonts import find_font_file
|
|
269
|
+
from pptx2.text.layout import TextFitter
|
|
270
|
+
from pptx2.util import Emu, Pt
|
|
271
|
+
|
|
272
|
+
if min_font_pt <= 0 or max_font_pt < min_font_pt:
|
|
273
|
+
raise ValueError(
|
|
274
|
+
"min_font_pt must be > 0 and max_font_pt must be >= min_font_pt"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
if font_file is None:
|
|
278
|
+
font_file = find_font_file(font_family, bold, italic)
|
|
279
|
+
|
|
280
|
+
# Default cell margins per OOXML: 0.1" left/right, 0.05" top/bottom.
|
|
281
|
+
DEFAULT_MARG_LR = 91440
|
|
282
|
+
DEFAULT_MARG_TB = 45720
|
|
283
|
+
|
|
284
|
+
cols = list(self.columns)
|
|
285
|
+
rows = list(self.rows)
|
|
286
|
+
|
|
287
|
+
per_cell_sizes: list[int] = []
|
|
288
|
+
for r_idx, row in enumerate(rows):
|
|
289
|
+
for c_idx, col in enumerate(cols):
|
|
290
|
+
cell = self.cell(r_idx, c_idx)
|
|
291
|
+
if not cell.text.strip():
|
|
292
|
+
continue
|
|
293
|
+
marL = cell.margin_left if cell.margin_left is not None else DEFAULT_MARG_LR
|
|
294
|
+
marR = cell.margin_right if cell.margin_right is not None else DEFAULT_MARG_LR
|
|
295
|
+
marT = cell.margin_top if cell.margin_top is not None else DEFAULT_MARG_TB
|
|
296
|
+
marB = cell.margin_bottom if cell.margin_bottom is not None else DEFAULT_MARG_TB
|
|
297
|
+
cx = max(1, int(col.width) - int(marL) - int(marR))
|
|
298
|
+
cy = max(1, int(row.height) - int(marT) - int(marB))
|
|
299
|
+
try:
|
|
300
|
+
size = TextFitter.best_fit_font_size(
|
|
301
|
+
cell.text, (Emu(cx), Emu(cy)), max_font_pt, font_file
|
|
302
|
+
)
|
|
303
|
+
except Exception:
|
|
304
|
+
# If measurement fails for a populated cell, treat it as
|
|
305
|
+
# the worst case so the final uniform size remains safe
|
|
306
|
+
# for every populated cell. Skipping the cell would let
|
|
307
|
+
# ``target = min(...)`` stay artificially high and other
|
|
308
|
+
# cells could end up still overflowing.
|
|
309
|
+
per_cell_sizes.append(int(min_font_pt))
|
|
310
|
+
continue
|
|
311
|
+
if size is None:
|
|
312
|
+
# Text genuinely does not fit at any size in this cell;
|
|
313
|
+
# treat as ``min_font_pt``.
|
|
314
|
+
per_cell_sizes.append(int(min_font_pt))
|
|
315
|
+
else:
|
|
316
|
+
per_cell_sizes.append(int(size))
|
|
317
|
+
|
|
318
|
+
target = min(per_cell_sizes) if per_cell_sizes else max_font_pt
|
|
319
|
+
target = max(target, min_font_pt)
|
|
320
|
+
|
|
321
|
+
for cell in self.iter_cells():
|
|
322
|
+
for paragraph in cell.text_frame.paragraphs:
|
|
323
|
+
for run in paragraph.runs:
|
|
324
|
+
run.font.size = Pt(target)
|
|
325
|
+
|
|
326
|
+
return int(target)
|
|
327
|
+
|
|
328
|
+
def format_cells(
|
|
329
|
+
self,
|
|
330
|
+
rows: "_CellSelector" = None,
|
|
331
|
+
cols: "_CellSelector" = None,
|
|
332
|
+
**style: Any,
|
|
333
|
+
) -> "Table":
|
|
334
|
+
"""Apply cell styling to a rectangular selection of cells; return self.
|
|
335
|
+
|
|
336
|
+
`rows` and `cols` each accept ``None`` (every row / column), an ``int``
|
|
337
|
+
(negative counts from the end), a ``slice``, or any iterable of ints.
|
|
338
|
+
The remaining keyword arguments are those of :meth:`_Cell.format`, so
|
|
339
|
+
the whole of a table's look is a handful of calls rather than a nest of
|
|
340
|
+
loops over ``cell.fill.fore_color.rgb``::
|
|
341
|
+
|
|
342
|
+
table.format_cells(rows=0, fill="#1F2937", color="#FFFFFF", bold=True)
|
|
343
|
+
table.format_cells(rows=slice(1, None), size_pt=11, anchor="middle")
|
|
344
|
+
table.format_cells(rows=range(2, len(table.rows), 2), fill="#F6F7F9")
|
|
345
|
+
table.format_cells(cols=-1, align="right")
|
|
346
|
+
|
|
347
|
+
Merged cells are styled through their origin cell only; spanned cells
|
|
348
|
+
carry no formatting of their own.
|
|
349
|
+
"""
|
|
350
|
+
row_idxs = _resolve_selector(rows, len(self.rows), "rows")
|
|
351
|
+
col_idxs = _resolve_selector(cols, len(self.columns), "cols")
|
|
352
|
+
for r in row_idxs:
|
|
353
|
+
for c in col_idxs:
|
|
354
|
+
cell = self.cell(r, c)
|
|
355
|
+
if cell.is_spanned:
|
|
356
|
+
continue
|
|
357
|
+
cell.format(**style)
|
|
358
|
+
return self
|
|
359
|
+
|
|
360
|
+
@property
|
|
361
|
+
def vert_banding(self) -> bool:
|
|
362
|
+
"""When `True`, indicates columns should have alternating shading.
|
|
363
|
+
|
|
364
|
+
Read/write. Used to allow columns to be traversed more easily without losing track of
|
|
365
|
+
which column is being read.
|
|
366
|
+
"""
|
|
367
|
+
return self._tbl.bandCol
|
|
368
|
+
|
|
369
|
+
@vert_banding.setter
|
|
370
|
+
def vert_banding(self, value: bool):
|
|
371
|
+
self._tbl.bandCol = value
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def style(self) -> str | None:
|
|
375
|
+
"""Built-in table style applied to this table, or |None|.
|
|
376
|
+
|
|
377
|
+
Read/write. PowerPoint ships a fixed gallery of named built-in
|
|
378
|
+
table styles ("Table Grid", "Medium Style 2 - Accent 1", "No Style,
|
|
379
|
+
No Grid", …); each is identified by a GUID stored in
|
|
380
|
+
``<a:tblPr><a:tableStyleId>``.
|
|
381
|
+
|
|
382
|
+
Reading returns the friendly name when the GUID is a recognized
|
|
383
|
+
built-in (see :data:`pptx2.table_styles.TABLE_STYLES`), the raw
|
|
384
|
+
``{GUID}`` string when it isn't, or |None| when no style id is
|
|
385
|
+
present.
|
|
386
|
+
|
|
387
|
+
Assigning accepts either a friendly name *or* a raw ``{GUID}``
|
|
388
|
+
string. An unknown friendly name raises :class:`ValueError` with a
|
|
389
|
+
"did you mean" hint. Assigning |None| detaches the table from any
|
|
390
|
+
built-in style (equivalent to :meth:`clear_style`)::
|
|
391
|
+
|
|
392
|
+
table.style = "Medium Style 2 - Accent 1"
|
|
393
|
+
table.style = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"
|
|
394
|
+
table.style = None
|
|
395
|
+
"""
|
|
396
|
+
tblPr = self._tbl.tblPr
|
|
397
|
+
if tblPr is None:
|
|
398
|
+
return None
|
|
399
|
+
guid = tblPr.tableStyleId_val
|
|
400
|
+
if guid is None:
|
|
401
|
+
return None
|
|
402
|
+
from pptx2.table_styles import name_for_guid
|
|
403
|
+
|
|
404
|
+
return name_for_guid(guid) or guid
|
|
405
|
+
|
|
406
|
+
@style.setter
|
|
407
|
+
def style(self, value: str | None) -> None:
|
|
408
|
+
if value is None:
|
|
409
|
+
self.clear_style()
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
from pptx2.table_styles import guid_for_name
|
|
413
|
+
|
|
414
|
+
text = value.strip()
|
|
415
|
+
is_raw_guid = text.startswith("{") and text.endswith("}")
|
|
416
|
+
guid = text if is_raw_guid else guid_for_name(text)
|
|
417
|
+
|
|
418
|
+
tblPr = self._tbl.get_or_add_tblPr()
|
|
419
|
+
tblPr.tableStyleId_val = guid
|
|
420
|
+
|
|
421
|
+
def clear_style(self) -> None:
|
|
422
|
+
"""Detach this table from any built-in table style.
|
|
423
|
+
|
|
424
|
+
Removes the ``<a:tableStyleId>`` element from ``a:tblPr``. By
|
|
425
|
+
default, every table created via ``slide.shapes.add_table(...)``
|
|
426
|
+
is attached to PowerPoint's "Medium Style 2 — Accent 1"
|
|
427
|
+
(``{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}``), which paints
|
|
428
|
+
alternating-row banding even when :attr:`horz_banding` is set
|
|
429
|
+
to ``False`` — the toggles only control *bandRow/bandCol*
|
|
430
|
+
attributes, not the style's own banded-row overlay (which
|
|
431
|
+
LibreOffice and PowerPoint apply independently of the toggle).
|
|
432
|
+
|
|
433
|
+
Call this when "I'll style every cell myself" — every fill,
|
|
434
|
+
border, and font is set explicitly — so the style's defaults
|
|
435
|
+
don't bleed through any cells the caller didn't paint. See
|
|
436
|
+
IMPROVEMENTS item 4.
|
|
437
|
+
"""
|
|
438
|
+
tblPr = self._tbl.tblPr
|
|
439
|
+
if tblPr is None:
|
|
440
|
+
return
|
|
441
|
+
from pptx2.oxml.ns import qn
|
|
442
|
+
|
|
443
|
+
for style_id in tblPr.findall(qn("a:tableStyleId")):
|
|
444
|
+
tblPr.remove(style_id)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class _Cell(Subshape):
|
|
448
|
+
"""Table cell"""
|
|
449
|
+
|
|
450
|
+
def __init__(self, tc: CT_TableCell, parent: ProvidesPart):
|
|
451
|
+
super(_Cell, self).__init__(parent)
|
|
452
|
+
self._tc = tc
|
|
453
|
+
|
|
454
|
+
def __eq__(self, other: object) -> bool:
|
|
455
|
+
"""|True| if this object proxies the same element as `other`.
|
|
456
|
+
|
|
457
|
+
Equality for proxy objects is defined as referring to the same XML element, whether or not
|
|
458
|
+
they are the same proxy object instance.
|
|
459
|
+
"""
|
|
460
|
+
if not isinstance(other, type(self)):
|
|
461
|
+
return False
|
|
462
|
+
return self._tc is other._tc
|
|
463
|
+
|
|
464
|
+
def __ne__(self, other: object) -> bool:
|
|
465
|
+
if not isinstance(other, type(self)):
|
|
466
|
+
return True
|
|
467
|
+
return self._tc is not other._tc
|
|
468
|
+
|
|
469
|
+
@lazyproperty
|
|
470
|
+
def borders(self) -> _Borders:
|
|
471
|
+
"""|_Borders| value object exposing per-edge border line formatting.
|
|
472
|
+
|
|
473
|
+
Each border edge is a |LineFormat| reachable as `borders.left`,
|
|
474
|
+
`borders.right`, `borders.top`, `borders.bottom`, `borders.diagonal_down`,
|
|
475
|
+
and `borders.diagonal_up`. Convenience helpers `borders.all(...)`,
|
|
476
|
+
`borders.outer(...)`, and `borders.none()` apply settings across
|
|
477
|
+
multiple edges in one call.
|
|
478
|
+
"""
|
|
479
|
+
return _Borders(self._tc)
|
|
480
|
+
|
|
481
|
+
@lazyproperty
|
|
482
|
+
def fill(self) -> FillFormat:
|
|
483
|
+
"""|FillFormat| instance for this cell.
|
|
484
|
+
|
|
485
|
+
Provides access to fill properties such as foreground color.
|
|
486
|
+
"""
|
|
487
|
+
tcPr = self._tc.get_or_add_tcPr()
|
|
488
|
+
return FillFormat.from_fill_parent(tcPr)
|
|
489
|
+
|
|
490
|
+
def format(
|
|
491
|
+
self,
|
|
492
|
+
*,
|
|
493
|
+
fill: "_ColorLike | str | None" = None,
|
|
494
|
+
color: "_ColorLike | None" = None,
|
|
495
|
+
font: str | None = None,
|
|
496
|
+
size_pt: float | None = None,
|
|
497
|
+
bold: bool | None = None,
|
|
498
|
+
italic: bool | None = None,
|
|
499
|
+
align: str | None = None,
|
|
500
|
+
anchor: str | None = None,
|
|
501
|
+
margin: "float | Length | Sequence[float | Length] | None" = None,
|
|
502
|
+
word_wrap: bool | None = None,
|
|
503
|
+
) -> "_Cell":
|
|
504
|
+
"""Style this cell's fill and text in one call; return self.
|
|
505
|
+
|
|
506
|
+
Every argument is optional and ``None`` means "leave alone", so calls
|
|
507
|
+
layer. The keyword vocabulary is the same as
|
|
508
|
+
:meth:`ShapeTree.add_text`, and colours accept anything the rest of the
|
|
509
|
+
library does — hex string, ``(r, g, b)`` tuple, or ``RGBColor``::
|
|
510
|
+
|
|
511
|
+
table.cell(0, 0).format(fill="#1F2937", color="#FFFFFF", bold=True)
|
|
512
|
+
table.cell(3, 2).format(align="right", size_pt=11, margin=(2, 8, 2, 8))
|
|
513
|
+
|
|
514
|
+
`fill` also accepts the string ``"none"`` for a transparent cell.
|
|
515
|
+
`margin` is in points — a scalar for all four insets, or a
|
|
516
|
+
``(top, right, bottom, left)`` 4-sequence.
|
|
517
|
+
|
|
518
|
+
Formatting is recorded as the cell's text-body defaults as well as on
|
|
519
|
+
its current text, so either order works — style an empty cell and then
|
|
520
|
+
assign ``cell.text``, or populate first and style afterwards.
|
|
521
|
+
"""
|
|
522
|
+
if fill is not None:
|
|
523
|
+
if isinstance(fill, str) and fill.lower() == "none":
|
|
524
|
+
self.fill.background()
|
|
525
|
+
else:
|
|
526
|
+
self.fill.solid()
|
|
527
|
+
self.fill.fore_color.rgb = coerce_color(fill)
|
|
528
|
+
# A cell's anchor and insets live on `<a:tcPr>`, not on the text
|
|
529
|
+
# frame's `<a:bodyPr>` — PowerPoint reads the cell properties and
|
|
530
|
+
# ignores the body ones, so route those two through `_Cell`.
|
|
531
|
+
if anchor is not None:
|
|
532
|
+
self.vertical_anchor = coerce_anchor(anchor)
|
|
533
|
+
if margin is not None:
|
|
534
|
+
_apply_cell_margins(self, margin)
|
|
535
|
+
text_frame = self.text_frame
|
|
536
|
+
apply_text_style(
|
|
537
|
+
text_frame,
|
|
538
|
+
font=font,
|
|
539
|
+
size_pt=size_pt,
|
|
540
|
+
bold=bold,
|
|
541
|
+
italic=italic,
|
|
542
|
+
color=color,
|
|
543
|
+
align=align,
|
|
544
|
+
word_wrap=word_wrap,
|
|
545
|
+
paragraph_defaults=True,
|
|
546
|
+
)
|
|
547
|
+
# Also record the styling as the text body's defaults, so a cell
|
|
548
|
+
# formatted *before* it is populated keeps that styling when
|
|
549
|
+
# `cell.text = ...` replaces its paragraphs.
|
|
550
|
+
apply_body_defaults(
|
|
551
|
+
text_frame,
|
|
552
|
+
font=font,
|
|
553
|
+
size_pt=size_pt,
|
|
554
|
+
bold=bold,
|
|
555
|
+
italic=italic,
|
|
556
|
+
color=color,
|
|
557
|
+
align=align,
|
|
558
|
+
)
|
|
559
|
+
return self
|
|
560
|
+
|
|
561
|
+
@property
|
|
562
|
+
def is_merge_origin(self) -> bool:
|
|
563
|
+
"""True if this cell is the top-left grid cell in a merged cell."""
|
|
564
|
+
return self._tc.is_merge_origin
|
|
565
|
+
|
|
566
|
+
@property
|
|
567
|
+
def is_spanned(self) -> bool:
|
|
568
|
+
"""True if this cell is spanned by a merge-origin cell.
|
|
569
|
+
|
|
570
|
+
A merge-origin cell "spans" the other grid cells in its merge range, consuming their area
|
|
571
|
+
and "shadowing" the spanned grid cells.
|
|
572
|
+
|
|
573
|
+
Note this value is |False| for a merge-origin cell. A merge-origin cell spans other grid
|
|
574
|
+
cells, but is not itself a spanned cell.
|
|
575
|
+
"""
|
|
576
|
+
return self._tc.is_spanned
|
|
577
|
+
|
|
578
|
+
@property
|
|
579
|
+
def margin_left(self) -> Length:
|
|
580
|
+
"""Left margin of cells.
|
|
581
|
+
|
|
582
|
+
Read/write. If assigned |None|, the default value is used, 0.1 inches for left and right
|
|
583
|
+
margins and 0.05 inches for top and bottom.
|
|
584
|
+
"""
|
|
585
|
+
return self._tc.marL
|
|
586
|
+
|
|
587
|
+
@margin_left.setter
|
|
588
|
+
def margin_left(self, margin_left: Length | None):
|
|
589
|
+
self._validate_margin_value(margin_left)
|
|
590
|
+
self._tc.marL = margin_left
|
|
591
|
+
|
|
592
|
+
@property
|
|
593
|
+
def margin_right(self) -> Length:
|
|
594
|
+
"""Right margin of cell."""
|
|
595
|
+
return self._tc.marR
|
|
596
|
+
|
|
597
|
+
@margin_right.setter
|
|
598
|
+
def margin_right(self, margin_right: Length | None):
|
|
599
|
+
self._validate_margin_value(margin_right)
|
|
600
|
+
self._tc.marR = margin_right
|
|
601
|
+
|
|
602
|
+
@property
|
|
603
|
+
def margin_top(self) -> Length:
|
|
604
|
+
"""Top margin of cell."""
|
|
605
|
+
return self._tc.marT
|
|
606
|
+
|
|
607
|
+
@margin_top.setter
|
|
608
|
+
def margin_top(self, margin_top: Length | None):
|
|
609
|
+
self._validate_margin_value(margin_top)
|
|
610
|
+
self._tc.marT = margin_top
|
|
611
|
+
|
|
612
|
+
@property
|
|
613
|
+
def margin_bottom(self) -> Length:
|
|
614
|
+
"""Bottom margin of cell."""
|
|
615
|
+
return self._tc.marB
|
|
616
|
+
|
|
617
|
+
@margin_bottom.setter
|
|
618
|
+
def margin_bottom(self, margin_bottom: Length | None):
|
|
619
|
+
self._validate_margin_value(margin_bottom)
|
|
620
|
+
self._tc.marB = margin_bottom
|
|
621
|
+
|
|
622
|
+
def merge(self, other_cell: _Cell) -> None:
|
|
623
|
+
"""Create merged cell from this cell to `other_cell`.
|
|
624
|
+
|
|
625
|
+
This cell and `other_cell` specify opposite corners of the merged cell range. Either
|
|
626
|
+
diagonal of the cell region may be specified in either order, e.g. self=bottom-right,
|
|
627
|
+
other_cell=top-left, etc.
|
|
628
|
+
|
|
629
|
+
Raises |ValueError| if the specified range already contains merged cells anywhere within
|
|
630
|
+
its extents or if `other_cell` is not in the same table as `self`.
|
|
631
|
+
"""
|
|
632
|
+
tc_range = TcRange(self._tc, other_cell._tc)
|
|
633
|
+
|
|
634
|
+
if not tc_range.in_same_table:
|
|
635
|
+
raise ValueError("other_cell from different table")
|
|
636
|
+
if tc_range.contains_merged_cell:
|
|
637
|
+
raise ValueError("range contains one or more merged cells")
|
|
638
|
+
|
|
639
|
+
tc_range.move_content_to_origin()
|
|
640
|
+
|
|
641
|
+
row_count, col_count = tc_range.dimensions
|
|
642
|
+
|
|
643
|
+
for tc in tc_range.iter_top_row_tcs():
|
|
644
|
+
tc.rowSpan = row_count
|
|
645
|
+
for tc in tc_range.iter_left_col_tcs():
|
|
646
|
+
tc.gridSpan = col_count
|
|
647
|
+
for tc in tc_range.iter_except_left_col_tcs():
|
|
648
|
+
tc.hMerge = True
|
|
649
|
+
for tc in tc_range.iter_except_top_row_tcs():
|
|
650
|
+
tc.vMerge = True
|
|
651
|
+
|
|
652
|
+
@property
|
|
653
|
+
def span_height(self) -> int:
|
|
654
|
+
"""int count of rows spanned by this cell.
|
|
655
|
+
|
|
656
|
+
The value of this property may be misleading (often 1) on cells where `.is_merge_origin`
|
|
657
|
+
is not |True|, since only a merge-origin cell contains complete span information. This
|
|
658
|
+
property is only intended for use on cells known to be a merge origin by testing
|
|
659
|
+
`.is_merge_origin`.
|
|
660
|
+
"""
|
|
661
|
+
return self._tc.rowSpan
|
|
662
|
+
|
|
663
|
+
@property
|
|
664
|
+
def span_width(self) -> int:
|
|
665
|
+
"""int count of columns spanned by this cell.
|
|
666
|
+
|
|
667
|
+
The value of this property may be misleading (often 1) on cells where `.is_merge_origin`
|
|
668
|
+
is not |True|, since only a merge-origin cell contains complete span information. This
|
|
669
|
+
property is only intended for use on cells known to be a merge origin by testing
|
|
670
|
+
`.is_merge_origin`.
|
|
671
|
+
"""
|
|
672
|
+
return self._tc.gridSpan
|
|
673
|
+
|
|
674
|
+
def split(self) -> None:
|
|
675
|
+
"""Remove merge from this (merge-origin) cell.
|
|
676
|
+
|
|
677
|
+
The merged cell represented by this object will be "unmerged", yielding a separate
|
|
678
|
+
unmerged cell for each grid cell previously spanned by this merge.
|
|
679
|
+
|
|
680
|
+
Raises |ValueError| when this cell is not a merge-origin cell. Test with
|
|
681
|
+
`.is_merge_origin` before calling.
|
|
682
|
+
"""
|
|
683
|
+
if not self.is_merge_origin:
|
|
684
|
+
raise ValueError("not a merge-origin cell; only a merge-origin cell can be sp" "lit")
|
|
685
|
+
|
|
686
|
+
tc_range = TcRange.from_merge_origin(self._tc)
|
|
687
|
+
|
|
688
|
+
for tc in tc_range.iter_tcs():
|
|
689
|
+
tc.rowSpan = tc.gridSpan = 1
|
|
690
|
+
tc.hMerge = tc.vMerge = False
|
|
691
|
+
|
|
692
|
+
@property
|
|
693
|
+
def text(self) -> str:
|
|
694
|
+
"""Textual content of cell as a single string.
|
|
695
|
+
|
|
696
|
+
The returned string will contain a newline character (`"\\n"`) separating each paragraph
|
|
697
|
+
and a vertical-tab (`"\\v"`) character for each line break (soft carriage return) in the
|
|
698
|
+
cell's text.
|
|
699
|
+
|
|
700
|
+
Assignment to `text` replaces all text currently contained in the cell. A newline
|
|
701
|
+
character (`"\\n"`) in the assigned text causes a new paragraph to be started. A
|
|
702
|
+
vertical-tab (`"\\v"`) character in the assigned text causes a line-break (soft
|
|
703
|
+
carriage-return) to be inserted. (The vertical-tab character appears in clipboard text
|
|
704
|
+
copied from PowerPoint as its encoding of line-breaks.)
|
|
705
|
+
"""
|
|
706
|
+
return self.text_frame.text
|
|
707
|
+
|
|
708
|
+
@text.setter
|
|
709
|
+
def text(self, text: str):
|
|
710
|
+
self.text_frame.text = text
|
|
711
|
+
|
|
712
|
+
@property
|
|
713
|
+
def text_frame(self) -> TextFrame:
|
|
714
|
+
"""|TextFrame| containing the text that appears in the cell."""
|
|
715
|
+
txBody = self._tc.get_or_add_txBody()
|
|
716
|
+
return TextFrame(txBody, self)
|
|
717
|
+
|
|
718
|
+
@property
|
|
719
|
+
def width(self) -> Length:
|
|
720
|
+
"""Width of this cell in EMU (the parent column's width).
|
|
721
|
+
|
|
722
|
+
Exposed so that :meth:`TextFrame.fit_text` can measure against the
|
|
723
|
+
cell's bounds rather than the whole table when called on
|
|
724
|
+
``cell.text_frame``.
|
|
725
|
+
"""
|
|
726
|
+
tr = self._tc.getparent()
|
|
727
|
+
if tr is None:
|
|
728
|
+
return Emu(0)
|
|
729
|
+
try:
|
|
730
|
+
col_idx = list(tr).index(self._tc)
|
|
731
|
+
except ValueError:
|
|
732
|
+
return Emu(0)
|
|
733
|
+
tbl = tr.getparent()
|
|
734
|
+
if tbl is None:
|
|
735
|
+
return Emu(0)
|
|
736
|
+
try:
|
|
737
|
+
gridCol = tbl.tblGrid.gridCol_lst[col_idx]
|
|
738
|
+
except IndexError:
|
|
739
|
+
return Emu(0)
|
|
740
|
+
return Emu(int(gridCol.w))
|
|
741
|
+
|
|
742
|
+
@property
|
|
743
|
+
def height(self) -> Length:
|
|
744
|
+
"""Height of this cell in EMU (the parent row's height).
|
|
745
|
+
|
|
746
|
+
Exposed so that :meth:`TextFrame.fit_text` can measure against the
|
|
747
|
+
cell's bounds rather than the whole table when called on
|
|
748
|
+
``cell.text_frame``.
|
|
749
|
+
"""
|
|
750
|
+
tr = self._tc.getparent()
|
|
751
|
+
if tr is None:
|
|
752
|
+
return Emu(0)
|
|
753
|
+
return Emu(int(tr.h or 0))
|
|
754
|
+
|
|
755
|
+
# Friendly short-string ↔ ST_TextVerticalType (`a:tcPr@vert`) mapping.
|
|
756
|
+
# The XSD default is ``horz`` (attribute may also be absent), so reading a
|
|
757
|
+
# cell with no explicit direction returns ``"horizontal"``.
|
|
758
|
+
_TEXT_DIRECTION_TO_VERT = {
|
|
759
|
+
"horizontal": "horz",
|
|
760
|
+
"rotate90": "vert",
|
|
761
|
+
"rotate270": "vert270",
|
|
762
|
+
"stacked": "wordArtVert",
|
|
763
|
+
}
|
|
764
|
+
_VERT_TO_TEXT_DIRECTION = {
|
|
765
|
+
"horz": "horizontal",
|
|
766
|
+
"vert": "rotate90",
|
|
767
|
+
"vert270": "rotate270",
|
|
768
|
+
"wordArtVert": "stacked",
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
@property
|
|
772
|
+
def text_direction(self) -> str | None:
|
|
773
|
+
"""Text direction (rotation/stacking) of this cell.
|
|
774
|
+
|
|
775
|
+
Read/write. Maps the `<a:tcPr vert="...">` attribute to friendly short
|
|
776
|
+
strings: ``"horizontal"`` (the default), ``"rotate90"``,
|
|
777
|
+
``"rotate270"``, and ``"stacked"``. This is what rotated / matrix
|
|
778
|
+
column headers need.
|
|
779
|
+
|
|
780
|
+
Reading returns the friendly string. When the attribute is absent the
|
|
781
|
+
value ``"horizontal"`` is returned (its effective default). Assigning
|
|
782
|
+
``"horizontal"`` or |None| clears any explicit setting and restores the
|
|
783
|
+
default. A ``vert`` value not covered by the friendly mapping (e.g.
|
|
784
|
+
``eaVert``) is returned verbatim.
|
|
785
|
+
"""
|
|
786
|
+
tcPr = self._tc.tcPr
|
|
787
|
+
if tcPr is None:
|
|
788
|
+
return "horizontal"
|
|
789
|
+
vert = tcPr.vert
|
|
790
|
+
if vert is None:
|
|
791
|
+
return "horizontal"
|
|
792
|
+
return self._VERT_TO_TEXT_DIRECTION.get(vert, vert)
|
|
793
|
+
|
|
794
|
+
@text_direction.setter
|
|
795
|
+
def text_direction(self, value: str | None):
|
|
796
|
+
if value is None or value == "horizontal":
|
|
797
|
+
if self._tc.tcPr is not None:
|
|
798
|
+
self._tc.tcPr.vert = None
|
|
799
|
+
return
|
|
800
|
+
try:
|
|
801
|
+
vert = self._TEXT_DIRECTION_TO_VERT[value]
|
|
802
|
+
except KeyError:
|
|
803
|
+
raise ValueError(
|
|
804
|
+
"text_direction must be one of 'horizontal', 'rotate90', "
|
|
805
|
+
"'rotate270', 'stacked' or None, got %r" % (value,)
|
|
806
|
+
)
|
|
807
|
+
self._tc.get_or_add_tcPr().vert = vert
|
|
808
|
+
|
|
809
|
+
@property
|
|
810
|
+
def vertical_anchor(self) -> MSO_VERTICAL_ANCHOR | None:
|
|
811
|
+
"""Vertical alignment of this cell.
|
|
812
|
+
|
|
813
|
+
This value is a member of the :ref:`MsoVerticalAnchor` enumeration or |None|. A value of
|
|
814
|
+
|None| indicates the cell has no explicitly applied vertical anchor setting and its
|
|
815
|
+
effective value is inherited from its style-hierarchy ancestors.
|
|
816
|
+
|
|
817
|
+
Assigning |None| to this property causes any explicitly applied vertical anchor setting to
|
|
818
|
+
be cleared and inheritance of its effective value to be restored.
|
|
819
|
+
"""
|
|
820
|
+
return self._tc.anchor
|
|
821
|
+
|
|
822
|
+
@vertical_anchor.setter
|
|
823
|
+
def vertical_anchor(self, mso_anchor_idx: MSO_VERTICAL_ANCHOR | None):
|
|
824
|
+
self._tc.anchor = mso_anchor_idx
|
|
825
|
+
|
|
826
|
+
@staticmethod
|
|
827
|
+
def _validate_margin_value(margin_value: Length | None) -> None:
|
|
828
|
+
"""Raise ValueError if `margin_value` is not a positive integer value or |None|."""
|
|
829
|
+
if not isinstance(margin_value, int) and margin_value is not None:
|
|
830
|
+
tmpl = "margin value must be integer or None, got '%s'"
|
|
831
|
+
raise TypeError(tmpl % margin_value)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
class _Column(Subshape):
|
|
835
|
+
"""Table column"""
|
|
836
|
+
|
|
837
|
+
def __init__(self, gridCol: CT_TableCol, parent: _ColumnCollection):
|
|
838
|
+
super(_Column, self).__init__(parent)
|
|
839
|
+
self._parent = parent
|
|
840
|
+
self._gridCol = gridCol
|
|
841
|
+
self._tbl = getattr(parent, "_tbl", None)
|
|
842
|
+
|
|
843
|
+
@property
|
|
844
|
+
def width(self) -> Length:
|
|
845
|
+
"""Width of column in EMU."""
|
|
846
|
+
return self._gridCol.w
|
|
847
|
+
|
|
848
|
+
@width.setter
|
|
849
|
+
def width(self, width: Length):
|
|
850
|
+
self._gridCol.w = width
|
|
851
|
+
self._parent.notify_width_changed()
|
|
852
|
+
|
|
853
|
+
@lazyproperty
|
|
854
|
+
def borders(self) -> _LineGroup:
|
|
855
|
+
"""Convenience helper for setting borders on every cell in this column.
|
|
856
|
+
|
|
857
|
+
Mirrors :class:`_Borders` on a single cell, but applied across the
|
|
858
|
+
whole column. Examples::
|
|
859
|
+
|
|
860
|
+
col.borders.left(width=Pt(2), color=RGBColor(0, 0, 0))
|
|
861
|
+
col.borders.outer(width=Pt(1))
|
|
862
|
+
col.borders.none()
|
|
863
|
+
"""
|
|
864
|
+
if self._tbl is None:
|
|
865
|
+
return _LineGroup([])
|
|
866
|
+
return _LineGroup(_iter_column_cells(self._tbl, self._gridCol))
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
class _Row(Subshape):
|
|
870
|
+
"""Table row"""
|
|
871
|
+
|
|
872
|
+
def __init__(self, tr: CT_TableRow, parent: _RowCollection):
|
|
873
|
+
super(_Row, self).__init__(parent)
|
|
874
|
+
self._parent = parent
|
|
875
|
+
self._tr = tr
|
|
876
|
+
|
|
877
|
+
@property
|
|
878
|
+
def cells(self):
|
|
879
|
+
"""Read-only reference to collection of cells in row.
|
|
880
|
+
|
|
881
|
+
An individual cell is referenced using list notation, e.g. `cell = row.cells[0]`.
|
|
882
|
+
"""
|
|
883
|
+
return _CellCollection(self._tr, self)
|
|
884
|
+
|
|
885
|
+
@property
|
|
886
|
+
def height(self) -> Length:
|
|
887
|
+
"""Height of row in EMU."""
|
|
888
|
+
return self._tr.h
|
|
889
|
+
|
|
890
|
+
@height.setter
|
|
891
|
+
def height(self, height: Length):
|
|
892
|
+
self._tr.h = height
|
|
893
|
+
self._parent.notify_height_changed()
|
|
894
|
+
|
|
895
|
+
@lazyproperty
|
|
896
|
+
def borders(self) -> _LineGroup:
|
|
897
|
+
"""Convenience helper for setting borders on every cell in this row.
|
|
898
|
+
|
|
899
|
+
Mirrors :class:`_Borders` on a single cell, but applied across the
|
|
900
|
+
whole row. Examples::
|
|
901
|
+
|
|
902
|
+
row.borders.bottom(width=Pt(2), color=RGBColor(0, 0, 0))
|
|
903
|
+
row.borders.outer(width=Pt(1))
|
|
904
|
+
row.borders.none()
|
|
905
|
+
"""
|
|
906
|
+
return _LineGroup(list(self._tr.tc_lst))
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _iter_column_cells(tbl: CT_Table, gridCol):
|
|
910
|
+
"""Return the list of ``CT_TableCell`` elements at this column's grid index."""
|
|
911
|
+
grid = list(tbl.tblGrid.gridCol_lst)
|
|
912
|
+
try:
|
|
913
|
+
col_idx = grid.index(gridCol)
|
|
914
|
+
except ValueError:
|
|
915
|
+
return []
|
|
916
|
+
cells = []
|
|
917
|
+
for tr in tbl.tr_lst:
|
|
918
|
+
tcs = tr.tc_lst
|
|
919
|
+
if col_idx < len(tcs):
|
|
920
|
+
cells.append(tcs[col_idx])
|
|
921
|
+
return cells
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
class _CellCollection(Subshape):
|
|
925
|
+
"""Horizontal sequence of row cells"""
|
|
926
|
+
|
|
927
|
+
def __init__(self, tr: CT_TableRow, parent: _Row):
|
|
928
|
+
super(_CellCollection, self).__init__(parent)
|
|
929
|
+
self._parent = parent
|
|
930
|
+
self._tr = tr
|
|
931
|
+
|
|
932
|
+
def __getitem__(self, idx: int) -> _Cell:
|
|
933
|
+
"""Provides indexed access, (e.g. 'cells[0]')."""
|
|
934
|
+
if idx < 0 or idx >= len(self._tr.tc_lst):
|
|
935
|
+
msg = "cell index [%d] out of range" % idx
|
|
936
|
+
raise IndexError(msg)
|
|
937
|
+
return _Cell(self._tr.tc_lst[idx], self)
|
|
938
|
+
|
|
939
|
+
def __iter__(self) -> Iterator[_Cell]:
|
|
940
|
+
"""Provides iterability."""
|
|
941
|
+
return (_Cell(tc, self) for tc in self._tr.tc_lst)
|
|
942
|
+
|
|
943
|
+
def __len__(self) -> int:
|
|
944
|
+
"""Supports len() function (e.g. 'len(cells) == 1')."""
|
|
945
|
+
return len(self._tr.tc_lst)
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
class _ColumnCollection(Subshape):
|
|
949
|
+
"""Sequence of table columns."""
|
|
950
|
+
|
|
951
|
+
def __init__(self, tbl: CT_Table, parent: Table):
|
|
952
|
+
super(_ColumnCollection, self).__init__(parent)
|
|
953
|
+
self._parent = parent
|
|
954
|
+
self._tbl = tbl
|
|
955
|
+
|
|
956
|
+
def __getitem__(self, idx: int):
|
|
957
|
+
"""Provides indexed access, (e.g. 'columns[0]')."""
|
|
958
|
+
if idx < 0 or idx >= len(self._tbl.tblGrid.gridCol_lst):
|
|
959
|
+
msg = "column index [%d] out of range" % idx
|
|
960
|
+
raise IndexError(msg)
|
|
961
|
+
return _Column(self._tbl.tblGrid.gridCol_lst[idx], self)
|
|
962
|
+
|
|
963
|
+
def __len__(self):
|
|
964
|
+
"""Supports len() function (e.g. 'len(columns) == 1')."""
|
|
965
|
+
return len(self._tbl.tblGrid.gridCol_lst)
|
|
966
|
+
|
|
967
|
+
def notify_width_changed(self):
|
|
968
|
+
"""Called by a column when its width changes. Pass along to parent."""
|
|
969
|
+
self._parent.notify_width_changed()
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
class _RowCollection(Subshape):
|
|
973
|
+
"""Sequence of table rows"""
|
|
974
|
+
|
|
975
|
+
def __init__(self, tbl: CT_Table, parent: Table):
|
|
976
|
+
super(_RowCollection, self).__init__(parent)
|
|
977
|
+
self._parent = parent
|
|
978
|
+
self._tbl = tbl
|
|
979
|
+
|
|
980
|
+
def __getitem__(self, idx: int) -> _Row:
|
|
981
|
+
"""Provides indexed access, (e.g. 'rows[0]')."""
|
|
982
|
+
if idx < 0 or idx >= len(self):
|
|
983
|
+
msg = "row index [%d] out of range" % idx
|
|
984
|
+
raise IndexError(msg)
|
|
985
|
+
return _Row(self._tbl.tr_lst[idx], self)
|
|
986
|
+
|
|
987
|
+
def __len__(self):
|
|
988
|
+
"""Supports len() function (e.g. 'len(rows) == 1')."""
|
|
989
|
+
return len(self._tbl.tr_lst)
|
|
990
|
+
|
|
991
|
+
def notify_height_changed(self):
|
|
992
|
+
"""Called by a row when its height changes. Pass along to parent."""
|
|
993
|
+
self._parent.notify_height_changed()
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
class _BorderEdge(object):
|
|
997
|
+
"""Adapter exposing the |LineFormat| parent contract for one cell-border edge.
|
|
998
|
+
|
|
999
|
+
A cell border (`a:lnL`, `a:lnR`, etc.) is itself an `<a:ln>`-shaped element
|
|
1000
|
+
living inside `<a:tcPr>`. |LineFormat| expects its parent to expose
|
|
1001
|
+
`get_or_add_ln()` and `ln`; this adapter routes those calls to the matching
|
|
1002
|
+
edge-specific accessor on `a:tcPr`, so a single |LineFormat| implementation
|
|
1003
|
+
serves shape lines and table borders alike.
|
|
1004
|
+
"""
|
|
1005
|
+
|
|
1006
|
+
def __init__(self, tc: CT_TableCell, edge: str):
|
|
1007
|
+
super(_BorderEdge, self).__init__()
|
|
1008
|
+
self._tc = tc
|
|
1009
|
+
self._edge = edge
|
|
1010
|
+
|
|
1011
|
+
def get_or_add_ln(self) -> CT_LineProperties:
|
|
1012
|
+
tcPr = self._tc.get_or_add_tcPr()
|
|
1013
|
+
return getattr(tcPr, "get_or_add_%s" % self._edge)()
|
|
1014
|
+
|
|
1015
|
+
@property
|
|
1016
|
+
def ln(self) -> CT_LineProperties | None:
|
|
1017
|
+
tcPr = self._tc.tcPr
|
|
1018
|
+
if tcPr is None:
|
|
1019
|
+
return None
|
|
1020
|
+
return getattr(tcPr, self._edge)
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
class _Borders(object):
|
|
1024
|
+
"""Per-edge line formatting for a table cell.
|
|
1025
|
+
|
|
1026
|
+
Returned by `cell.borders`. Each edge is a |LineFormat|; assignments such
|
|
1027
|
+
as `cell.borders.left.color.rgb = RGBColor(...)` materialize the border
|
|
1028
|
+
XML on demand. Convenience helpers act on multiple edges in one call.
|
|
1029
|
+
|
|
1030
|
+
Edge accessors (`left`, `right`, etc.) construct a fresh |LineFormat| on
|
|
1031
|
+
every access rather than caching one. This keeps the common
|
|
1032
|
+
set → ``none()`` → set-again flow correct: after ``none()`` removes the
|
|
1033
|
+
underlying ``<a:ln*>`` element, the next access returns a |LineFormat|
|
|
1034
|
+
that re-creates the element on first write, instead of writing through
|
|
1035
|
+
a stale reference to a detached element.
|
|
1036
|
+
"""
|
|
1037
|
+
|
|
1038
|
+
def __init__(self, tc: CT_TableCell):
|
|
1039
|
+
super(_Borders, self).__init__()
|
|
1040
|
+
self._tc = tc
|
|
1041
|
+
|
|
1042
|
+
@property
|
|
1043
|
+
def left(self) -> LineFormat:
|
|
1044
|
+
"""|LineFormat| for the left edge (`a:lnL`)."""
|
|
1045
|
+
return LineFormat(_BorderEdge(self._tc, "lnL"))
|
|
1046
|
+
|
|
1047
|
+
@property
|
|
1048
|
+
def right(self) -> LineFormat:
|
|
1049
|
+
"""|LineFormat| for the right edge (`a:lnR`)."""
|
|
1050
|
+
return LineFormat(_BorderEdge(self._tc, "lnR"))
|
|
1051
|
+
|
|
1052
|
+
@property
|
|
1053
|
+
def top(self) -> LineFormat:
|
|
1054
|
+
"""|LineFormat| for the top edge (`a:lnT`)."""
|
|
1055
|
+
return LineFormat(_BorderEdge(self._tc, "lnT"))
|
|
1056
|
+
|
|
1057
|
+
@property
|
|
1058
|
+
def bottom(self) -> LineFormat:
|
|
1059
|
+
"""|LineFormat| for the bottom edge (`a:lnB`)."""
|
|
1060
|
+
return LineFormat(_BorderEdge(self._tc, "lnB"))
|
|
1061
|
+
|
|
1062
|
+
@property
|
|
1063
|
+
def diagonal_down(self) -> LineFormat:
|
|
1064
|
+
"""|LineFormat| for the top-left-to-bottom-right diagonal (`a:lnTlToBr`)."""
|
|
1065
|
+
return LineFormat(_BorderEdge(self._tc, "lnTlToBr"))
|
|
1066
|
+
|
|
1067
|
+
@property
|
|
1068
|
+
def diagonal_up(self) -> LineFormat:
|
|
1069
|
+
"""|LineFormat| for the bottom-left-to-top-right diagonal (`a:lnBlToTr`)."""
|
|
1070
|
+
return LineFormat(_BorderEdge(self._tc, "lnBlToTr"))
|
|
1071
|
+
|
|
1072
|
+
def all(self, width: Length | None = None, color: _ColorLike | None = None) -> None:
|
|
1073
|
+
"""Apply `width` and/or `color` to every border edge (4 sides + 2 diagonals).
|
|
1074
|
+
|
|
1075
|
+
`color` accepts anything the library accepts elsewhere — a hex string
|
|
1076
|
+
(`"1F4E79"`), an `(r, g, b)` 3-tuple, or an |RGBColor|. Either argument
|
|
1077
|
+
may be |None| to leave that aspect alone.
|
|
1078
|
+
"""
|
|
1079
|
+
for edge in (self.left, self.right, self.top, self.bottom,
|
|
1080
|
+
self.diagonal_down, self.diagonal_up):
|
|
1081
|
+
self._apply(edge, width, color)
|
|
1082
|
+
|
|
1083
|
+
def outer(self, width: Length | None = None, color: _ColorLike | None = None) -> None:
|
|
1084
|
+
"""Apply `width` and/or `color` to the four outer edges (left/right/top/bottom).
|
|
1085
|
+
|
|
1086
|
+
`color` accepts a hex string, an `(r, g, b)` 3-tuple, or an |RGBColor|.
|
|
1087
|
+
"""
|
|
1088
|
+
for edge in (self.left, self.right, self.top, self.bottom):
|
|
1089
|
+
self._apply(edge, width, color)
|
|
1090
|
+
|
|
1091
|
+
def none(self) -> None:
|
|
1092
|
+
"""Remove all border edge elements from the cell.
|
|
1093
|
+
|
|
1094
|
+
Restores theme/style inheritance for every edge. Diagonal borders are
|
|
1095
|
+
also cleared. Note: |LineFormat| objects retrieved before this call
|
|
1096
|
+
cache an internal reference to the now-detached ``<a:ln*>`` element
|
|
1097
|
+
and should not be reused; re-access via ``cell.borders.left`` (etc.)
|
|
1098
|
+
to get a fresh |LineFormat| over a re-created element.
|
|
1099
|
+
"""
|
|
1100
|
+
tcPr = self._tc.tcPr
|
|
1101
|
+
if tcPr is None:
|
|
1102
|
+
return
|
|
1103
|
+
tcPr._remove_lnL()
|
|
1104
|
+
tcPr._remove_lnR()
|
|
1105
|
+
tcPr._remove_lnT()
|
|
1106
|
+
tcPr._remove_lnB()
|
|
1107
|
+
tcPr._remove_lnTlToBr()
|
|
1108
|
+
tcPr._remove_lnBlToTr()
|
|
1109
|
+
|
|
1110
|
+
@staticmethod
|
|
1111
|
+
def _apply(line: LineFormat, width: Length | None, color: _ColorLike | None) -> None:
|
|
1112
|
+
if width is not None:
|
|
1113
|
+
line.width = width
|
|
1114
|
+
if color is not None:
|
|
1115
|
+
# LineFormat.color.rgb coerces hex strings, 3-tuples, and RGBColor;
|
|
1116
|
+
# pass through rather than pre-wrapping (RGBColor(*"1F4E79") would
|
|
1117
|
+
# splat the hex string into 6 positional args and raise).
|
|
1118
|
+
line.color.rgb = color
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
class _LineGroup(object):
|
|
1122
|
+
"""Apply border edges across a group of cells (a row or a column).
|
|
1123
|
+
|
|
1124
|
+
Returned by ``row.borders`` and ``col.borders``. Each edge accessor
|
|
1125
|
+
is callable; calling it with ``width`` and/or ``color`` applies those
|
|
1126
|
+
settings to that edge of every cell in the group.
|
|
1127
|
+
"""
|
|
1128
|
+
|
|
1129
|
+
def __init__(self, tcs):
|
|
1130
|
+
self._tcs = tcs
|
|
1131
|
+
|
|
1132
|
+
def _apply_edge(
|
|
1133
|
+
self,
|
|
1134
|
+
edge: str,
|
|
1135
|
+
width: Length | None,
|
|
1136
|
+
color: _ColorLike | None,
|
|
1137
|
+
) -> None:
|
|
1138
|
+
for tc in self._tcs:
|
|
1139
|
+
line = LineFormat(_BorderEdge(tc, edge))
|
|
1140
|
+
if width is not None:
|
|
1141
|
+
line.width = width
|
|
1142
|
+
if color is not None:
|
|
1143
|
+
# color.rgb coerces hex strings, 3-tuples, and RGBColor — pass
|
|
1144
|
+
# through rather than pre-wrapping (RGBColor(*"1F4E79") raises).
|
|
1145
|
+
line.color.rgb = color
|
|
1146
|
+
|
|
1147
|
+
def left(self, width: Length | None = None, color=None) -> None:
|
|
1148
|
+
"""Apply *width* and/or *color* to the left edge of every cell."""
|
|
1149
|
+
self._apply_edge("lnL", width, color)
|
|
1150
|
+
|
|
1151
|
+
def right(self, width: Length | None = None, color=None) -> None:
|
|
1152
|
+
"""Apply *width* and/or *color* to the right edge of every cell."""
|
|
1153
|
+
self._apply_edge("lnR", width, color)
|
|
1154
|
+
|
|
1155
|
+
def top(self, width: Length | None = None, color=None) -> None:
|
|
1156
|
+
"""Apply *width* and/or *color* to the top edge of every cell."""
|
|
1157
|
+
self._apply_edge("lnT", width, color)
|
|
1158
|
+
|
|
1159
|
+
def bottom(self, width: Length | None = None, color=None) -> None:
|
|
1160
|
+
"""Apply *width* and/or *color* to the bottom edge of every cell."""
|
|
1161
|
+
self._apply_edge("lnB", width, color)
|
|
1162
|
+
|
|
1163
|
+
def all(self, width: Length | None = None, color=None) -> None:
|
|
1164
|
+
"""Apply *width* and/or *color* to all four outer edges of every cell."""
|
|
1165
|
+
for edge in ("lnL", "lnR", "lnT", "lnB"):
|
|
1166
|
+
self._apply_edge(edge, width, color)
|
|
1167
|
+
|
|
1168
|
+
outer = all # alias for parity with ``cell.borders.outer``
|
|
1169
|
+
|
|
1170
|
+
def none(self) -> None:
|
|
1171
|
+
"""Clear every border edge from every cell in the group."""
|
|
1172
|
+
for tc in self._tcs:
|
|
1173
|
+
tcPr = tc.tcPr
|
|
1174
|
+
if tcPr is None:
|
|
1175
|
+
continue
|
|
1176
|
+
tcPr._remove_lnL()
|
|
1177
|
+
tcPr._remove_lnR()
|
|
1178
|
+
tcPr._remove_lnT()
|
|
1179
|
+
tcPr._remove_lnB()
|
|
1180
|
+
tcPr._remove_lnTlToBr()
|
|
1181
|
+
tcPr._remove_lnBlToTr()
|