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.
Files changed (175) hide show
  1. pptx2/__init__.py +152 -0
  2. pptx2/_color.py +75 -0
  3. pptx2/_slide_importer.py +597 -0
  4. pptx2/_svg.py +155 -0
  5. pptx2/_template_applier.py +292 -0
  6. pptx2/_textstyle.py +187 -0
  7. pptx2/accessibility.py +365 -0
  8. pptx2/action.py +270 -0
  9. pptx2/animation.py +2237 -0
  10. pptx2/api.py +49 -0
  11. pptx2/audit.py +258 -0
  12. pptx2/chart/__init__.py +0 -0
  13. pptx2/chart/analytics.py +381 -0
  14. pptx2/chart/axis.py +543 -0
  15. pptx2/chart/category.py +200 -0
  16. pptx2/chart/chart.py +670 -0
  17. pptx2/chart/data.py +864 -0
  18. pptx2/chart/datalabel.py +406 -0
  19. pptx2/chart/legend.py +86 -0
  20. pptx2/chart/marker.py +70 -0
  21. pptx2/chart/palettes.py +129 -0
  22. pptx2/chart/plot.py +462 -0
  23. pptx2/chart/point.py +101 -0
  24. pptx2/chart/quick_layouts.py +325 -0
  25. pptx2/chart/series.py +334 -0
  26. pptx2/chart/xlsx.py +272 -0
  27. pptx2/chart/xmlwriter.py +1845 -0
  28. pptx2/compose/__init__.py +28 -0
  29. pptx2/compose/from_spec.py +1094 -0
  30. pptx2/design/__init__.py +8 -0
  31. pptx2/design/components.py +607 -0
  32. pptx2/design/figures.py +389 -0
  33. pptx2/design/layout.py +370 -0
  34. pptx2/design/recipes.py +1967 -0
  35. pptx2/design/style.py +209 -0
  36. pptx2/design/tokens.py +915 -0
  37. pptx2/diagrams.py +754 -0
  38. pptx2/dml/__init__.py +0 -0
  39. pptx2/dml/chtfmt.py +40 -0
  40. pptx2/dml/color.py +496 -0
  41. pptx2/dml/effect.py +909 -0
  42. pptx2/dml/fill.py +691 -0
  43. pptx2/dml/line.py +287 -0
  44. pptx2/dml/picture.py +212 -0
  45. pptx2/dml/three_d.py +381 -0
  46. pptx2/enum/__init__.py +0 -0
  47. pptx2/enum/action.py +71 -0
  48. pptx2/enum/animation.py +31 -0
  49. pptx2/enum/base.py +218 -0
  50. pptx2/enum/chart.py +574 -0
  51. pptx2/enum/dml.py +740 -0
  52. pptx2/enum/lang.py +685 -0
  53. pptx2/enum/presentation.py +133 -0
  54. pptx2/enum/shapes.py +1029 -0
  55. pptx2/enum/text.py +230 -0
  56. pptx2/exc.py +42 -0
  57. pptx2/formats.py +139 -0
  58. pptx2/geometry.py +420 -0
  59. pptx2/inherit.py +109 -0
  60. pptx2/lint.py +2256 -0
  61. pptx2/math.py +177 -0
  62. pptx2/media.py +197 -0
  63. pptx2/opc/__init__.py +0 -0
  64. pptx2/opc/constants.py +332 -0
  65. pptx2/opc/oxml.py +188 -0
  66. pptx2/opc/package.py +762 -0
  67. pptx2/opc/packuri.py +109 -0
  68. pptx2/opc/serialized.py +296 -0
  69. pptx2/opc/shared.py +20 -0
  70. pptx2/opc/spec.py +45 -0
  71. pptx2/oxml/__init__.py +555 -0
  72. pptx2/oxml/action.py +53 -0
  73. pptx2/oxml/chart/__init__.py +0 -0
  74. pptx2/oxml/chart/axis.py +337 -0
  75. pptx2/oxml/chart/chart.py +481 -0
  76. pptx2/oxml/chart/datalabel.py +253 -0
  77. pptx2/oxml/chart/legend.py +72 -0
  78. pptx2/oxml/chart/marker.py +61 -0
  79. pptx2/oxml/chart/plot.py +365 -0
  80. pptx2/oxml/chart/series.py +425 -0
  81. pptx2/oxml/chart/shared.py +220 -0
  82. pptx2/oxml/coreprops.py +288 -0
  83. pptx2/oxml/dml/__init__.py +0 -0
  84. pptx2/oxml/dml/color.py +135 -0
  85. pptx2/oxml/dml/effect.py +213 -0
  86. pptx2/oxml/dml/fill.py +316 -0
  87. pptx2/oxml/dml/line.py +12 -0
  88. pptx2/oxml/dml/three_d.py +110 -0
  89. pptx2/oxml/ns.py +135 -0
  90. pptx2/oxml/presentation.py +313 -0
  91. pptx2/oxml/shapes/__init__.py +19 -0
  92. pptx2/oxml/shapes/autoshape.py +467 -0
  93. pptx2/oxml/shapes/connector.py +107 -0
  94. pptx2/oxml/shapes/graphfrm.py +347 -0
  95. pptx2/oxml/shapes/groupshape.py +329 -0
  96. pptx2/oxml/shapes/picture.py +270 -0
  97. pptx2/oxml/shapes/shared.py +577 -0
  98. pptx2/oxml/simpletypes.py +1027 -0
  99. pptx2/oxml/slide.py +563 -0
  100. pptx2/oxml/table.py +650 -0
  101. pptx2/oxml/text.py +815 -0
  102. pptx2/oxml/theme.py +36 -0
  103. pptx2/oxml/xmlchemy.py +717 -0
  104. pptx2/package.py +222 -0
  105. pptx2/parts/__init__.py +0 -0
  106. pptx2/parts/chart.py +95 -0
  107. pptx2/parts/coreprops.py +167 -0
  108. pptx2/parts/diagram.py +37 -0
  109. pptx2/parts/embeddedpackage.py +93 -0
  110. pptx2/parts/image.py +275 -0
  111. pptx2/parts/media.py +37 -0
  112. pptx2/parts/presentation.py +136 -0
  113. pptx2/parts/slide.py +371 -0
  114. pptx2/presentation.py +408 -0
  115. pptx2/py.typed +0 -0
  116. pptx2/render.py +586 -0
  117. pptx2/section.py +272 -0
  118. pptx2/shapes/__init__.py +26 -0
  119. pptx2/shapes/autoshape.py +442 -0
  120. pptx2/shapes/base.py +1078 -0
  121. pptx2/shapes/connector.py +297 -0
  122. pptx2/shapes/freeform.py +337 -0
  123. pptx2/shapes/graphfrm.py +316 -0
  124. pptx2/shapes/group.py +264 -0
  125. pptx2/shapes/picture.py +422 -0
  126. pptx2/shapes/placeholder.py +468 -0
  127. pptx2/shapes/shapetree.py +2027 -0
  128. pptx2/shared.py +82 -0
  129. pptx2/skill/SKILL.md +450 -0
  130. pptx2/skill/__init__.py +78 -0
  131. pptx2/skill/__main__.py +64 -0
  132. pptx2/skill/references/animations.md +189 -0
  133. pptx2/skill/references/basics.md +421 -0
  134. pptx2/skill/references/charts.md +254 -0
  135. pptx2/skill/references/compose.md +234 -0
  136. pptx2/skill/references/design.md +366 -0
  137. pptx2/skill/references/effects.md +249 -0
  138. pptx2/skill/references/end-to-end-deck.md +231 -0
  139. pptx2/skill/references/geometry-and-arrows.md +334 -0
  140. pptx2/skill/references/lint.md +275 -0
  141. pptx2/skill/references/math.md +86 -0
  142. pptx2/skill/references/picture-effects.md +129 -0
  143. pptx2/skill/references/render.md +151 -0
  144. pptx2/skill/references/smart-art.md +75 -0
  145. pptx2/skill/references/space-aware-authoring.md +249 -0
  146. pptx2/skill/references/tables.md +244 -0
  147. pptx2/skill/references/theme.md +127 -0
  148. pptx2/skill/references/three-d.md +109 -0
  149. pptx2/skill/references/transitions.md +100 -0
  150. pptx2/slide.py +1244 -0
  151. pptx2/smart_art.py +220 -0
  152. pptx2/spec.py +633 -0
  153. pptx2/table.py +1181 -0
  154. pptx2/table_styles.py +184 -0
  155. pptx2/templates/default.pptx +0 -0
  156. pptx2/templates/docx-icon.emf +0 -0
  157. pptx2/templates/generic-icon.emf +0 -0
  158. pptx2/templates/notes.xml +23 -0
  159. pptx2/templates/notesMaster.xml +352 -0
  160. pptx2/templates/pptx-icon.emf +0 -0
  161. pptx2/templates/theme.xml +321 -0
  162. pptx2/templates/xlsx-icon.emf +0 -0
  163. pptx2/text/__init__.py +0 -0
  164. pptx2/text/fonts.py +482 -0
  165. pptx2/text/layout.py +374 -0
  166. pptx2/text/text.py +1272 -0
  167. pptx2/theme.py +721 -0
  168. pptx2/types.py +36 -0
  169. pptx2/util.py +263 -0
  170. python_pptx2-2.13.0.dist-info/METADATA +351 -0
  171. python_pptx2-2.13.0.dist-info/RECORD +175 -0
  172. python_pptx2-2.13.0.dist-info/WHEEL +5 -0
  173. python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
  174. python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
  175. python_pptx2-2.13.0.dist-info/top_level.txt +1 -0
