ps-python-pptx 1.1.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 (117) hide show
  1. pptx/__init__.py +84 -0
  2. pptx/action.py +270 -0
  3. pptx/api.py +49 -0
  4. pptx/chart/__init__.py +0 -0
  5. pptx/chart/axis.py +523 -0
  6. pptx/chart/category.py +200 -0
  7. pptx/chart/chart.py +280 -0
  8. pptx/chart/data.py +864 -0
  9. pptx/chart/datalabel.py +288 -0
  10. pptx/chart/legend.py +79 -0
  11. pptx/chart/marker.py +70 -0
  12. pptx/chart/plot.py +412 -0
  13. pptx/chart/point.py +101 -0
  14. pptx/chart/series.py +258 -0
  15. pptx/chart/xlsx.py +272 -0
  16. pptx/chart/xmlwriter.py +1840 -0
  17. pptx/dml/__init__.py +0 -0
  18. pptx/dml/chtfmt.py +40 -0
  19. pptx/dml/color.py +301 -0
  20. pptx/dml/effect.py +41 -0
  21. pptx/dml/fill.py +468 -0
  22. pptx/dml/line.py +100 -0
  23. pptx/enum/__init__.py +0 -0
  24. pptx/enum/action.py +71 -0
  25. pptx/enum/base.py +175 -0
  26. pptx/enum/chart.py +492 -0
  27. pptx/enum/dml.py +405 -0
  28. pptx/enum/lang.py +685 -0
  29. pptx/enum/shapes.py +1029 -0
  30. pptx/enum/text.py +371 -0
  31. pptx/exc.py +23 -0
  32. pptx/media.py +197 -0
  33. pptx/opc/__init__.py +0 -0
  34. pptx/opc/constants.py +331 -0
  35. pptx/opc/oxml.py +188 -0
  36. pptx/opc/package.py +762 -0
  37. pptx/opc/packuri.py +109 -0
  38. pptx/opc/serialized.py +296 -0
  39. pptx/opc/shared.py +20 -0
  40. pptx/opc/spec.py +44 -0
  41. pptx/oxml/__init__.py +492 -0
  42. pptx/oxml/action.py +53 -0
  43. pptx/oxml/chart/__init__.py +0 -0
  44. pptx/oxml/chart/axis.py +297 -0
  45. pptx/oxml/chart/chart.py +282 -0
  46. pptx/oxml/chart/datalabel.py +252 -0
  47. pptx/oxml/chart/legend.py +72 -0
  48. pptx/oxml/chart/marker.py +61 -0
  49. pptx/oxml/chart/plot.py +345 -0
  50. pptx/oxml/chart/series.py +254 -0
  51. pptx/oxml/chart/shared.py +219 -0
  52. pptx/oxml/coreprops.py +288 -0
  53. pptx/oxml/dml/__init__.py +0 -0
  54. pptx/oxml/dml/color.py +111 -0
  55. pptx/oxml/dml/fill.py +197 -0
  56. pptx/oxml/dml/line.py +12 -0
  57. pptx/oxml/ns.py +129 -0
  58. pptx/oxml/presentation.py +130 -0
  59. pptx/oxml/shapes/__init__.py +19 -0
  60. pptx/oxml/shapes/autoshape.py +455 -0
  61. pptx/oxml/shapes/connector.py +107 -0
  62. pptx/oxml/shapes/graphfrm.py +342 -0
  63. pptx/oxml/shapes/groupshape.py +280 -0
  64. pptx/oxml/shapes/picture.py +270 -0
  65. pptx/oxml/shapes/shared.py +523 -0
  66. pptx/oxml/simpletypes.py +740 -0
  67. pptx/oxml/slide.py +347 -0
  68. pptx/oxml/table.py +588 -0
  69. pptx/oxml/text.py +748 -0
  70. pptx/oxml/theme.py +29 -0
  71. pptx/oxml/xmlchemy.py +717 -0
  72. pptx/package.py +222 -0
  73. pptx/parts/__init__.py +0 -0
  74. pptx/parts/chart.py +95 -0
  75. pptx/parts/coreprops.py +167 -0
  76. pptx/parts/embeddedpackage.py +93 -0
  77. pptx/parts/image.py +275 -0
  78. pptx/parts/media.py +37 -0
  79. pptx/parts/presentation.py +126 -0
  80. pptx/parts/slide.py +297 -0
  81. pptx/parts/theme.py +13 -0
  82. pptx/presentation.py +113 -0
  83. pptx/py.typed +0 -0
  84. pptx/shapes/__init__.py +26 -0
  85. pptx/shapes/autoshape.py +355 -0
  86. pptx/shapes/base.py +293 -0
  87. pptx/shapes/connector.py +297 -0
  88. pptx/shapes/freeform.py +337 -0
  89. pptx/shapes/graphfrm.py +166 -0
  90. pptx/shapes/group.py +69 -0
  91. pptx/shapes/picture.py +203 -0
  92. pptx/shapes/placeholder.py +407 -0
  93. pptx/shapes/shapetree.py +1190 -0
  94. pptx/shared.py +82 -0
  95. pptx/slide.py +577 -0
  96. pptx/spec.py +632 -0
  97. pptx/table.py +520 -0
  98. pptx/templates/default.pptx +0 -0
  99. pptx/templates/docx-icon.emf +0 -0
  100. pptx/templates/generic-icon.emf +0 -0
  101. pptx/templates/notes.xml +23 -0
  102. pptx/templates/notesMaster.xml +352 -0
  103. pptx/templates/pptx-icon.emf +0 -0
  104. pptx/templates/theme.xml +321 -0
  105. pptx/templates/xlsx-icon.emf +0 -0
  106. pptx/text/__init__.py +0 -0
  107. pptx/text/fonts.py +399 -0
  108. pptx/text/layout.py +325 -0
  109. pptx/text/text.py +770 -0
  110. pptx/theme.py +135 -0
  111. pptx/types.py +36 -0
  112. pptx/util.py +284 -0
  113. ps_python_pptx-1.1.0.dist-info/METADATA +75 -0
  114. ps_python_pptx-1.1.0.dist-info/RECORD +117 -0
  115. ps_python_pptx-1.1.0.dist-info/WHEEL +5 -0
  116. ps_python_pptx-1.1.0.dist-info/licenses/LICENSE +20 -0
  117. ps_python_pptx-1.1.0.dist-info/top_level.txt +1 -0