pptx2/text/text.py ADDED
@@ -0,0 +1,1272 @@
1
+ """Text-related objects such as TextFrame and Paragraph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from typing import TYPE_CHECKING, Iterator, cast
7
+
8
+ from pptx2.dml.color import _LazyColorFormat
9
+ from pptx2.dml.effect import GlowFormat, ShadowFormat
10
+ from pptx2.dml.fill import FillFormat
11
+ from pptx2.dml.line import LineFormat
12
+ from pptx2.enum.lang import MSO_LANGUAGE_ID
13
+ from pptx2.enum.text import MSO_AUTO_SIZE, MSO_UNDERLINE, MSO_VERTICAL_ANCHOR
14
+ from pptx2.exc import FontMetricsWarning
15
+ from pptx2.opc.constants import RELATIONSHIP_TYPE as RT
16
+ from pptx2.oxml.simpletypes import ST_TextWrappingType
17
+ from pptx2.shapes import Subshape
18
+ from pptx2.text.fonts import find_font_file
19
+ from pptx2.text.layout import TextFitter
20
+ from pptx2.util import Centipoints, Emu, Length, Pt, lazyproperty
21
+
22
+ if TYPE_CHECKING:
23
+ from pptx2.dml.color import ColorFormat
24
+ from pptx2.enum.text import (
25
+ MSO_TEXT_UNDERLINE_TYPE,
26
+ MSO_VERTICAL_ANCHOR,
27
+ PP_PARAGRAPH_ALIGNMENT,
28
+ )
29
+ from pptx2.oxml.action import CT_Hyperlink
30
+ from pptx2.oxml.text import (
31
+ CT_RegularTextRun,
32
+ CT_TextBody,
33
+ CT_TextCharacterProperties,
34
+ CT_TextParagraph,
35
+ CT_TextParagraphProperties,
36
+ CT_TextTabStop,
37
+ )
38
+ from pptx2.parts.slide import SlidePart
39
+ from pptx2.slide import Slide
40
+ from pptx2.types import ProvidesExtents, ProvidesPart
41
+
42
+
43
+ #: Family `TextFrame.fit_text` measures with when the caller names none. An
44
+ #: omitted `font_family` is told apart from an explicit one by the `None`
45
+ #: default, so a fallback to Pillow's metrics warns whenever a face was actually
46
+ #: asked for — this one included. See `TextFrame.fit_text`.
47
+ _DEFAULT_FIT_FAMILY = "Calibri"
48
+
49
+
50
+ class TextFrame(Subshape):
51
+ """The part of a shape that contains its text.
52
+
53
+ Not all shapes have a text frame. Corresponds to the `p:txBody` element that can
54
+ appear as a child element of `p:sp`. Not intended to be constructed directly.
55
+ """
56
+
57
+ def __init__(self, txBody: CT_TextBody, parent: ProvidesPart):
58
+ super(TextFrame, self).__init__(parent)
59
+ self._element = self._txBody = txBody
60
+ self._parent = parent
61
+
62
+ def add_paragraph(self):
63
+ """
64
+ Return new |_Paragraph| instance appended to the sequence of
65
+ paragraphs contained in this text frame.
66
+ """
67
+ p = self._txBody.add_p()
68
+ return _Paragraph(p, self)
69
+
70
+ @property
71
+ def auto_size(self) -> MSO_AUTO_SIZE | None:
72
+ """Resizing strategy used to fit text within this shape.
73
+
74
+ Determins the type of automatic resizing used to fit the text of this shape within its
75
+ bounding box when the text would otherwise extend beyond the shape boundaries. May be
76
+ |None|, `MSO_AUTO_SIZE.NONE`, `MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT`, or
77
+ `MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE`.
78
+ """
79
+ return self._bodyPr.autofit
80
+
81
+ @auto_size.setter
82
+ def auto_size(self, value: MSO_AUTO_SIZE | None):
83
+ self._bodyPr.autofit = value
84
+
85
+ def clear(self):
86
+ """Remove all paragraphs except one empty one."""
87
+ for p in self._txBody.p_lst[1:]:
88
+ self._txBody.remove(p)
89
+ p = self.paragraphs[0]
90
+ p.clear()
91
+
92
+ def fit_text(
93
+ self,
94
+ font_family: str | None = None,
95
+ max_size: int = 18,
96
+ bold: bool = False,
97
+ italic: bool = False,
98
+ font_file: str | None = None,
99
+ strict: bool = False,
100
+ ) -> int | None:
101
+ """Fit text-frame text entirely within bounds of its shape.
102
+
103
+ Make the text in this text frame fit entirely within the bounds of its shape by setting
104
+ word wrap on and applying the "best-fit" font size to all the text it contains. Returns
105
+ the point size applied (|None| when the frame is empty and nothing was done).
106
+
107
+ :attr:`TextFrame.auto_size` is set to :attr:`MSO_AUTO_SIZE.NONE`. The font size will not
108
+ be set larger than `max_size` points. If the path to a matching TrueType font is provided
109
+ as `font_file`, that font file will be used for the font metrics. If `font_file` is |None|,
110
+ best efforts are made to locate a font file with matching `font_family`, `bold`, and
111
+ `italic` installed on the current system (usually succeeds if the font is installed).
112
+
113
+ `font_family` defaults to ``"Calibri"`` when omitted.
114
+
115
+ **The fit is only as good as the metrics it measures against.** When neither `font_file`
116
+ nor an installed `font_family` can be found, measurement falls back to Pillow's bundled
117
+ default font: the result is a plausible estimate, not the guarantee this method usually
118
+ provides, and a display face can still overflow. *Naming* a family that isn't installed —
119
+ the brand-font-in-a-container case — emits a
120
+ :class:`~pptx2.exc.FontMetricsWarning`. Omitting the argument does not, since no
121
+ particular face was asked for; passing ``"Calibri"`` explicitly does, because that is a
122
+ request like any other. ``strict=True`` turns *any* fallback into a |ValueError|, which
123
+ is what a build that must be exact should do::
124
+
125
+ # bundle the real metrics with the deck build
126
+ tf.fit_text("Instrument Serif", max_size=44, font_file="fonts/InstrumentSerif.ttf")
127
+
128
+ # or fail the build rather than ship a guess
129
+ tf.fit_text("Inter", max_size=18, strict=True)
130
+
131
+ :func:`pptx2.text.fonts.font_is_installed` answers the same question up front,
132
+ without measuring anything.
133
+ """
134
+ # ---no-op when empty as fit behavior not defined for that case---
135
+ if self.text == "":
136
+ return None # pragma: no cover
137
+
138
+ family = _DEFAULT_FIT_FAMILY if font_family is None else font_family
139
+ font_size = self._best_fit_font_size(
140
+ family,
141
+ max_size,
142
+ bold,
143
+ italic,
144
+ font_file,
145
+ strict,
146
+ # Only a caller who *named* a face is owed a warning; an omitted
147
+ # argument expresses no requirement to break.
148
+ warn_on_fallback=font_family is not None,
149
+ )
150
+ self._apply_fit(family, font_size, bold, italic)
151
+ return font_size
152
+
153
+ @property
154
+ def margin_bottom(self) -> Length:
155
+ """|Length| value representing the inset of text from the bottom text frame border.
156
+
157
+ :meth:`pptx2.util.Inches` provides a convenient way of setting the value, e.g.
158
+ `text_frame.margin_bottom = Inches(0.05)`.
159
+ """
160
+ return self._bodyPr.bIns
161
+
162
+ @margin_bottom.setter
163
+ def margin_bottom(self, emu: Length):
164
+ self._bodyPr.bIns = emu
165
+
166
+ @property
167
+ def margin_left(self) -> Length:
168
+ """Inset of text from left text frame border as |Length| value."""
169
+ return self._bodyPr.lIns
170
+
171
+ @margin_left.setter
172
+ def margin_left(self, emu: Length):
173
+ self._bodyPr.lIns = emu
174
+
175
+ @property
176
+ def margin_right(self) -> Length:
177
+ """Inset of text from right text frame border as |Length| value."""
178
+ return self._bodyPr.rIns
179
+
180
+ @margin_right.setter
181
+ def margin_right(self, emu: Length):
182
+ self._bodyPr.rIns = emu
183
+
184
+ @property
185
+ def margin_top(self) -> Length:
186
+ """Inset of text from top text frame border as |Length| value."""
187
+ return self._bodyPr.tIns
188
+
189
+ @margin_top.setter
190
+ def margin_top(self, emu: Length):
191
+ self._bodyPr.tIns = emu
192
+
193
+ @property
194
+ def paragraphs(self) -> tuple[_Paragraph, ...]:
195
+ """Sequence of paragraphs in this text frame.
196
+
197
+ A text frame always contains at least one paragraph.
198
+ """
199
+ return tuple([_Paragraph(p, self) for p in self._txBody.p_lst])
200
+
201
+ @property
202
+ def text(self) -> str:
203
+ """All text in this text-frame as a single string.
204
+
205
+ Read/write. The return value contains all text in this text-frame. A line-feed character
206
+ (`"\\n"`) separates the text for each paragraph. A vertical-tab character (`"\\v"`) appears
207
+ for each line break (aka. soft carriage-return) encountered.
208
+
209
+ The vertical-tab character is how PowerPoint represents a soft carriage return in clipboard
210
+ text, which is why that encoding was chosen.
211
+
212
+ Assignment replaces all text in the text frame. A new paragraph is added for each line-feed
213
+ character (`"\\n"`) encountered. A line-break (soft carriage-return) is inserted for each
214
+ vertical-tab character (`"\\v"`) encountered.
215
+
216
+ Any control character other than newline, tab, or vertical-tab are escaped as plain-text
217
+ like "_x001B_" (for ESC (ASCII 32) in this example).
218
+ """
219
+ return "\n".join(paragraph.text for paragraph in self.paragraphs)
220
+
221
+ @text.setter
222
+ def text(self, text: str):
223
+ txBody = self._txBody
224
+ txBody.clear_content()
225
+ for p_text in text.split("\n"):
226
+ p = txBody.add_p()
227
+ p.append_text(p_text)
228
+
229
+ def set_paragraph_defaults(
230
+ self,
231
+ *,
232
+ font_name: str | None = None,
233
+ size: Length | None = None,
234
+ bold: bool | None = None,
235
+ italic: bool | None = None,
236
+ color: object | None = None,
237
+ ) -> None:
238
+ """Apply default font properties to every paragraph/run in this text frame.
239
+
240
+ Sets the supplied properties on each existing paragraph (and on
241
+ every run inside each paragraph) **only where the property is
242
+ currently unset** — explicit per-run overrides are preserved
243
+ verbatim. Pass only the keyword arguments you want to enforce.
244
+
245
+ Branded decks repeatedly want to set the same six lines
246
+ (``font.name``, ``font.size``, ``font.bold``, ``font.color.rgb``)
247
+ on every paragraph in a card body. This wrapper collapses that
248
+ ritual into one call::
249
+
250
+ from pptx2.util import Pt
251
+
252
+ tf.set_paragraph_defaults(
253
+ font_name="Inter",
254
+ size=Pt(14),
255
+ color="#222222",
256
+ )
257
+
258
+ ``color`` accepts any "color-like" value supported by
259
+ :func:`pptx2._color.coerce_color` (``RGBColor``,
260
+ ``"#RRGGBB"`` hex, or ``(r, g, b)`` 3-tuple). Pass ``None`` to
261
+ leave a property alone — explicit defaults are required to be
262
+ keyword-only so ``set_paragraph_defaults(font_name="Inter")``
263
+ is unambiguous.
264
+
265
+ See ``IMPROVEMENT_PLAN.md`` item 8.
266
+ """
267
+ rgb = None
268
+ if color is not None:
269
+ from pptx2._color import coerce_color
270
+
271
+ rgb = coerce_color(color)
272
+
273
+ def _apply_to_font(font: Font) -> None:
274
+ if font_name is not None and font.name is None:
275
+ font.name = font_name
276
+ if size is not None and font.size is None:
277
+ font.size = size
278
+ if bold is not None and font.bold is None:
279
+ font.bold = bold
280
+ if italic is not None and font.italic is None:
281
+ font.italic = italic
282
+ if rgb is not None:
283
+ # Don't read ``font.color.rgb`` first — that raises
284
+ # ``AttributeError`` for runs with an explicit non-RGB
285
+ # color (e.g. ``theme_color`` / scheme colour), which
286
+ # would crash the helper on mixed-format frames. Use
287
+ # ``font.color.type`` as the "is anything set?" probe
288
+ # instead; ``None`` means no explicit colour, scheme /
289
+ # RGB / preset / system means leave it alone.
290
+ try:
291
+ color_type = font.color.type
292
+ except AttributeError:
293
+ color_type = None
294
+ if color_type is None:
295
+ font.color.rgb = rgb
296
+
297
+ for paragraph in self.paragraphs:
298
+ # The paragraph-level Font controls run defaults via
299
+ # `a:defRPr`; setting it here gives a baseline that empty
300
+ # paragraphs inherit, while the per-run pass below
301
+ # overwrites any already-set run-level properties.
302
+ _apply_to_font(paragraph.font)
303
+ for run in paragraph.runs:
304
+ _apply_to_font(run.font)
305
+
306
+ @property
307
+ def column_count(self) -> int:
308
+ """Number of text columns laid out within this text frame.
309
+
310
+ Read/write. Corresponds to the ``numCol`` attribute on ``<a:bodyPr>``. A value of
311
+ ``1`` (the default) means a single column. Valid values are integers in the range
312
+ 1..16. Reading returns ``1`` when no explicit value is set (the inherited default);
313
+ assigning ``1`` removes any explicit setting.
314
+ """
315
+ numCol = self._bodyPr.numCol
316
+ return 1 if numCol is None else numCol
317
+
318
+ @column_count.setter
319
+ def column_count(self, value: int):
320
+ self._bodyPr.numCol = None if value == 1 else value
321
+
322
+ @property
323
+ def column_spacing(self) -> Length | None:
324
+ """Spacing between adjacent text columns as a |Length|.
325
+
326
+ Read/write. Corresponds to the ``spcCol`` attribute on ``<a:bodyPr>``, the gutter
327
+ between columns when :attr:`column_count` is greater than 1. |None| indicates no
328
+ explicit value is set; assigning |None| removes any explicit value.
329
+ """
330
+ return self._bodyPr.spcCol
331
+
332
+ @column_spacing.setter
333
+ def column_spacing(self, value: Length | None):
334
+ self._bodyPr.spcCol = value
335
+
336
+ @property
337
+ def vertical_anchor(self) -> MSO_VERTICAL_ANCHOR | None:
338
+ """Represents the vertical alignment of text in this text frame.
339
+
340
+ |None| indicates the effective value should be inherited from this object's style hierarchy.
341
+ """
342
+ return self._txBody.bodyPr.anchor
343
+
344
+ @vertical_anchor.setter
345
+ def vertical_anchor(self, value: MSO_VERTICAL_ANCHOR | None):
346
+ bodyPr = self._txBody.bodyPr
347
+ bodyPr.anchor = value
348
+
349
+ @property
350
+ def word_wrap(self) -> bool | None:
351
+ """`True` when lines of text in this shape are wrapped to fit within the shape's width.
352
+
353
+ Read-write. Valid values are True, False, or None. True and False turn word wrap on and
354
+ off, respectively. Assigning None to word wrap causes any word wrap setting to be removed
355
+ from the text frame, causing it to inherit this setting from its style hierarchy.
356
+ """
357
+ return {
358
+ ST_TextWrappingType.SQUARE: True,
359
+ ST_TextWrappingType.NONE: False,
360
+ None: None,
361
+ }[self._txBody.bodyPr.wrap]
362
+
363
+ @word_wrap.setter
364
+ def word_wrap(self, value: bool | None):
365
+ if value not in (True, False, None):
366
+ raise ValueError( # pragma: no cover
367
+ "assigned value must be True, False, or None, got %s" % value
368
+ )
369
+ self._txBody.bodyPr.wrap = {
370
+ True: ST_TextWrappingType.SQUARE,
371
+ False: ST_TextWrappingType.NONE,
372
+ None: None,
373
+ }[value]
374
+
375
+ def _apply_fit(self, font_family: str, font_size: int, is_bold: bool, is_italic: bool):
376
+ """Arrange text in this text frame to fit inside its extents.
377
+
378
+ This is accomplished by setting auto size off, wrap on, and setting the font of
379
+ all its text to `font_family`, `font_size`, `is_bold`, and `is_italic`.
380
+ """
381
+ self.auto_size = MSO_AUTO_SIZE.NONE
382
+ self.word_wrap = True
383
+ self._set_font(font_family, font_size, is_bold, is_italic)
384
+
385
+ def _best_fit_font_size(
386
+ self,
387
+ family: str,
388
+ max_size: int,
389
+ bold: bool,
390
+ italic: bool,
391
+ font_file: str | None,
392
+ strict: bool = False,
393
+ *,
394
+ warn_on_fallback: bool = True,
395
+ ) -> int:
396
+ """Return font-size in points that best fits text in this text-frame.
397
+
398
+ The best-fit font size is the largest integer point size not greater than `max_size` that
399
+ allows all the text in this text frame to fit inside its extents when rendered using the
400
+ font described by `family`, `bold`, and `italic`. If `font_file` is specified, it is used
401
+ to calculate the fit, whether or not it matches `family`, `bold`, and `italic`. When no
402
+ font file is provided and no matching system font can be located, Pillow's bundled
403
+ default font is used so `fit_text` produces a usable estimate rather than raising.
404
+
405
+ Raises :class:`ValueError` when even 1pt overflows the text frame — typically the
406
+ frame is too small to render the wrapped text at any usable size. Pre-IMPROVEMENTS
407
+ item 7 this silently returned ``None`` and crashed the downstream ``_apply_fit``
408
+ setter with a confusing ``TypeError`` from inside ``Pt(None)``.
409
+ """
410
+ if font_file is None:
411
+ font_file = find_font_file(family, bold, italic)
412
+ if font_file is None:
413
+ style = "".join((" bold" if bold else "", " italic" if italic else ""))
414
+ detail = (
415
+ f"{family!r}{style} is not installed, so fit_text measured with Pillow's "
416
+ "default font instead of real metrics — the chosen size is an estimate and "
417
+ "the text may still overflow when rendered with the intended face"
418
+ )
419
+ if strict:
420
+ raise ValueError(
421
+ f"fit_text(strict=True): {detail}. Pass font_file= with a TrueType "
422
+ "file for this family, or choose an installed family "
423
+ "(pptx2.text.fonts.installed_font_families() lists them)."
424
+ )
425
+ if warn_on_fallback:
426
+ warnings.warn(
427
+ f"{detail}. Pass font_file=, use strict=True to make this an error, or "
428
+ "check pptx2.text.fonts.font_is_installed() first.",
429
+ FontMetricsWarning,
430
+ stacklevel=3,
431
+ )
432
+ size = TextFitter.best_fit_font_size(self.text, self._extents, max_size, font_file)
433
+ if size is None:
434
+ raise ValueError(
435
+ "fit_text: text does not fit at any size from 1pt to "
436
+ f"{max_size}pt in this text frame; resize the shape, "
437
+ "shorten the text, or split it across multiple frames."
438
+ )
439
+ return size
440
+
441
+ @property
442
+ def _bodyPr(self):
443
+ return self._txBody.bodyPr
444
+
445
+ @property
446
+ def _extents(self) -> tuple[Length, Length]:
447
+ """(cx, cy) 2-tuple representing the effective rendering area of this text-frame.
448
+
449
+ Margins are taken into account.
450
+ """
451
+ parent = cast("ProvidesExtents", self._parent)
452
+ return (
453
+ Length(parent.width - self.margin_left - self.margin_right),
454
+ Length(parent.height - self.margin_top - self.margin_bottom),
455
+ )
456
+
457
+ def _set_font(self, family: str, size: int, bold: bool, italic: bool):
458
+ """Set the font properties of all the text in this text frame."""
459
+
460
+ def iter_rPrs(txBody: CT_TextBody) -> Iterator[CT_TextCharacterProperties]:
461
+ for p in txBody.p_lst:
462
+ for elm in p.content_children:
463
+ yield elm.get_or_add_rPr()
464
+ # generate a:endParaRPr for each <a:p> element
465
+ yield p.get_or_add_endParaRPr()
466
+
467
+ def set_rPr_font(
468
+ rPr: CT_TextCharacterProperties, name: str, size: int, bold: bool, italic: bool
469
+ ):
470
+ f = Font(rPr)
471
+ f.name, f.size, f.bold, f.italic = family, Pt(size), bold, italic
472
+
473
+ txBody = self._element
474
+ for rPr in iter_rPrs(txBody):
475
+ set_rPr_font(rPr, family, size, bold, italic)
476
+
477
+
478
+ class Font(object):
479
+ """Character properties object, providing font size, font name, bold, italic, etc.
480
+
481
+ Corresponds to `a:rPr` child element of a run. Also appears as `a:defRPr` and
482
+ `a:endParaRPr` in paragraph and `a:defRPr` in list style elements.
483
+ """
484
+
485
+ def __init__(self, rPr: CT_TextCharacterProperties):
486
+ super(Font, self).__init__()
487
+ self._element = self._rPr = rPr
488
+
489
+ @property
490
+ def bold(self) -> bool | None:
491
+ """Get or set boolean bold value of |Font|, e.g. `paragraph.font.bold = True`.
492
+
493
+ If set to |None|, the bold setting is cleared and is inherited from an enclosing shape's
494
+ setting, or a setting in a style or master. Returns None if no bold attribute is present,
495
+ meaning the effective bold value is inherited from a master or the theme.
496
+ """
497
+ return self._rPr.b
498
+
499
+ @bold.setter
500
+ def bold(self, value: bool | None):
501
+ self._rPr.b = value
502
+
503
+ @lazyproperty
504
+ def color(self) -> ColorFormat:
505
+ """The |ColorFormat| instance that provides access to the color settings for this font.
506
+
507
+ Reads are non-mutating: when no explicit solid fill is set, accessing color
508
+ properties returns the "no explicit color" sentinel (preserving theme
509
+ inheritance) instead of inserting an empty `<a:solidFill>`. The fill is only
510
+ switched to solid when `rgb` or `theme_color` is assigned.
511
+ """
512
+ return _LazyColorFormat(peek_fill=lambda: self.fill, ensure_fill=lambda: self.fill)
513
+
514
+ @lazyproperty
515
+ def fill(self) -> FillFormat:
516
+ """|FillFormat| instance for this font.
517
+
518
+ Provides access to fill properties such as fill color.
519
+ """
520
+ return FillFormat.from_fill_parent(self._rPr)
521
+
522
+ @property
523
+ def italic(self) -> bool | None:
524
+ """Get or set boolean italic value of |Font| instance.
525
+
526
+ Has the same behaviors as bold with respect to None values.
527
+ """
528
+ return self._rPr.i
529
+
530
+ @italic.setter
531
+ def italic(self, value: bool | None):
532
+ self._rPr.i = value
533
+
534
+ @lazyproperty
535
+ def outline(self) -> LineFormat:
536
+ """|LineFormat| for the text-outline stroke of this font.
537
+
538
+ Wraps the `<a:ln>` child of the run's `<a:rPr>`, letting you give glyphs a
539
+ coloured outline::
540
+
541
+ run.font.outline.color.rgb = "FF0000"
542
+ run.font.outline.width = Pt(1)
543
+
544
+ Reads are non-mutating — no `<a:ln>` element is written until an outline
545
+ property is assigned, preserving inheritance from the style hierarchy.
546
+ """
547
+ return LineFormat(self._rPr)
548
+
549
+ @lazyproperty
550
+ def shadow(self) -> ShadowFormat:
551
+ """|ShadowFormat| for the outer-shadow effect on this font's glyphs.
552
+
553
+ Wraps the `<a:effectLst>` child of the run's `<a:rPr>`::
554
+
555
+ run.font.shadow.color.rgb = "808080"
556
+ run.font.shadow.blur_radius = Pt(3)
557
+
558
+ Reads are non-mutating; the effect XML is created lazily on first write.
559
+ """
560
+ # -- ShadowFormat needs an element exposing `effectLst` /
561
+ # -- `get_or_add_effectLst`, which CT_TextCharacterProperties now
562
+ # -- provides; the annotated `CT_ShapeProperties` is structural here. --
563
+ return ShadowFormat(self._rPr) # pyright: ignore[reportArgumentType]
564
+
565
+ @lazyproperty
566
+ def glow(self) -> GlowFormat:
567
+ """|GlowFormat| for the glow effect on this font's glyphs.
568
+
569
+ Wraps the `<a:effectLst>` child of the run's `<a:rPr>`::
570
+
571
+ run.font.glow.color.rgb = "00B0F0"
572
+ run.font.glow.radius = Pt(6)
573
+
574
+ Reads are non-mutating; the effect XML is created lazily on first write.
575
+ """
576
+ # -- GlowFormat needs an element exposing `effectLst` /
577
+ # -- `get_or_add_effectLst` (structural; see `shadow` above). --
578
+ return GlowFormat(self._rPr) # pyright: ignore[reportArgumentType]
579
+
580
+ @property
581
+ def caps(self) -> str | None:
582
+ """Capitalization effect: ``"none"``, ``"small"``, ``"all"``, or |None|.
583
+
584
+ |None| means the setting is inherited (no ``cap`` attribute is written).
585
+ Most callers want the :attr:`all_caps` / :attr:`small_caps` booleans;
586
+ this is the raw accessor.
587
+ """
588
+ return self._rPr.cap
589
+
590
+ @caps.setter
591
+ def caps(self, value: str | None):
592
+ self._rPr.cap = value
593
+
594
+ @property
595
+ def all_caps(self) -> bool | None:
596
+ """Whether the text renders in all capitals (``<a:rPr cap="all">``).
597
+
598
+ Returns |None| when no capitalization is set (inherited). Setting
599
+ ``False`` writes ``cap="none"`` (an explicit override); setting |None|
600
+ clears the attribute. ``all_caps`` and :attr:`small_caps` share the one
601
+ ``cap`` attribute, so they are mutually exclusive.
602
+ """
603
+ cap = self._rPr.cap
604
+ return None if cap is None else cap == "all"
605
+
606
+ @all_caps.setter
607
+ def all_caps(self, value: bool | None):
608
+ self._rPr.cap = None if value is None else ("all" if value else "none")
609
+
610
+ @property
611
+ def small_caps(self) -> bool | None:
612
+ """Whether the text renders in small capitals (``<a:rPr cap="small">``).
613
+
614
+ Same inheritance / mutual-exclusion semantics as :attr:`all_caps`.
615
+ """
616
+ cap = self._rPr.cap
617
+ return None if cap is None else cap == "small"
618
+
619
+ @small_caps.setter
620
+ def small_caps(self, value: bool | None):
621
+ self._rPr.cap = None if value is None else ("small" if value else "none")
622
+
623
+ @property
624
+ def letter_spacing(self) -> Length | None:
625
+ """Inter-character spacing (tracking) as a |Length|, e.g. ``Pt(1.5)``.
626
+
627
+ Positive values spread the text out, negative values tighten it.
628
+ Returns |None| when inherited. Assign a |Length| (``Pt(...)``,
629
+ ``Centipoints(...)``) or |None| to clear.
630
+ """
631
+ return self._rPr.spc
632
+
633
+ @letter_spacing.setter
634
+ def letter_spacing(self, value: Length | None):
635
+ self._rPr.spc = value
636
+
637
+ @property
638
+ def strikethrough(self) -> bool | None:
639
+ """Whether a strikethrough line is drawn through the text.
640
+
641
+ Returns |None| when inherited, |True| for either single or double
642
+ strike, |False| for an explicit no-strike. Setting |True| writes a
643
+ single strike (``strike="sngStrike"``); use :attr:`caps`-style raw
644
+ access via the XML for the double variant.
645
+ """
646
+ strike = self._rPr.strike
647
+ return None if strike is None else strike != "noStrike"
648
+
649
+ @strikethrough.setter
650
+ def strikethrough(self, value: bool | None):
651
+ self._rPr.strike = None if value is None else ("sngStrike" if value else "noStrike")
652
+
653
+ @property
654
+ def superscript(self) -> bool | None:
655
+ """Whether the text is raised as superscript (``<a:rPr baseline="...">`` > 0).
656
+
657
+ Returns |None| when no baseline shift is set. Setting |True| raises the
658
+ run by 30%; |False| / |None| clears the shift. :attr:`superscript` and
659
+ :attr:`subscript` share the one ``baseline`` attribute and so are
660
+ mutually exclusive.
661
+ """
662
+ baseline = self._rPr.baseline
663
+ return None if baseline is None else baseline > 0
664
+
665
+ @superscript.setter
666
+ def superscript(self, value: bool | None):
667
+ if value:
668
+ self._rPr.baseline = 0.30
669
+ elif value is None or self.superscript:
670
+ # Only clear when actually superscript so we don't wipe a
671
+ # sibling subscript (both share the one ``baseline`` attribute).
672
+ self._rPr.baseline = None
673
+
674
+ @property
675
+ def subscript(self) -> bool | None:
676
+ """Whether the text is lowered as subscript (``<a:rPr baseline="...">`` < 0).
677
+
678
+ Same semantics as :attr:`superscript`; setting |True| lowers the run by
679
+ 25%.
680
+ """
681
+ baseline = self._rPr.baseline
682
+ return None if baseline is None else baseline < 0
683
+
684
+ @subscript.setter
685
+ def subscript(self, value: bool | None):
686
+ if value:
687
+ self._rPr.baseline = -0.25
688
+ elif value is None or self.subscript:
689
+ # Only clear when actually subscript so we don't wipe a
690
+ # sibling superscript (both share the one ``baseline`` attribute).
691
+ self._rPr.baseline = None
692
+
693
+ @property
694
+ def language_id(self) -> MSO_LANGUAGE_ID | None:
695
+ """Get or set the language id of this |Font| instance.
696
+
697
+ The language id is a member of the :ref:`MsoLanguageId` enumeration. Assigning |None|
698
+ removes any language setting, the same behavior as assigning `MSO_LANGUAGE_ID.NONE`.
699
+ """
700
+ lang = self._rPr.lang
701
+ if lang is None:
702
+ return MSO_LANGUAGE_ID.NONE
703
+ return self._rPr.lang
704
+
705
+ @language_id.setter
706
+ def language_id(self, value: MSO_LANGUAGE_ID | None):
707
+ if value == MSO_LANGUAGE_ID.NONE:
708
+ value = None
709
+ self._rPr.lang = value
710
+
711
+ @property
712
+ def name(self) -> str | None:
713
+ """Get or set the typeface name for this |Font| instance.
714
+
715
+ Causes the text it controls to appear in the named font, if a matching font is found.
716
+ Returns |None| if the typeface is currently inherited from the theme. Setting it to |None|
717
+ removes any override of the theme typeface.
718
+ """
719
+ latin = self._rPr.latin
720
+ if latin is None:
721
+ return None
722
+ return latin.typeface
723
+
724
+ @name.setter
725
+ def name(self, value: str | None):
726
+ if value is None:
727
+ self._rPr._remove_latin() # pyright: ignore[reportPrivateUsage]
728
+ else:
729
+ latin = self._rPr.get_or_add_latin()
730
+ latin.typeface = value
731
+
732
+ @property
733
+ def size(self) -> Length | None:
734
+ """Indicates the font height in English Metric Units (EMU).
735
+
736
+ Read/write. |None| indicates the font size should be inherited from its style hierarchy,
737
+ such as a placeholder or document defaults (usually 18pt). |Length| is a subclass of |int|
738
+ having properties for convenient conversion into points or other length units. Likewise,
739
+ the :class:`pptx2.util.Pt` class allows convenient specification of point values::
740
+
741
+ >>> font.size = Pt(24)
742
+ >>> font.size
743
+ 304800
744
+ >>> font.size.pt
745
+ 24.0
746
+ """
747
+ sz = self._rPr.sz
748
+ if sz is None:
749
+ return None
750
+ return Centipoints(sz)
751
+
752
+ @size.setter
753
+ def size(self, emu: Length | None):
754
+ if emu is None:
755
+ self._rPr.sz = None
756
+ else:
757
+ sz = Emu(emu).centipoints
758
+ self._rPr.sz = sz
759
+
760
+ @property
761
+ def underline(self) -> bool | MSO_TEXT_UNDERLINE_TYPE | None:
762
+ """Indicaties the underline setting for this font.
763
+
764
+ Value is |True|, |False|, |None|, or a member of the :ref:`MsoTextUnderlineType`
765
+ enumeration. |None| is the default and indicates the underline setting should be inherited
766
+ from the style hierarchy, such as from a placeholder. |True| indicates single underline.
767
+ |False| indicates no underline. Other settings such as double and wavy underlining are
768
+ indicated with members of the :ref:`MsoTextUnderlineType` enumeration.
769
+ """
770
+ u = self._rPr.u
771
+ if u is MSO_UNDERLINE.NONE:
772
+ return False
773
+ if u is MSO_UNDERLINE.SINGLE_LINE:
774
+ return True
775
+ return u
776
+
777
+ @underline.setter
778
+ def underline(self, value: bool | MSO_TEXT_UNDERLINE_TYPE | None):
779
+ if value is True:
780
+ value = MSO_UNDERLINE.SINGLE_LINE
781
+ elif value is False:
782
+ value = MSO_UNDERLINE.NONE
783
+ self._element.u = value
784
+
785
+
786
+ class _Hyperlink(Subshape):
787
+ """Text run hyperlink object.
788
+
789
+ Corresponds to `a:hlinkClick` child element of the run's properties element (`a:rPr`).
790
+ """
791
+
792
+ def __init__(self, rPr: CT_TextCharacterProperties, parent: ProvidesPart):
793
+ super(_Hyperlink, self).__init__(parent)
794
+ self._rPr = rPr
795
+
796
+ @property
797
+ def address(self) -> str | None:
798
+ """The URL of the hyperlink.
799
+
800
+ Read/write. URL can be on http, https, mailto, or file scheme; others may work.
801
+ """
802
+ if self._hlinkClick is None:
803
+ return None
804
+ return self.part.target_ref(self._hlinkClick.rId)
805
+
806
+ @address.setter
807
+ def address(self, url: str | None):
808
+ # implements all three of add, change, and remove hyperlink
809
+ if self._hlinkClick is not None:
810
+ self._remove_hlinkClick()
811
+ if url:
812
+ self._add_hlinkClick(url)
813
+
814
+ @property
815
+ def target_slide(self) -> Slide | None:
816
+ """Slide in this presentation that this hyperlink jumps to.
817
+
818
+ Read/write. Returns |None| when no hyperlink is present, or when the
819
+ hyperlink targets an external URL rather than an internal slide. Assigning a
820
+ |Slide| writes a relationship-based slide-jump action
821
+ (``ppaction://hlinksldjump``) instead of a URI; assigning |None| removes the
822
+ hyperlink.
823
+ """
824
+ hlink = self._hlinkClick
825
+ if hlink is None:
826
+ return None
827
+ if hlink.action != "ppaction://hlinksldjump":
828
+ return None
829
+ rId = hlink.rId
830
+ if not rId:
831
+ return None
832
+ slide_part = cast("SlidePart", self.part.related_part(rId))
833
+ return slide_part.slide
834
+
835
+ @target_slide.setter
836
+ def target_slide(self, slide: Slide | None):
837
+ if self._hlinkClick is not None:
838
+ self._remove_hlinkClick()
839
+ if slide is None:
840
+ return
841
+ rId = self.part.relate_to(slide.part, RT.SLIDE)
842
+ hlink = self._rPr.get_or_add_hlinkClick()
843
+ hlink.action = "ppaction://hlinksldjump"
844
+ hlink.rId = rId
845
+
846
+ def _add_hlinkClick(self, url: str):
847
+ rId = self.part.relate_to(url, RT.HYPERLINK, is_external=True)
848
+ self._rPr.add_hlinkClick(rId)
849
+
850
+ @property
851
+ def _hlinkClick(self) -> CT_Hyperlink | None:
852
+ return self._rPr.hlinkClick
853
+
854
+ def _remove_hlinkClick(self):
855
+ assert self._hlinkClick is not None
856
+ rId = self._hlinkClick.rId
857
+ if rId:
858
+ self.part.drop_rel(rId)
859
+ self._rPr._remove_hlinkClick() # pyright: ignore[reportPrivateUsage]
860
+
861
+
862
+ class _Paragraph(Subshape):
863
+ """Paragraph object. Not intended to be constructed directly."""
864
+
865
+ def __init__(self, p: CT_TextParagraph, parent: ProvidesPart):
866
+ super(_Paragraph, self).__init__(parent)
867
+ self._element = self._p = p
868
+
869
+ def add_line_break(self):
870
+ """Add line break at end of this paragraph."""
871
+ self._p.add_br()
872
+
873
+ def add_run(self) -> _Run:
874
+ """Return a new run appended to the runs in this paragraph."""
875
+ r = self._p.add_r()
876
+ return _Run(r, self)
877
+
878
+ def add_math(
879
+ self,
880
+ latex: str,
881
+ *,
882
+ display: bool = False,
883
+ font: str | None = None,
884
+ size_pt: float | None = None,
885
+ color=None,
886
+ ):
887
+ """Append a native PowerPoint equation from a LaTeX math fragment.
888
+
889
+ Requires ``latex2mathml`` and ``mathml2omml``
890
+ (``pip install "python-pptx2[math]"``). *display* wraps the equation
891
+ in ``m:oMathPara`` (a standalone line); the default is inline
892
+ ``m:oMath`` so it can sit between ordinary runs.
893
+
894
+ *font* / *size_pt* / *color* are written onto each math run when
895
+ given. Returns this paragraph so mixed text + math can be chained.
896
+ """
897
+ from pptx2.math import office_math_element, style_office_math
898
+
899
+ # pPr must exist before the a14:m sibling so later alignment /
900
+ # font writes still insert it at the start of the paragraph.
901
+ self._p.get_or_add_pPr()
902
+ marker = office_math_element(latex, display=display)
903
+ style_office_math(marker, size_pt=size_pt, color=color, font=font)
904
+ self._p.add_math(marker)
905
+ return self
906
+
907
+ @property
908
+ def alignment(self) -> PP_PARAGRAPH_ALIGNMENT | None:
909
+ """Horizontal alignment of this paragraph.
910
+
911
+ The value |None| indicates the paragraph should 'inherit' its effective value from its
912
+ style hierarchy. Assigning |None| removes any explicit setting, causing its inherited
913
+ value to be used.
914
+ """
915
+ return self._pPr.algn
916
+
917
+ @alignment.setter
918
+ def alignment(self, value: PP_PARAGRAPH_ALIGNMENT | None):
919
+ self._pPr.algn = value
920
+
921
+ def clear(self):
922
+ """Remove all content from this paragraph.
923
+
924
+ Paragraph properties are preserved. Content includes runs, line breaks, and fields.
925
+ """
926
+ for elm in self._element.content_children:
927
+ self._element.remove(elm)
928
+ return self
929
+
930
+ @property
931
+ def font(self) -> Font:
932
+ """|Font| object containing default character properties for the runs in this paragraph.
933
+
934
+ These character properties override default properties inherited from parent objects such
935
+ as the text frame the paragraph is contained in and they may be overridden by character
936
+ properties set at the run level.
937
+ """
938
+ return Font(self._defRPr)
939
+
940
+ @property
941
+ def level(self) -> int:
942
+ """Indentation level of this paragraph.
943
+
944
+ Read-write. Integer in range 0..8 inclusive. 0 represents a top-level paragraph and is the
945
+ default value. Indentation level is most commonly encountered in a bulleted list, as is
946
+ found on a word bullet slide.
947
+ """
948
+ return self._pPr.lvl
949
+
950
+ @level.setter
951
+ def level(self, level: int):
952
+ self._pPr.lvl = level
953
+
954
+ @property
955
+ def rtl(self) -> bool | None:
956
+ """Right-to-left paragraph direction, e.g. for Hebrew, Arabic, or Farsi text.
957
+
958
+ Read/write tri-state, like :attr:`Font.bold`. Corresponds to the ``rtl`` attribute on
959
+ ``<a:pPr>``. |True| lays the paragraph out right-to-left, |False| forces
960
+ left-to-right, and |None| (the default) removes any explicit setting so the effective
961
+ value is inherited from the style hierarchy.
962
+ """
963
+ pPr = self._p.pPr
964
+ if pPr is None:
965
+ return None
966
+ return pPr.rtl
967
+
968
+ @rtl.setter
969
+ def rtl(self, value: bool | None):
970
+ self._pPr.rtl = value
971
+
972
+ @property
973
+ def start_at(self) -> int | None:
974
+ """Starting number of an auto-numbered (ordered) list paragraph.
975
+
976
+ Read/write. Corresponds to the ``startAt`` attribute on ``<a:buAutoNum>``. Returns
977
+ |None| when this paragraph is not an auto-number list, or when the numbering starts at
978
+ the default of ``1`` (no explicit ``startAt``).
979
+
980
+ Assigning an integer turns this paragraph into an auto-numbered list if it isn't one
981
+ already, defaulting the numbering scheme to ``"arabicPeriod"`` (e.g. ``1.``, ``2.``).
982
+ Use :meth:`set_numbered` to choose a different scheme. Assigning |None| removes the
983
+ explicit ``startAt`` (numbering resumes from ``1``) but leaves the list auto-numbered.
984
+ """
985
+ buAutoNum = self._p.pPr.buAutoNum if self._p.pPr is not None else None
986
+ if buAutoNum is None:
987
+ return None
988
+ return buAutoNum.startAt
989
+
990
+ @start_at.setter
991
+ def start_at(self, value: int | None):
992
+ existing = self._p.pPr.buAutoNum if self._p.pPr is not None else None
993
+ if value is None:
994
+ if existing is not None:
995
+ existing.startAt = None
996
+ return
997
+ if existing is not None:
998
+ # -- already a numbered list: just update startAt, preserving the scheme --
999
+ existing.startAt = value
1000
+ else:
1001
+ self.set_numbered(start_at=value)
1002
+
1003
+ def set_numbered(self, scheme: str = "arabicPeriod", start_at: int | None = None):
1004
+ """Make this paragraph an auto-numbered list item.
1005
+
1006
+ ``scheme`` is an ``ST_TextAutonumberScheme`` token such as ``"arabicPeriod"`` (the
1007
+ default, ``1.`` ``2.`` ``3.``), ``"romanLcPeriod"`` (``i.`` ``ii.``), or
1008
+ ``"alphaUcParenR"`` (``A)`` ``B)``). ``start_at`` optionally sets the first number
1009
+ (1..32767); when |None| the list starts at ``1``. Replaces any existing bullet on the
1010
+ paragraph (the bullet element group is an XSD choice).
1011
+ """
1012
+ buAutoNum = self._pPr.get_or_add_buAutoNum_only()
1013
+ buAutoNum.type = scheme
1014
+ buAutoNum.startAt = start_at
1015
+
1016
+ @lazyproperty
1017
+ def tab_stops(self) -> TabStops:
1018
+ """|TabStops| collection of the explicit tab stops for this paragraph.
1019
+
1020
+ Tab stops are stored in the ``<a:tabLst>`` child of ``<a:pPr>``. The collection is
1021
+ iterable, supports ``len()``, and exposes :meth:`TabStops.add_tab_stop` to append a
1022
+ stop at a given position and alignment.
1023
+ """
1024
+ return TabStops(self._pPr)
1025
+
1026
+ @property
1027
+ def line_spacing(self) -> int | float | Length | None:
1028
+ """The space between baselines in successive lines of this paragraph.
1029
+
1030
+ A value of |None| indicates no explicit value is assigned and its effective value is
1031
+ inherited from the paragraph's style hierarchy. A numeric value, e.g. `2` or `1.5`,
1032
+ indicates spacing is applied in multiples of line heights. A |Length| value such as
1033
+ `Pt(12)` indicates spacing is a fixed height. The |Pt| value class is a convenient way to
1034
+ apply line spacing in units of points.
1035
+ """
1036
+ pPr = self._p.pPr
1037
+ if pPr is None:
1038
+ return None
1039
+ return pPr.line_spacing
1040
+
1041
+ @line_spacing.setter
1042
+ def line_spacing(self, value: int | float | Length | None):
1043
+ pPr = self._p.get_or_add_pPr()
1044
+ pPr.line_spacing = value
1045
+
1046
+ @property
1047
+ def runs(self) -> tuple[_Run, ...]:
1048
+ """Sequence of runs in this paragraph."""
1049
+ return tuple(_Run(r, self) for r in self._element.r_lst)
1050
+
1051
+ @property
1052
+ def space_after(self) -> Length | None:
1053
+ """The spacing to appear between this paragraph and the subsequent paragraph.
1054
+
1055
+ A value of |None| indicates no explicit value is assigned and its effective value is
1056
+ inherited from the paragraph's style hierarchy. |Length| objects provide convenience
1057
+ properties, such as `.pt` and `.inches`, that allow easy conversion to various length
1058
+ units.
1059
+ """
1060
+ pPr = self._p.pPr
1061
+ if pPr is None:
1062
+ return None
1063
+ return pPr.space_after
1064
+
1065
+ @space_after.setter
1066
+ def space_after(self, value: Length | None):
1067
+ pPr = self._p.get_or_add_pPr()
1068
+ pPr.space_after = value
1069
+
1070
+ @property
1071
+ def space_before(self) -> Length | None:
1072
+ """The spacing to appear between this paragraph and the prior paragraph.
1073
+
1074
+ A value of |None| indicates no explicit value is assigned and its effective value is
1075
+ inherited from the paragraph's style hierarchy. |Length| objects provide convenience
1076
+ properties, such as `.pt` and `.cm`, that allow easy conversion to various length units.
1077
+ """
1078
+ pPr = self._p.pPr
1079
+ if pPr is None:
1080
+ return None
1081
+ return pPr.space_before
1082
+
1083
+ @space_before.setter
1084
+ def space_before(self, value: Length | None):
1085
+ pPr = self._p.get_or_add_pPr()
1086
+ pPr.space_before = value
1087
+
1088
+ @property
1089
+ def text(self) -> str:
1090
+ """Text of paragraph as a single string.
1091
+
1092
+ Read/write. This value is formed by concatenating the text in each run and field making up
1093
+ the paragraph, adding a vertical-tab character (`"\\v"`) for each line-break element
1094
+ (`<a:br>`, soft carriage-return) encountered.
1095
+
1096
+ While the encoding of line-breaks as a vertical tab might be surprising at first, doing so
1097
+ is consistent with PowerPoint's clipboard copy behavior and allows a line-break to be
1098
+ distinguished from a paragraph boundary within the str return value.
1099
+
1100
+ Assignment causes all content in the paragraph to be replaced. Each vertical-tab character
1101
+ (`"\\v"`) in the assigned str is translated to a line-break, as is each line-feed
1102
+ character (`"\\n"`). Contrast behavior of line-feed character in `TextFrame.text` setter.
1103
+ If line-feed characters are intended to produce new paragraphs, use `TextFrame.text`
1104
+ instead. Any other control characters in the assigned string are escaped as a hex
1105
+ representation like "_x001B_" (for ESC (ASCII 27) in this example).
1106
+ """
1107
+ return "".join(elm.text for elm in self._element.content_children)
1108
+
1109
+ @text.setter
1110
+ def text(self, text: str):
1111
+ self.clear()
1112
+ self._element.append_text(text)
1113
+
1114
+ @property
1115
+ def _defRPr(self) -> CT_TextCharacterProperties:
1116
+ """The element that defines the default run properties for runs in this paragraph.
1117
+
1118
+ Causes the element to be added if not present.
1119
+ """
1120
+ return self._pPr.get_or_add_defRPr()
1121
+
1122
+ @property
1123
+ def _pPr(self) -> CT_TextParagraphProperties:
1124
+ """Contains the properties for this paragraph.
1125
+
1126
+ Causes the element to be added if not present.
1127
+ """
1128
+ return self._p.get_or_add_pPr()
1129
+
1130
+
1131
+ # -- mapping between the friendly tab-stop alignment names accepted by the API
1132
+ # -- and the `ST_TextTabAlignType` tokens emitted to the XML `algn` attribute. --
1133
+ _TAB_ALIGNMENTS = {
1134
+ "left": "l",
1135
+ "center": "ctr",
1136
+ "right": "r",
1137
+ "decimal": "dec",
1138
+ }
1139
+ _TAB_ALIGNMENTS_INV = {v: k for k, v in _TAB_ALIGNMENTS.items()}
1140
+
1141
+
1142
+ class TabStops(object):
1143
+ """A sequence of |TabStop| objects providing access to a paragraph's tab stops.
1144
+
1145
+ Wraps the ``<a:tabLst>`` element of an ``<a:pPr>``. The collection is created on demand and
1146
+ is iterable and sized (``len()``); indexing returns a |TabStop|.
1147
+ """
1148
+
1149
+ def __init__(self, pPr: CT_TextParagraphProperties):
1150
+ super(TabStops, self).__init__()
1151
+ self._pPr = pPr
1152
+
1153
+ def __getitem__(self, idx: int) -> TabStop:
1154
+ tabLst = self._pPr.tabLst
1155
+ if tabLst is None:
1156
+ raise IndexError("TabStops object has no tab stops")
1157
+ return TabStop(tabLst.tab_lst[idx])
1158
+
1159
+ def __iter__(self) -> Iterator[TabStop]:
1160
+ tabLst = self._pPr.tabLst
1161
+ if tabLst is None:
1162
+ return iter(())
1163
+ return (TabStop(tab) for tab in tabLst.tab_lst)
1164
+
1165
+ def __len__(self) -> int:
1166
+ tabLst = self._pPr.tabLst
1167
+ if tabLst is None:
1168
+ return 0
1169
+ return len(tabLst.tab_lst)
1170
+
1171
+ def add_tab_stop(self, position: Length, alignment: str = "left") -> TabStop:
1172
+ """Append and return a new |TabStop| at horizontal `position`.
1173
+
1174
+ `position` is a |Length| measured from the left edge of the text frame. `alignment` is
1175
+ one of ``"left"`` (default), ``"center"``, ``"right"``, or ``"decimal"``, controlling
1176
+ how text aligns to the tab stop.
1177
+ """
1178
+ if alignment not in _TAB_ALIGNMENTS:
1179
+ raise ValueError(
1180
+ "alignment must be one of %s, got %r"
1181
+ % (", ".join(repr(k) for k in _TAB_ALIGNMENTS), alignment)
1182
+ )
1183
+ tabLst = self._pPr.get_or_add_tabLst()
1184
+ tab = tabLst.add_tab(position, _TAB_ALIGNMENTS[alignment])
1185
+ return TabStop(tab)
1186
+
1187
+
1188
+ class TabStop(object):
1189
+ """An individual tab stop, an `<a:tab>` element within a paragraph's `<a:tabLst>`."""
1190
+
1191
+ def __init__(self, tab: CT_TextTabStop):
1192
+ super(TabStop, self).__init__()
1193
+ self._tab = tab
1194
+
1195
+ @property
1196
+ def position(self) -> Length | None:
1197
+ """The horizontal offset of this tab stop from the text frame's left edge, a |Length|."""
1198
+ return self._tab.pos
1199
+
1200
+ @position.setter
1201
+ def position(self, value: Length):
1202
+ self._tab.pos = value
1203
+
1204
+ @property
1205
+ def alignment(self) -> str:
1206
+ """Alignment of text at this tab stop.
1207
+
1208
+ One of ``"left"``, ``"center"``, ``"right"``, or ``"decimal"``. Defaults to ``"left"``
1209
+ when the underlying ``algn`` attribute is absent.
1210
+ """
1211
+ algn = self._tab.algn
1212
+ if algn is None:
1213
+ return "left"
1214
+ return _TAB_ALIGNMENTS_INV[algn]
1215
+
1216
+ @alignment.setter
1217
+ def alignment(self, value: str):
1218
+ if value not in _TAB_ALIGNMENTS:
1219
+ raise ValueError(
1220
+ "alignment must be one of %s, got %r"
1221
+ % (", ".join(repr(k) for k in _TAB_ALIGNMENTS), value)
1222
+ )
1223
+ self._tab.algn = _TAB_ALIGNMENTS[value]
1224
+
1225
+
1226
+ class _Run(Subshape):
1227
+ """Text run object. Corresponds to `a:r` child element in a paragraph."""
1228
+
1229
+ def __init__(self, r: CT_RegularTextRun, parent: ProvidesPart):
1230
+ super(_Run, self).__init__(parent)
1231
+ self._r = r
1232
+
1233
+ @property
1234
+ def font(self):
1235
+ """|Font| instance containing run-level character properties for the text in this run.
1236
+
1237
+ Character properties can be and perhaps most often are inherited from parent objects such
1238
+ as the paragraph and slide layout the run is contained in. Only those specifically
1239
+ overridden at the run level are contained in the font object.
1240
+ """
1241
+ rPr = self._r.get_or_add_rPr()
1242
+ return Font(rPr)
1243
+
1244
+ @lazyproperty
1245
+ def hyperlink(self) -> _Hyperlink:
1246
+ """Proxy for any `a:hlinkClick` element under the run properties element.
1247
+
1248
+ Created on demand, the hyperlink object is available whether an `a:hlinkClick` element is
1249
+ present or not, and creates or deletes that element as appropriate in response to actions
1250
+ on its methods and attributes.
1251
+ """
1252
+ rPr = self._r.get_or_add_rPr()
1253
+ return _Hyperlink(rPr, self)
1254
+
1255
+ @property
1256
+ def text(self):
1257
+ """Read/write. A unicode string containing the text in this run.
1258
+
1259
+ Assignment replaces all text in the run. The assigned value can be a 7-bit ASCII
1260
+ string, a UTF-8 encoded 8-bit string, or unicode. String values are converted to
1261
+ unicode assuming UTF-8 encoding.
1262
+
1263
+ Any other control characters in the assigned string other than tab or newline
1264
+ are escaped as a hex representation. For example, ESC (ASCII 27) is escaped as
1265
+ "_x001B_". Contrast the behavior of `TextFrame.text` and `_Paragraph.text` with
1266
+ respect to line-feed and vertical-tab characters.
1267
+ """
1268
+ return self._r.text
1269
+
1270
+ @text.setter
1271
+ def text(self, text: str):
1272
+ self._r.text = text