pptx/text/text.py ADDED
@@ -0,0 +1,770 @@
1
+ """Text-related objects such as TextFrame and Paragraph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Iterator, cast
6
+
7
+ from pptx.dml.fill import FillFormat
8
+ from pptx.enum.dml import MSO_FILL
9
+ from pptx.enum.lang import MSO_LANGUAGE_ID
10
+ from pptx.enum.text import (
11
+ MSO_AUTO_SIZE,
12
+ MSO_UNDERLINE,
13
+ MSO_VERTICAL_ANCHOR,
14
+ )
15
+ from pptx.opc.constants import RELATIONSHIP_TYPE as RT
16
+ from pptx.oxml.simpletypes import ST_TextWrappingType
17
+ from pptx.shapes import Subshape
18
+ from pptx.text.fonts import FontFiles
19
+ from pptx.text.layout import TextFitter
20
+ from pptx.util import BulletStyle, Centipoints, Emu, Length, Pt, lazyproperty
21
+
22
+ if TYPE_CHECKING:
23
+ from pptx.dml.color import ColorFormat
24
+ from pptx.enum.text import (
25
+ MSO_TEXT_UNDERLINE_TYPE,
26
+ MSO_VERTICAL_ANCHOR,
27
+ PP_PARAGRAPH_ALIGNMENT,
28
+ )
29
+ from pptx.oxml.action import CT_Hyperlink
30
+ from pptx.oxml.text import (
31
+ CT_RegularTextRun,
32
+ CT_TextBody,
33
+ CT_TextCharacterProperties,
34
+ CT_TextParagraph,
35
+ CT_TextParagraphProperties,
36
+ )
37
+ from pptx.types import ProvidesExtents, ProvidesPart
38
+
39
+
40
+ class TextFrame(Subshape):
41
+ """The part of a shape that contains its text.
42
+
43
+ Not all shapes have a text frame. Corresponds to the `p:txBody` element that can
44
+ appear as a child element of `p:sp`. Not intended to be constructed directly.
45
+ """
46
+
47
+ def __init__(self, txBody: CT_TextBody, parent: ProvidesPart):
48
+ super(TextFrame, self).__init__(parent)
49
+ self._element = self._txBody = txBody
50
+ self._parent = parent
51
+
52
+ def add_paragraph(self):
53
+ """
54
+ Return new |_Paragraph| instance appended to the sequence of
55
+ paragraphs contained in this text frame.
56
+ """
57
+ p = self._txBody.add_p()
58
+ return _Paragraph(p, self)
59
+
60
+ @property
61
+ def auto_size(self) -> MSO_AUTO_SIZE | None:
62
+ """Resizing strategy used to fit text within this shape.
63
+
64
+ Determins the type of automatic resizing used to fit the text of this shape within its
65
+ bounding box when the text would otherwise extend beyond the shape boundaries. May be
66
+ |None|, `MSO_AUTO_SIZE.NONE`, `MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT`, or
67
+ `MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE`.
68
+ """
69
+ return self._bodyPr.autofit
70
+
71
+ @auto_size.setter
72
+ def auto_size(self, value: MSO_AUTO_SIZE | None):
73
+ self._bodyPr.autofit = value
74
+
75
+ def clear(self):
76
+ """Remove all paragraphs except one empty one."""
77
+ for p in self._txBody.p_lst[1:]:
78
+ self._txBody.remove(p)
79
+ p = self.paragraphs[0]
80
+ p.clear()
81
+
82
+ def fit_text(
83
+ self,
84
+ font_family: str = "Calibri",
85
+ max_size: int = 18,
86
+ bold: bool = False,
87
+ italic: bool = False,
88
+ font_file: str | None = None,
89
+ ):
90
+ """Fit text-frame text entirely within bounds of its shape.
91
+
92
+ Make the text in this text frame fit entirely within the bounds of its shape by setting
93
+ word wrap on and applying the "best-fit" font size to all the text it contains.
94
+
95
+ :attr:`TextFrame.auto_size` is set to :attr:`MSO_AUTO_SIZE.NONE`. The font size will not
96
+ be set larger than `max_size` points. If the path to a matching TrueType font is provided
97
+ as `font_file`, that font file will be used for the font metrics. If `font_file` is |None|,
98
+ best efforts are made to locate a font file with matchhing `font_family`, `bold`, and
99
+ `italic` installed on the current system (usually succeeds if the font is installed).
100
+ """
101
+ # ---no-op when empty as fit behavior not defined for that case---
102
+ if self.text == "":
103
+ return # pragma: no cover
104
+
105
+ font_size = self._best_fit_font_size(font_family, max_size, bold, italic, font_file)
106
+ self._apply_fit(font_family, font_size, bold, italic)
107
+
108
+ @property
109
+ def margin_bottom(self) -> Length:
110
+ """|Length| value representing the inset of text from the bottom text frame border.
111
+
112
+ :meth:`pptx.util.Inches` provides a convenient way of setting the value, e.g.
113
+ `text_frame.margin_bottom = Inches(0.05)`.
114
+ """
115
+ return self._bodyPr.bIns
116
+
117
+ @margin_bottom.setter
118
+ def margin_bottom(self, emu: Length):
119
+ self._bodyPr.bIns = emu
120
+
121
+ @property
122
+ def margin_left(self) -> Length:
123
+ """Inset of text from left text frame border as |Length| value."""
124
+ return self._bodyPr.lIns
125
+
126
+ @margin_left.setter
127
+ def margin_left(self, emu: Length):
128
+ self._bodyPr.lIns = emu
129
+
130
+ @property
131
+ def margin_right(self) -> Length:
132
+ """Inset of text from right text frame border as |Length| value."""
133
+ return self._bodyPr.rIns
134
+
135
+ @margin_right.setter
136
+ def margin_right(self, emu: Length):
137
+ self._bodyPr.rIns = emu
138
+
139
+ @property
140
+ def margin_top(self) -> Length:
141
+ """Inset of text from top text frame border as |Length| value."""
142
+ return self._bodyPr.tIns
143
+
144
+ @margin_top.setter
145
+ def margin_top(self, emu: Length):
146
+ self._bodyPr.tIns = emu
147
+
148
+ @property
149
+ def paragraphs(self) -> tuple[_Paragraph, ...]:
150
+ """Sequence of paragraphs in this text frame.
151
+
152
+ A text frame always contains at least one paragraph.
153
+ """
154
+ return tuple([_Paragraph(p, self) for p in self._txBody.p_lst])
155
+
156
+ @property
157
+ def text(self) -> str:
158
+ """All text in this text-frame as a single string.
159
+
160
+ Read/write. The return value contains all text in this text-frame. A line-feed character
161
+ (`"\\n"`) separates the text for each paragraph. A vertical-tab character (`"\\v"`) appears
162
+ for each line break (aka. soft carriage-return) encountered.
163
+
164
+ The vertical-tab character is how PowerPoint represents a soft carriage return in clipboard
165
+ text, which is why that encoding was chosen.
166
+
167
+ Assignment replaces all text in the text frame. A new paragraph is added for each line-feed
168
+ character (`"\\n"`) encountered. A line-break (soft carriage-return) is inserted for each
169
+ vertical-tab character (`"\\v"`) encountered.
170
+
171
+ Any control character other than newline, tab, or vertical-tab are escaped as plain-text
172
+ like "_x001B_" (for ESC (ASCII 32) in this example).
173
+ """
174
+ return "\n".join(paragraph.text for paragraph in self.paragraphs)
175
+
176
+ @text.setter
177
+ def text(self, text: str):
178
+ txBody = self._txBody
179
+ txBody.clear_content()
180
+ for p_text in text.split("\n"):
181
+ p = txBody.add_p()
182
+ p.append_text(p_text)
183
+
184
+ @property
185
+ def vertical_anchor(self) -> MSO_VERTICAL_ANCHOR | None:
186
+ """Represents the vertical alignment of text in this text frame.
187
+
188
+ |None| indicates the effective value should be inherited from this object's style hierarchy.
189
+ """
190
+ return self._txBody.bodyPr.anchor
191
+
192
+ @vertical_anchor.setter
193
+ def vertical_anchor(self, value: MSO_VERTICAL_ANCHOR | None):
194
+ bodyPr = self._txBody.bodyPr
195
+ bodyPr.anchor = value
196
+
197
+ @property
198
+ def word_wrap(self) -> bool | None:
199
+ """`True` when lines of text in this shape are wrapped to fit within the shape's width.
200
+
201
+ Read-write. Valid values are True, False, or None. True and False turn word wrap on and
202
+ off, respectively. Assigning None to word wrap causes any word wrap setting to be removed
203
+ from the text frame, causing it to inherit this setting from its style hierarchy.
204
+ """
205
+ return {
206
+ ST_TextWrappingType.SQUARE: True,
207
+ ST_TextWrappingType.NONE: False,
208
+ None: None,
209
+ }[self._txBody.bodyPr.wrap]
210
+
211
+ @word_wrap.setter
212
+ def word_wrap(self, value: bool | None):
213
+ if value not in (True, False, None):
214
+ raise ValueError( # pragma: no cover
215
+ "assigned value must be True, False, or None, got %s" % value
216
+ )
217
+ self._txBody.bodyPr.wrap = {
218
+ True: ST_TextWrappingType.SQUARE,
219
+ False: ST_TextWrappingType.NONE,
220
+ None: None,
221
+ }[value]
222
+
223
+ def _apply_fit(self, font_family: str, font_size: int, is_bold: bool, is_italic: bool):
224
+ """Arrange text in this text frame to fit inside its extents.
225
+
226
+ This is accomplished by setting auto size off, wrap on, and setting the font of
227
+ all its text to `font_family`, `font_size`, `is_bold`, and `is_italic`.
228
+ """
229
+ self.auto_size = MSO_AUTO_SIZE.NONE
230
+ self.word_wrap = True
231
+ self._set_font(font_family, font_size, is_bold, is_italic)
232
+
233
+ def _best_fit_font_size(
234
+ self, family: str, max_size: int, bold: bool, italic: bool, font_file: str | None
235
+ ) -> int:
236
+ """Return font-size in points that best fits text in this text-frame.
237
+
238
+ The best-fit font size is the largest integer point size not greater than `max_size` that
239
+ allows all the text in this text frame to fit inside its extents when rendered using the
240
+ font described by `family`, `bold`, and `italic`. If `font_file` is specified, it is used
241
+ to calculate the fit, whether or not it matches `family`, `bold`, and `italic`.
242
+ """
243
+ if font_file is None:
244
+ font_file = FontFiles.find(family, bold, italic)
245
+ return TextFitter.best_fit_font_size(self.text, self._extents, max_size, font_file)
246
+
247
+ @property
248
+ def _bodyPr(self):
249
+ return self._txBody.bodyPr
250
+
251
+ @property
252
+ def _extents(self) -> tuple[Length, Length]:
253
+ """(cx, cy) 2-tuple representing the effective rendering area of this text-frame.
254
+
255
+ Margins are taken into account.
256
+ """
257
+ parent = cast("ProvidesExtents", self._parent)
258
+ return (
259
+ Length(parent.width - self.margin_left - self.margin_right),
260
+ Length(parent.height - self.margin_top - self.margin_bottom),
261
+ )
262
+
263
+ def _set_font(self, family: str, size: int, bold: bool, italic: bool):
264
+ """Set the font properties of all the text in this text frame."""
265
+
266
+ def iter_rPrs(txBody: CT_TextBody) -> Iterator[CT_TextCharacterProperties]:
267
+ for p in txBody.p_lst:
268
+ for elm in p.content_children:
269
+ yield elm.get_or_add_rPr()
270
+ # generate a:endParaRPr for each <a:p> element
271
+ yield p.get_or_add_endParaRPr()
272
+
273
+ def set_rPr_font(
274
+ rPr: CT_TextCharacterProperties, name: str, size: int, bold: bool, italic: bool
275
+ ):
276
+ f = Font(rPr)
277
+ f.name, f.size, f.bold, f.italic = family, Pt(size), bold, italic
278
+
279
+ txBody = self._element
280
+ for rPr in iter_rPrs(txBody):
281
+ set_rPr_font(rPr, family, size, bold, italic)
282
+
283
+
284
+ class Font(object):
285
+ """Character properties object, providing font size, font name, bold, italic, etc.
286
+
287
+ Corresponds to `a:rPr` child element of a run. Also appears as `a:defRPr` and
288
+ `a:endParaRPr` in paragraph and `a:defRPr` in list style elements.
289
+ """
290
+
291
+ def __init__(self, rPr: CT_TextCharacterProperties):
292
+ super(Font, self).__init__()
293
+ self._element = self._rPr = rPr
294
+
295
+ @property
296
+ def baseline(self) -> float | None:
297
+ """Baseline offset as a fraction of font height. None inherits, zero resets.
298
+
299
+ Positive values raise text. Negative values lower text. Font size is independent.
300
+ """
301
+ return self._rPr.baseline
302
+
303
+ @baseline.setter
304
+ def baseline(self, value: float | None):
305
+ self._rPr.baseline = value
306
+
307
+ @property
308
+ def strike(self):
309
+ """MSO_TEXT_STRIKE_TYPE value, or None to inherit strikethrough."""
310
+ return self._rPr.strike
311
+
312
+ @strike.setter
313
+ def strike(self, value):
314
+ self._rPr.strike = value
315
+
316
+ @property
317
+ def theme_font(self):
318
+ """Major/minor theme font selection for Latin, East Asian, and complex scripts.
319
+
320
+ None indicates no complete theme selection. Assigning None clears these
321
+ three explicit typeface settings and restores inheritance.
322
+ """
323
+ return self._rPr.theme_font
324
+
325
+ @theme_font.setter
326
+ def theme_font(self, value):
327
+ self._rPr.theme_font = value
328
+
329
+ @property
330
+ def bold(self) -> bool | None:
331
+ """Get or set boolean bold value of |Font|, e.g. `paragraph.font.bold = True`.
332
+
333
+ If set to |None|, the bold setting is cleared and is inherited from an enclosing shape's
334
+ setting, or a setting in a style or master. Returns None if no bold attribute is present,
335
+ meaning the effective bold value is inherited from a master or the theme.
336
+ """
337
+ return self._rPr.b
338
+
339
+ @bold.setter
340
+ def bold(self, value: bool | None):
341
+ self._rPr.b = value
342
+
343
+ @lazyproperty
344
+ def color(self) -> ColorFormat:
345
+ """The |ColorFormat| instance that provides access to the color settings for this font."""
346
+ if self.fill.type != MSO_FILL.SOLID:
347
+ self.fill.solid()
348
+ return self.fill.fore_color
349
+
350
+ @lazyproperty
351
+ def fill(self) -> FillFormat:
352
+ """|FillFormat| instance for this font.
353
+
354
+ Provides access to fill properties such as fill color.
355
+ """
356
+ return FillFormat.from_fill_parent(self._rPr)
357
+
358
+ @property
359
+ def italic(self) -> bool | None:
360
+ """Get or set boolean italic value of |Font| instance.
361
+
362
+ Has the same behaviors as bold with respect to None values.
363
+ """
364
+ return self._rPr.i
365
+
366
+ @italic.setter
367
+ def italic(self, value: bool | None):
368
+ self._rPr.i = value
369
+
370
+ @property
371
+ def language_id(self) -> MSO_LANGUAGE_ID | None:
372
+ """Get or set the language id of this |Font| instance.
373
+
374
+ The language id is a member of the :ref:`MsoLanguageId` enumeration. Assigning |None|
375
+ removes any language setting, the same behavior as assigning `MSO_LANGUAGE_ID.NONE`.
376
+ """
377
+ lang = self._rPr.lang
378
+ if lang is None:
379
+ return MSO_LANGUAGE_ID.NONE
380
+ return self._rPr.lang
381
+
382
+ @language_id.setter
383
+ def language_id(self, value: MSO_LANGUAGE_ID | None):
384
+ if value == MSO_LANGUAGE_ID.NONE:
385
+ value = None
386
+ self._rPr.lang = value
387
+
388
+ @property
389
+ def name(self) -> str | None:
390
+ """Get or set the typeface name for this |Font| instance.
391
+
392
+ Causes the text it controls to appear in the named font, if a matching font is found.
393
+ Returns |None| if the typeface is currently inherited from the theme. Setting it to |None|
394
+ removes any override of the theme typeface.
395
+ """
396
+ latin = self._rPr.latin
397
+ if latin is None:
398
+ return None
399
+ return latin.typeface
400
+
401
+ @name.setter
402
+ def name(self, value: str | None):
403
+ if value is None:
404
+ self._rPr._remove_latin() # pyright: ignore[reportPrivateUsage]
405
+ else:
406
+ latin = self._rPr.get_or_add_latin()
407
+ latin.typeface = value
408
+
409
+ @property
410
+ def size(self) -> Length | None:
411
+ """Indicates the font height in English Metric Units (EMU).
412
+
413
+ Read/write. |None| indicates the font size should be inherited from its style hierarchy,
414
+ such as a placeholder or document defaults (usually 18pt). |Length| is a subclass of |int|
415
+ having properties for convenient conversion into points or other length units. Likewise,
416
+ the :class:`pptx.util.Pt` class allows convenient specification of point values::
417
+
418
+ >>> font.size = Pt(24)
419
+ >>> font.size
420
+ 304800
421
+ >>> font.size.pt
422
+ 24.0
423
+ """
424
+ sz = self._rPr.sz
425
+ if sz is None:
426
+ return None
427
+ return Centipoints(sz)
428
+
429
+ @size.setter
430
+ def size(self, emu: Length | None):
431
+ if emu is None:
432
+ self._rPr.sz = None
433
+ else:
434
+ sz = Emu(emu).centipoints
435
+ self._rPr.sz = sz
436
+
437
+ @property
438
+ def underline(self) -> bool | MSO_TEXT_UNDERLINE_TYPE | None:
439
+ """Indicaties the underline setting for this font.
440
+
441
+ Value is |True|, |False|, |None|, or a member of the :ref:`MsoTextUnderlineType`
442
+ enumeration. |None| is the default and indicates the underline setting should be inherited
443
+ from the style hierarchy, such as from a placeholder. |True| indicates single underline.
444
+ |False| indicates no underline. Other settings such as double and wavy underlining are
445
+ indicated with members of the :ref:`MsoTextUnderlineType` enumeration.
446
+ """
447
+ u = self._rPr.u
448
+ if u is MSO_UNDERLINE.NONE:
449
+ return False
450
+ if u is MSO_UNDERLINE.SINGLE_LINE:
451
+ return True
452
+ return u
453
+
454
+ @underline.setter
455
+ def underline(self, value: bool | MSO_TEXT_UNDERLINE_TYPE | None):
456
+ if value is True:
457
+ value = MSO_UNDERLINE.SINGLE_LINE
458
+ elif value is False:
459
+ value = MSO_UNDERLINE.NONE
460
+ self._element.u = value
461
+
462
+
463
+ class _Hyperlink(Subshape):
464
+ """Text run hyperlink object.
465
+
466
+ Corresponds to `a:hlinkClick` child element of the run's properties element (`a:rPr`).
467
+ """
468
+
469
+ def __init__(self, rPr: CT_TextCharacterProperties, parent: ProvidesPart):
470
+ super(_Hyperlink, self).__init__(parent)
471
+ self._rPr = rPr
472
+
473
+ @property
474
+ def address(self) -> str | None:
475
+ """The URL of the hyperlink.
476
+
477
+ Read/write. URL can be on http, https, mailto, or file scheme; others may work.
478
+ """
479
+ if self._hlinkClick is None:
480
+ return None
481
+ return self.part.target_ref(self._hlinkClick.rId)
482
+
483
+ @address.setter
484
+ def address(self, url: str | None):
485
+ # implements all three of add, change, and remove hyperlink
486
+ if self._hlinkClick is not None:
487
+ self._remove_hlinkClick()
488
+ if url:
489
+ self._add_hlinkClick(url)
490
+
491
+ def _add_hlinkClick(self, url: str):
492
+ rId = self.part.relate_to(url, RT.HYPERLINK, is_external=True)
493
+ self._rPr.add_hlinkClick(rId)
494
+
495
+ @property
496
+ def _hlinkClick(self) -> CT_Hyperlink | None:
497
+ return self._rPr.hlinkClick
498
+
499
+ def _remove_hlinkClick(self):
500
+ assert self._hlinkClick is not None
501
+ self.part.drop_rel(self._hlinkClick.rId)
502
+ self._rPr._remove_hlinkClick() # pyright: ignore[reportPrivateUsage]
503
+
504
+
505
+ class _Paragraph(Subshape):
506
+ """Paragraph object. Not intended to be constructed directly."""
507
+
508
+ def __init__(self, p: CT_TextParagraph, parent: ProvidesPart):
509
+ super(_Paragraph, self).__init__(parent)
510
+ self._element = self._p = p
511
+
512
+ def add_line_break(self):
513
+ """Add line break at end of this paragraph."""
514
+ self._p.add_br()
515
+
516
+ def add_run(self) -> _Run:
517
+ """Return a new run appended to the runs in this paragraph."""
518
+ r = self._p.add_r()
519
+ return _Run(r, self)
520
+
521
+ @property
522
+ def alignment(self) -> PP_PARAGRAPH_ALIGNMENT | None:
523
+ """Horizontal alignment of this paragraph.
524
+
525
+ The value |None| indicates the paragraph should 'inherit' its effective value from its
526
+ style hierarchy. Assigning |None| removes any explicit setting, causing its inherited
527
+ value to be used.
528
+ """
529
+ return self._pPr.algn
530
+
531
+ @alignment.setter
532
+ def alignment(self, value: PP_PARAGRAPH_ALIGNMENT | None):
533
+ self._pPr.algn = value
534
+
535
+ def clear(self):
536
+ """Remove all content from this paragraph.
537
+
538
+ Paragraph properties are preserved. Content includes runs, line breaks, and fields.
539
+ """
540
+ for elm in self._element.content_children:
541
+ self._element.remove(elm)
542
+ return self
543
+
544
+ @property
545
+ def font(self) -> Font:
546
+ """|Font| object containing default character properties for the runs in this paragraph.
547
+
548
+ These character properties override default properties inherited from parent objects such
549
+ as the text frame the paragraph is contained in and they may be overridden by character
550
+ properties set at the run level.
551
+ """
552
+ return Font(self._defRPr)
553
+
554
+ @property
555
+ def level(self) -> int:
556
+ """Indentation level of this paragraph.
557
+
558
+ Read-write. Integer in range 0..8 inclusive. 0 represents a top-level paragraph and is the
559
+ default value. Indentation level is most commonly encountered in a bulleted list, as is
560
+ found on a word bullet slide.
561
+ """
562
+ return self._pPr.lvl
563
+
564
+ @level.setter
565
+ def level(self, level: int):
566
+ self._pPr.lvl = level
567
+
568
+ @property
569
+ def line_spacing(self) -> int | float | Length | None:
570
+ """The space between baselines in successive lines of this paragraph.
571
+
572
+ A value of |None| indicates no explicit value is assigned and its effective value is
573
+ inherited from the paragraph's style hierarchy. A numeric value, e.g. `2` or `1.5`,
574
+ indicates spacing is applied in multiples of line heights. A |Length| value such as
575
+ `Pt(12)` indicates spacing is a fixed height. The |Pt| value class is a convenient way to
576
+ apply line spacing in units of points.
577
+ """
578
+ pPr = self._p.pPr
579
+ if pPr is None:
580
+ return None
581
+ return pPr.line_spacing
582
+
583
+ @line_spacing.setter
584
+ def line_spacing(self, value: int | float | Length | None):
585
+ pPr = self._p.get_or_add_pPr()
586
+ pPr.line_spacing = value
587
+
588
+ @property
589
+ def left_indent(self):
590
+ """Left paragraph margin as a Length, or None to inherit."""
591
+ value = self._pPr.marL
592
+ return None if value is None else Emu(value)
593
+
594
+ @left_indent.setter
595
+ def left_indent(self, value):
596
+ if value is not None and not 0 <= value <= 51206400:
597
+ raise ValueError("left_indent must be between 0 and 51206400 EMU")
598
+ self._pPr.marL = value
599
+
600
+ @property
601
+ def first_line_indent(self):
602
+ """First-line offset as a Length. Negative values create a hanging indent."""
603
+ value = self._pPr.indent
604
+ return None if value is None else Emu(value)
605
+
606
+ @first_line_indent.setter
607
+ def first_line_indent(self, value):
608
+ if value is not None and not -51206400 <= value <= 51206400:
609
+ raise ValueError("first_line_indent must be within +/-51206400 EMU")
610
+ self._pPr.indent = value
611
+
612
+ @property
613
+ def end_font(self) -> Font:
614
+ """Character formatting of the paragraph mark, including an empty paragraph."""
615
+ return Font(self._p.get_or_add_endParaRPr())
616
+
617
+ @property
618
+ def bullet(self) -> BulletStyle:
619
+ """The type of bullet, if any, defined for this paragraph.
620
+
621
+ ``BulletStyle.NO_BULLET`` indicates that bullets are explicitly disabled
622
+ a paragraph. ``BulletStyle.DEFAULT`` indicates that whether the paragraph
623
+ is rendered as a bullet is defined in the slide master or layout.
624
+
625
+ The methods ``BulletStyle.custom`` and ``BulletStyle.numbered`` can be
626
+ used to create ``BulletStyle``s that control what kind of bullet is used
627
+ for the paragraph.
628
+ """
629
+ pPr = self._p.pPr
630
+ if pPr is None:
631
+ return BulletStyle.DEFAULT
632
+ return pPr.bullet
633
+
634
+ @bullet.setter
635
+ def bullet(self, value: BulletStyle):
636
+ pPr = self._p.get_or_add_pPr()
637
+ pPr.bullet = value
638
+
639
+ @property
640
+ def runs(self) -> tuple[_Run, ...]:
641
+ """Sequence of runs in this paragraph."""
642
+ return tuple(_Run(r, self) for r in self._element.r_lst)
643
+
644
+ @property
645
+ def space_after(self) -> Length | None:
646
+ """The spacing to appear between this paragraph and the subsequent paragraph.
647
+
648
+ A value of |None| indicates no explicit value is assigned and its effective value is
649
+ inherited from the paragraph's style hierarchy. |Length| objects provide convenience
650
+ properties, such as `.pt` and `.inches`, that allow easy conversion to various length
651
+ units.
652
+ """
653
+ pPr = self._p.pPr
654
+ if pPr is None:
655
+ return None
656
+ return pPr.space_after
657
+
658
+ @space_after.setter
659
+ def space_after(self, value: Length | None):
660
+ pPr = self._p.get_or_add_pPr()
661
+ pPr.space_after = value
662
+
663
+ @property
664
+ def space_before(self) -> Length | None:
665
+ """The spacing to appear between this paragraph and the prior paragraph.
666
+
667
+ A value of |None| indicates no explicit value is assigned and its effective value is
668
+ inherited from the paragraph's style hierarchy. |Length| objects provide convenience
669
+ properties, such as `.pt` and `.cm`, that allow easy conversion to various length units.
670
+ """
671
+ pPr = self._p.pPr
672
+ if pPr is None:
673
+ return None
674
+ return pPr.space_before
675
+
676
+ @space_before.setter
677
+ def space_before(self, value: Length | None):
678
+ pPr = self._p.get_or_add_pPr()
679
+ pPr.space_before = value
680
+
681
+ @property
682
+ def text(self) -> str:
683
+ """Text of paragraph as a single string.
684
+
685
+ Read/write. This value is formed by concatenating the text in each run and field making up
686
+ the paragraph, adding a vertical-tab character (`"\\v"`) for each line-break element
687
+ (`<a:br>`, soft carriage-return) encountered.
688
+
689
+ While the encoding of line-breaks as a vertical tab might be surprising at first, doing so
690
+ is consistent with PowerPoint's clipboard copy behavior and allows a line-break to be
691
+ distinguished from a paragraph boundary within the str return value.
692
+
693
+ Assignment causes all content in the paragraph to be replaced. Each vertical-tab character
694
+ (`"\\v"`) in the assigned str is translated to a line-break, as is each line-feed
695
+ character (`"\\n"`). Contrast behavior of line-feed character in `TextFrame.text` setter.
696
+ If line-feed characters are intended to produce new paragraphs, use `TextFrame.text`
697
+ instead. Any other control characters in the assigned string are escaped as a hex
698
+ representation like "_x001B_" (for ESC (ASCII 27) in this example).
699
+ """
700
+ return "".join(elm.text for elm in self._element.content_children)
701
+
702
+ @text.setter
703
+ def text(self, text: str):
704
+ self.clear()
705
+ self._element.append_text(text)
706
+
707
+ @property
708
+ def _defRPr(self) -> CT_TextCharacterProperties:
709
+ """The element that defines the default run properties for runs in this paragraph.
710
+
711
+ Causes the element to be added if not present.
712
+ """
713
+ return self._pPr.get_or_add_defRPr()
714
+
715
+ @property
716
+ def _pPr(self) -> CT_TextParagraphProperties:
717
+ """Contains the properties for this paragraph.
718
+
719
+ Causes the element to be added if not present.
720
+ """
721
+ return self._p.get_or_add_pPr()
722
+
723
+
724
+ class _Run(Subshape):
725
+ """Text run object. Corresponds to `a:r` child element in a paragraph."""
726
+
727
+ def __init__(self, r: CT_RegularTextRun, parent: ProvidesPart):
728
+ super(_Run, self).__init__(parent)
729
+ self._r = r
730
+
731
+ @property
732
+ def font(self):
733
+ """|Font| instance containing run-level character properties for the text in this run.
734
+
735
+ Character properties can be and perhaps most often are inherited from parent objects such
736
+ as the paragraph and slide layout the run is contained in. Only those specifically
737
+ overridden at the run level are contained in the font object.
738
+ """
739
+ rPr = self._r.get_or_add_rPr()
740
+ return Font(rPr)
741
+
742
+ @lazyproperty
743
+ def hyperlink(self) -> _Hyperlink:
744
+ """Proxy for any `a:hlinkClick` element under the run properties element.
745
+
746
+ Created on demand, the hyperlink object is available whether an `a:hlinkClick` element is
747
+ present or not, and creates or deletes that element as appropriate in response to actions
748
+ on its methods and attributes.
749
+ """
750
+ rPr = self._r.get_or_add_rPr()
751
+ return _Hyperlink(rPr, self)
752
+
753
+ @property
754
+ def text(self):
755
+ """Read/write. A unicode string containing the text in this run.
756
+
757
+ Assignment replaces all text in the run. The assigned value can be a 7-bit ASCII
758
+ string, a UTF-8 encoded 8-bit string, or unicode. String values are converted to
759
+ unicode assuming UTF-8 encoding.
760
+
761
+ Any other control characters in the assigned string other than tab or newline
762
+ are escaped as a hex representation. For example, ESC (ASCII 27) is escaped as
763
+ "_x001B_". Contrast the behavior of `TextFrame.text` and `_Paragraph.text` with
764
+ respect to line-feed and vertical-tab characters.
765
+ """
766
+ return self._r.text
767
+
768
+ @text.setter
769
+ def text(self, text: str):
770
+ self._r.text = text