aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/pptx
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx')
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/__init__.py82
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/action.py270
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/api.py49
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/axis.py523
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/category.py200
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/chart.py280
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/data.py864
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/datalabel.py288
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/legend.py79
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/marker.py70
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/plot.py412
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/point.py101
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/series.py258
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py272
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/chart/xmlwriter.py1840
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/dml/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/dml/chtfmt.py40
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/dml/color.py301
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/dml/effect.py41
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/dml/fill.py398
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/dml/line.py100
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/enum/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/enum/action.py71
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/enum/base.py175
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/enum/chart.py492
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/enum/dml.py405
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/enum/lang.py685
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/enum/shapes.py1029
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/enum/text.py230
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/exc.py23
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/media.py197
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/opc/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/opc/constants.py331
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/opc/oxml.py188
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/opc/package.py762
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/opc/packuri.py109
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/opc/serialized.py296
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/opc/shared.py20
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/opc/spec.py44
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/__init__.py486
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/action.py53
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/axis.py297
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/chart.py282
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/datalabel.py252
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/legend.py72
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/marker.py61
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/plot.py345
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/series.py254
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/chart/shared.py219
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/coreprops.py288
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/dml/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/dml/color.py111
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/dml/fill.py197
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/dml/line.py12
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/ns.py129
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/presentation.py130
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/__init__.py19
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/autoshape.py455
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/connector.py107
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/graphfrm.py342
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/groupshape.py280
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/picture.py270
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/shapes/shared.py523
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/simpletypes.py740
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/slide.py347
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/table.py588
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/text.py618
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/theme.py29
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/oxml/xmlchemy.py717
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/package.py222
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/chart.py95
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/coreprops.py167
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/embeddedpackage.py93
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/image.py275
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/media.py37
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/presentation.py126
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/parts/slide.py297
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/presentation.py113
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/py.typed0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/__init__.py26
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/autoshape.py355
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/base.py244
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/connector.py297
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/freeform.py337
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/graphfrm.py166
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/group.py69
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/picture.py203
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/placeholder.py407
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/shapetree.py1190
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shared.py82
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/slide.py498
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/spec.py632
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/table.py496
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/templates/default.pptxbin0 -> 34030 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/templates/docx-icon.emfbin0 -> 127228 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/templates/generic-icon.emfbin0 -> 5396 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/templates/notes.xml23
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/templates/notesMaster.xml352
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/templates/pptx-icon.emfbin0 -> 5492 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/templates/theme.xml321
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/templates/xlsx-icon.emfbin0 -> 130380 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/text/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/text/fonts.py399
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/text/layout.py325
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/text/text.py681
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/types.py36
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/util.py214
110 files changed, 28526 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pptx/__init__.py b/.venv/lib/python3.12/site-packages/pptx/__init__.py
new file mode 100644
index 00000000..fb5c2d7e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/__init__.py
@@ -0,0 +1,82 @@
+"""Initialization module for python-pptx package."""
+
+from __future__ import annotations
+
+import sys
+from typing import TYPE_CHECKING
+
+import pptx.exc as exceptions
+from pptx.api import Presentation
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.opc.package import PartFactory
+from pptx.parts.chart import ChartPart
+from pptx.parts.coreprops import CorePropertiesPart
+from pptx.parts.image import ImagePart
+from pptx.parts.media import MediaPart
+from pptx.parts.presentation import PresentationPart
+from pptx.parts.slide import (
+ NotesMasterPart,
+ NotesSlidePart,
+ SlideLayoutPart,
+ SlideMasterPart,
+ SlidePart,
+)
+
+if TYPE_CHECKING:
+ from pptx.opc.package import Part
+
+__version__ = "1.0.2"
+
+sys.modules["pptx.exceptions"] = exceptions
+del sys
+
+__all__ = ["Presentation"]
+
+content_type_to_part_class_map: dict[str, type[Part]] = {
+ CT.PML_PRESENTATION_MAIN: PresentationPart,
+ CT.PML_PRES_MACRO_MAIN: PresentationPart,
+ CT.PML_TEMPLATE_MAIN: PresentationPart,
+ CT.PML_SLIDESHOW_MAIN: PresentationPart,
+ CT.OPC_CORE_PROPERTIES: CorePropertiesPart,
+ CT.PML_NOTES_MASTER: NotesMasterPart,
+ CT.PML_NOTES_SLIDE: NotesSlidePart,
+ CT.PML_SLIDE: SlidePart,
+ CT.PML_SLIDE_LAYOUT: SlideLayoutPart,
+ CT.PML_SLIDE_MASTER: SlideMasterPart,
+ CT.DML_CHART: ChartPart,
+ CT.BMP: ImagePart,
+ CT.GIF: ImagePart,
+ CT.JPEG: ImagePart,
+ CT.MS_PHOTO: ImagePart,
+ CT.PNG: ImagePart,
+ CT.TIFF: ImagePart,
+ CT.X_EMF: ImagePart,
+ CT.X_WMF: ImagePart,
+ CT.ASF: MediaPart,
+ CT.AVI: MediaPart,
+ CT.MOV: MediaPart,
+ CT.MP4: MediaPart,
+ CT.MPG: MediaPart,
+ CT.MS_VIDEO: MediaPart,
+ CT.SWF: MediaPart,
+ CT.VIDEO: MediaPart,
+ CT.WMV: MediaPart,
+ CT.X_MS_VIDEO: MediaPart,
+ # -- accommodate "image/jpg" as an alias for "image/jpeg" --
+ "image/jpg": ImagePart,
+}
+
+PartFactory.part_type_for.update(content_type_to_part_class_map)
+
+del (
+ ChartPart,
+ CorePropertiesPart,
+ ImagePart,
+ MediaPart,
+ SlidePart,
+ SlideLayoutPart,
+ SlideMasterPart,
+ PresentationPart,
+ CT,
+ PartFactory,
+)
diff --git a/.venv/lib/python3.12/site-packages/pptx/action.py b/.venv/lib/python3.12/site-packages/pptx/action.py
new file mode 100644
index 00000000..83c6ebf1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/action.py
@@ -0,0 +1,270 @@
+"""Objects related to mouse click and hover actions on a shape or text."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+
+from pptx.enum.action import PP_ACTION
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.shapes import Subshape
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.oxml.action import CT_Hyperlink
+ from pptx.oxml.shapes.shared import CT_NonVisualDrawingProps
+ from pptx.oxml.text import CT_TextCharacterProperties
+ from pptx.parts.slide import SlidePart
+ from pptx.shapes.base import BaseShape
+ from pptx.slide import Slide, Slides
+
+
+class ActionSetting(Subshape):
+ """Properties specifying how a shape or run reacts to mouse actions."""
+
+ # -- The Subshape base class provides access to the Slide Part, which is needed to access
+ # -- relationships, which is where hyperlinks live.
+
+ def __init__(
+ self,
+ xPr: CT_NonVisualDrawingProps | CT_TextCharacterProperties,
+ parent: BaseShape,
+ hover: bool = False,
+ ):
+ super(ActionSetting, self).__init__(parent)
+ # xPr is either a cNvPr or rPr element
+ self._element = xPr
+ # _hover determines use of `a:hlinkClick` or `a:hlinkHover`
+ self._hover = hover
+
+ @property
+ def action(self):
+ """Member of :ref:`PpActionType` enumeration, such as `PP_ACTION.HYPERLINK`.
+
+ The returned member indicates the type of action that will result when the
+ specified shape or text is clicked or the mouse pointer is positioned over the
+ shape during a slide show.
+
+ If there is no click-action or the click-action value is not recognized (is not
+ one of the official `MsoPpAction` values) then `PP_ACTION.NONE` is returned.
+ """
+ hlink = self._hlink
+
+ if hlink is None:
+ return PP_ACTION.NONE
+
+ action_verb = hlink.action_verb
+
+ if action_verb == "hlinkshowjump":
+ relative_target = hlink.action_fields["jump"]
+ return {
+ "firstslide": PP_ACTION.FIRST_SLIDE,
+ "lastslide": PP_ACTION.LAST_SLIDE,
+ "lastslideviewed": PP_ACTION.LAST_SLIDE_VIEWED,
+ "nextslide": PP_ACTION.NEXT_SLIDE,
+ "previousslide": PP_ACTION.PREVIOUS_SLIDE,
+ "endshow": PP_ACTION.END_SHOW,
+ }[relative_target]
+
+ return {
+ None: PP_ACTION.HYPERLINK,
+ "hlinksldjump": PP_ACTION.NAMED_SLIDE,
+ "hlinkpres": PP_ACTION.PLAY,
+ "hlinkfile": PP_ACTION.OPEN_FILE,
+ "customshow": PP_ACTION.NAMED_SLIDE_SHOW,
+ "ole": PP_ACTION.OLE_VERB,
+ "macro": PP_ACTION.RUN_MACRO,
+ "program": PP_ACTION.RUN_PROGRAM,
+ }.get(action_verb, PP_ACTION.NONE)
+
+ @lazyproperty
+ def hyperlink(self) -> Hyperlink:
+ """
+ A |Hyperlink| object representing the hyperlink action defined on
+ this click or hover mouse event. A |Hyperlink| object is always
+ returned, even if no hyperlink or other click action is defined.
+ """
+ return Hyperlink(self._element, self._parent, self._hover)
+
+ @property
+ def target_slide(self) -> Slide | None:
+ """
+ A reference to the slide in this presentation that is the target of
+ the slide jump action in this shape. Slide jump actions include
+ `PP_ACTION.FIRST_SLIDE`, `LAST_SLIDE`, `NEXT_SLIDE`,
+ `PREVIOUS_SLIDE`, and `NAMED_SLIDE`. Returns |None| for all other
+ actions. In particular, the `LAST_SLIDE_VIEWED` action and the `PLAY`
+ (start other presentation) actions are not supported.
+
+ A slide object may be assigned to this property, which makes the
+ shape an "internal hyperlink" to the assigened slide::
+
+ slide, target_slide = prs.slides[0], prs.slides[1]
+ shape = slide.shapes[0]
+ shape.target_slide = target_slide
+
+ Assigning |None| removes any slide jump action. Note that this is
+ accomplished by removing any action present (such as a hyperlink),
+ without first checking that it is a slide jump action.
+ """
+ slide_jump_actions = (
+ PP_ACTION.FIRST_SLIDE,
+ PP_ACTION.LAST_SLIDE,
+ PP_ACTION.NEXT_SLIDE,
+ PP_ACTION.PREVIOUS_SLIDE,
+ PP_ACTION.NAMED_SLIDE,
+ )
+
+ if self.action not in slide_jump_actions:
+ return None
+
+ if self.action == PP_ACTION.FIRST_SLIDE:
+ return self._slides[0]
+ elif self.action == PP_ACTION.LAST_SLIDE:
+ return self._slides[-1]
+ elif self.action == PP_ACTION.NEXT_SLIDE:
+ next_slide_idx = self._slide_index + 1
+ if next_slide_idx >= len(self._slides):
+ raise ValueError("no next slide")
+ return self._slides[next_slide_idx]
+ elif self.action == PP_ACTION.PREVIOUS_SLIDE:
+ prev_slide_idx = self._slide_index - 1
+ if prev_slide_idx < 0:
+ raise ValueError("no previous slide")
+ return self._slides[prev_slide_idx]
+ elif self.action == PP_ACTION.NAMED_SLIDE:
+ assert self._hlink is not None
+ rId = self._hlink.rId
+ slide_part = cast("SlidePart", self.part.related_part(rId))
+ return slide_part.slide
+
+ @target_slide.setter
+ def target_slide(self, slide: Slide | None):
+ self._clear_click_action()
+ if slide is None:
+ return
+ hlink = self._element.get_or_add_hlinkClick()
+ hlink.action = "ppaction://hlinksldjump"
+ hlink.rId = self.part.relate_to(slide.part, RT.SLIDE)
+
+ def _clear_click_action(self):
+ """Remove any existing click action."""
+ hlink = self._hlink
+ if hlink is None:
+ return
+ rId = hlink.rId
+ if rId:
+ self.part.drop_rel(rId)
+ self._element.remove(hlink)
+
+ @property
+ def _hlink(self) -> CT_Hyperlink | None:
+ """
+ Reference to the `a:hlinkClick` or `a:hlinkHover` element for this
+ click action. Returns |None| if the element is not present.
+ """
+ if self._hover:
+ assert isinstance(self._element, CT_NonVisualDrawingProps)
+ return self._element.hlinkHover
+ return self._element.hlinkClick
+
+ @lazyproperty
+ def _slide(self):
+ """
+ Reference to the slide containing the shape having this click action.
+ """
+ return self.part.slide
+
+ @lazyproperty
+ def _slide_index(self):
+ """
+ Position in the slide collection of the slide containing the shape
+ having this click action.
+ """
+ return self._slides.index(self._slide)
+
+ @lazyproperty
+ def _slides(self) -> Slides:
+ """
+ Reference to the slide collection for this presentation.
+ """
+ return self.part.package.presentation_part.presentation.slides
+
+
+class Hyperlink(Subshape):
+ """Represents a hyperlink action on a shape or text run."""
+
+ def __init__(
+ self,
+ xPr: CT_NonVisualDrawingProps | CT_TextCharacterProperties,
+ parent: BaseShape,
+ hover: bool = False,
+ ):
+ super(Hyperlink, self).__init__(parent)
+ # xPr is either a cNvPr or rPr element
+ self._element = xPr
+ # _hover determines use of `a:hlinkClick` or `a:hlinkHover`
+ self._hover = hover
+
+ @property
+ def address(self) -> str | None:
+ """Read/write. The URL of the hyperlink.
+
+ URL can be on http, https, mailto, or file scheme; others may work. Returns |None| if no
+ hyperlink is defined, including when another action such as `RUN_MACRO` is defined on the
+ object. Assigning |None| removes any action defined on the object, whether it is a hyperlink
+ action or not.
+ """
+ hlink = self._hlink
+
+ # there's no URL if there's no click action
+ if hlink is None:
+ return None
+
+ # a click action without a relationship has no URL
+ rId = hlink.rId
+ if not rId:
+ return None
+
+ return self.part.target_ref(rId)
+
+ @address.setter
+ def address(self, url: str | None):
+ # implements all three of add, change, and remove hyperlink
+ self._remove_hlink()
+
+ if url:
+ rId = self.part.relate_to(url, RT.HYPERLINK, is_external=True)
+ hlink = self._get_or_add_hlink()
+ hlink.rId = rId
+
+ def _get_or_add_hlink(self) -> CT_Hyperlink:
+ """Get the `a:hlinkClick` or `a:hlinkHover` element for the Hyperlink object.
+
+ The actual element depends on the value of `self._hover`. Create the element if not present.
+ """
+ if self._hover:
+ return cast("CT_NonVisualDrawingProps", self._element).get_or_add_hlinkHover()
+ return self._element.get_or_add_hlinkClick()
+
+ @property
+ def _hlink(self) -> CT_Hyperlink | None:
+ """Reference to the `a:hlinkClick` or `h:hlinkHover` element for this click action.
+
+ Returns |None| if the element is not present.
+ """
+ if self._hover:
+ return cast("CT_NonVisualDrawingProps", self._element).hlinkHover
+ return self._element.hlinkClick
+
+ def _remove_hlink(self):
+ """Remove the a:hlinkClick or a:hlinkHover element.
+
+ Also drops any relationship it might have.
+ """
+ hlink = self._hlink
+ if hlink is None:
+ return
+ rId = hlink.rId
+ if rId:
+ self.part.drop_rel(rId)
+ self._element.remove(hlink)
diff --git a/.venv/lib/python3.12/site-packages/pptx/api.py b/.venv/lib/python3.12/site-packages/pptx/api.py
new file mode 100644
index 00000000..892f425a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/api.py
@@ -0,0 +1,49 @@
+"""Directly exposed API classes, Presentation for now.
+
+Provides some syntactic sugar for interacting with the pptx.presentation.Package graph and also
+provides some insulation so not so many classes in the other modules need to be named as internal
+(leading underscore).
+"""
+
+from __future__ import annotations
+
+import os
+from typing import IO, TYPE_CHECKING
+
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.package import Package
+
+if TYPE_CHECKING:
+ from pptx import presentation
+ from pptx.parts.presentation import PresentationPart
+
+
+def Presentation(pptx: str | IO[bytes] | None = None) -> presentation.Presentation:
+ """
+ Return a |Presentation| object loaded from *pptx*, where *pptx* can be
+ either a path to a ``.pptx`` file (a string) or a file-like object. If
+ *pptx* is missing or ``None``, the built-in default presentation
+ "template" is loaded.
+ """
+ if pptx is None:
+ pptx = _default_pptx_path()
+
+ presentation_part = Package.open(pptx).main_document_part
+
+ if not _is_pptx_package(presentation_part):
+ tmpl = "file '%s' is not a PowerPoint file, content type is '%s'"
+ raise ValueError(tmpl % (pptx, presentation_part.content_type))
+
+ return presentation_part.presentation
+
+
+def _default_pptx_path() -> str:
+ """Return the path to the built-in default .pptx package."""
+ _thisdir = os.path.split(__file__)[0]
+ return os.path.join(_thisdir, "templates", "default.pptx")
+
+
+def _is_pptx_package(prs_part: PresentationPart):
+ """Return |True| if *prs_part* is a valid main document part, |False| otherwise."""
+ valid_content_types = (CT.PML_PRESENTATION_MAIN, CT.PML_PRES_MACRO_MAIN)
+ return prs_part.content_type in valid_content_types
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/__init__.py b/.venv/lib/python3.12/site-packages/pptx/chart/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/axis.py b/.venv/lib/python3.12/site-packages/pptx/chart/axis.py
new file mode 100644
index 00000000..a9b87703
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/axis.py
@@ -0,0 +1,523 @@
+"""Axis-related chart objects."""
+
+from __future__ import annotations
+
+from pptx.dml.chtfmt import ChartFormat
+from pptx.enum.chart import (
+ XL_AXIS_CROSSES,
+ XL_CATEGORY_TYPE,
+ XL_TICK_LABEL_POSITION,
+ XL_TICK_MARK,
+)
+from pptx.oxml.ns import qn
+from pptx.oxml.simpletypes import ST_Orientation
+from pptx.shared import ElementProxy
+from pptx.text.text import Font, TextFrame
+from pptx.util import lazyproperty
+
+
+class _BaseAxis(object):
+ """Base class for chart axis objects. All axis objects share these properties."""
+
+ def __init__(self, xAx):
+ super(_BaseAxis, self).__init__()
+ self._element = xAx # axis element, c:catAx or c:valAx
+ self._xAx = xAx
+
+ @property
+ def axis_title(self):
+ """An |AxisTitle| object providing access to title properties.
+
+ Calling this property is destructive in the sense that it adds an
+ axis title element (`c:title`) to the axis XML if one is not already
+ present. Use :attr:`has_title` to test for presence of axis title
+ non-destructively.
+ """
+ return AxisTitle(self._element.get_or_add_title())
+
+ @lazyproperty
+ def format(self):
+ """
+ The |ChartFormat| object providing access to the shape formatting
+ properties of this axis, such as its line color and fill.
+ """
+ return ChartFormat(self._element)
+
+ @property
+ def has_major_gridlines(self):
+ """
+ Read/write boolean value specifying whether this axis has gridlines
+ at its major tick mark locations. Assigning |True| to this property
+ causes major gridlines to be displayed. Assigning |False| causes them
+ to be removed.
+ """
+ if self._element.majorGridlines is None:
+ return False
+ return True
+
+ @has_major_gridlines.setter
+ def has_major_gridlines(self, value):
+ if bool(value) is True:
+ self._element.get_or_add_majorGridlines()
+ else:
+ self._element._remove_majorGridlines()
+
+ @property
+ def has_minor_gridlines(self):
+ """
+ Read/write boolean value specifying whether this axis has gridlines
+ at its minor tick mark locations. Assigning |True| to this property
+ causes minor gridlines to be displayed. Assigning |False| causes them
+ to be removed.
+ """
+ if self._element.minorGridlines is None:
+ return False
+ return True
+
+ @has_minor_gridlines.setter
+ def has_minor_gridlines(self, value):
+ if bool(value) is True:
+ self._element.get_or_add_minorGridlines()
+ else:
+ self._element._remove_minorGridlines()
+
+ @property
+ def has_title(self):
+ """Read/write boolean specifying whether this axis has a title.
+
+ |True| if this axis has a title, |False| otherwise. Assigning |True|
+ causes an axis title to be added if not already present. Assigning
+ |False| causes any existing title to be deleted.
+ """
+ if self._element.title is None:
+ return False
+ return True
+
+ @has_title.setter
+ def has_title(self, value):
+ if bool(value) is True:
+ self._element.get_or_add_title()
+ else:
+ self._element._remove_title()
+
+ @lazyproperty
+ def major_gridlines(self):
+ """
+ The |MajorGridlines| object representing the major gridlines for
+ this axis.
+ """
+ return MajorGridlines(self._element)
+
+ @property
+ def major_tick_mark(self):
+ """
+ Read/write :ref:`XlTickMark` value specifying the type of major tick
+ mark to display on this axis.
+ """
+ majorTickMark = self._element.majorTickMark
+ if majorTickMark is None:
+ return XL_TICK_MARK.CROSS
+ return majorTickMark.val
+
+ @major_tick_mark.setter
+ def major_tick_mark(self, value):
+ self._element._remove_majorTickMark()
+ if value is XL_TICK_MARK.CROSS:
+ return
+ self._element._add_majorTickMark(val=value)
+
+ @property
+ def maximum_scale(self):
+ """
+ Read/write float value specifying the upper limit of the value range
+ for this axis, the number at the top or right of the vertical or
+ horizontal value scale, respectively. The value |None| indicates the
+ upper limit should be determined automatically based on the range of
+ data point values associated with the axis.
+ """
+ return self._element.scaling.maximum
+
+ @maximum_scale.setter
+ def maximum_scale(self, value):
+ scaling = self._element.scaling
+ scaling.maximum = value
+
+ @property
+ def minimum_scale(self):
+ """
+ Read/write float value specifying lower limit of value range, the
+ number at the bottom or left of the value scale. |None| if no minimum
+ scale has been set. The value |None| indicates the lower limit should
+ be determined automatically based on the range of data point values
+ associated with the axis.
+ """
+ return self._element.scaling.minimum
+
+ @minimum_scale.setter
+ def minimum_scale(self, value):
+ scaling = self._element.scaling
+ scaling.minimum = value
+
+ @property
+ def minor_tick_mark(self):
+ """
+ Read/write :ref:`XlTickMark` value specifying the type of minor tick
+ mark for this axis.
+ """
+ minorTickMark = self._element.minorTickMark
+ if minorTickMark is None:
+ return XL_TICK_MARK.CROSS
+ return minorTickMark.val
+
+ @minor_tick_mark.setter
+ def minor_tick_mark(self, value):
+ self._element._remove_minorTickMark()
+ if value is XL_TICK_MARK.CROSS:
+ return
+ self._element._add_minorTickMark(val=value)
+
+ @property
+ def reverse_order(self):
+ """Read/write bool value specifying whether to reverse plotting order for axis.
+
+ For a category axis, this reverses the order in which the categories are
+ displayed. This may be desired, for example, on a (horizontal) bar-chart where
+ by default the first category appears at the bottom. Since we read from
+ top-to-bottom, many viewers may find it most natural for the first category to
+ appear on top.
+
+ For a value axis, it reverses the direction of increasing value from
+ bottom-to-top to top-to-bottom.
+ """
+ return self._element.orientation == ST_Orientation.MAX_MIN
+
+ @reverse_order.setter
+ def reverse_order(self, value):
+ self._element.orientation = (
+ ST_Orientation.MAX_MIN if bool(value) is True else ST_Orientation.MIN_MAX
+ )
+
+ @lazyproperty
+ def tick_labels(self):
+ """
+ The |TickLabels| instance providing access to axis tick label
+ formatting properties. Tick labels are the numbers appearing on
+ a value axis or the category names appearing on a category axis.
+ """
+ return TickLabels(self._element)
+
+ @property
+ def tick_label_position(self):
+ """
+ Read/write :ref:`XlTickLabelPosition` value specifying where the tick
+ labels for this axis should appear.
+ """
+ tickLblPos = self._element.tickLblPos
+ if tickLblPos is None:
+ return XL_TICK_LABEL_POSITION.NEXT_TO_AXIS
+ if tickLblPos.val is None:
+ return XL_TICK_LABEL_POSITION.NEXT_TO_AXIS
+ return tickLblPos.val
+
+ @tick_label_position.setter
+ def tick_label_position(self, value):
+ tickLblPos = self._element.get_or_add_tickLblPos()
+ tickLblPos.val = value
+
+ @property
+ def visible(self):
+ """
+ Read/write. |True| if axis is visible, |False| otherwise.
+ """
+ delete = self._element.delete_
+ if delete is None:
+ return False
+ return False if delete.val else True
+
+ @visible.setter
+ def visible(self, value):
+ if value not in (True, False):
+ raise ValueError("assigned value must be True or False, got: %s" % value)
+ delete = self._element.get_or_add_delete_()
+ delete.val = not value
+
+
+class AxisTitle(ElementProxy):
+ """Provides properties for manipulating axis title."""
+
+ def __init__(self, title):
+ super(AxisTitle, self).__init__(title)
+ self._title = title
+
+ @lazyproperty
+ def format(self):
+ """|ChartFormat| object providing access to shape formatting.
+
+ Return the |ChartFormat| object providing shape formatting properties
+ for this axis title, such as its line color and fill.
+ """
+ return ChartFormat(self._element)
+
+ @property
+ def has_text_frame(self):
+ """Read/write Boolean specifying presence of a text frame.
+
+ Return |True| if this axis title has a text frame, and |False|
+ otherwise. Assigning |True| causes a text frame to be added if not
+ already present. Assigning |False| causes any existing text frame to
+ be removed along with any text contained in the text frame.
+ """
+ if self._title.tx_rich is None:
+ return False
+ return True
+
+ @has_text_frame.setter
+ def has_text_frame(self, value):
+ if bool(value) is True:
+ self._title.get_or_add_tx_rich()
+ else:
+ self._title._remove_tx()
+
+ @property
+ def text_frame(self):
+ """|TextFrame| instance for this axis title.
+
+ Return a |TextFrame| instance allowing read/write access to the text
+ of this axis title and its text formatting properties. Accessing this
+ property is destructive as it adds a new text frame if not already
+ present.
+ """
+ rich = self._title.get_or_add_tx_rich()
+ return TextFrame(rich, self)
+
+
+class CategoryAxis(_BaseAxis):
+ """A category axis of a chart."""
+
+ @property
+ def category_type(self):
+ """
+ A member of :ref:`XlCategoryType` specifying the scale type of this
+ axis. Unconditionally ``CATEGORY_SCALE`` for a |CategoryAxis| object.
+ """
+ return XL_CATEGORY_TYPE.CATEGORY_SCALE
+
+
+class DateAxis(_BaseAxis):
+ """A category axis with dates as its category labels.
+
+ This axis-type has some special display behaviors such as making length of equal
+ periods equal and normalizing month start dates despite unequal month lengths.
+ """
+
+ @property
+ def category_type(self):
+ """
+ A member of :ref:`XlCategoryType` specifying the scale type of this
+ axis. Unconditionally ``TIME_SCALE`` for a |DateAxis| object.
+ """
+ return XL_CATEGORY_TYPE.TIME_SCALE
+
+
+class MajorGridlines(ElementProxy):
+ """Provides access to the properties of the major gridlines appearing on an axis."""
+
+ def __init__(self, xAx):
+ super(MajorGridlines, self).__init__(xAx)
+ self._xAx = xAx # axis element, catAx or valAx
+
+ @lazyproperty
+ def format(self):
+ """
+ The |ChartFormat| object providing access to the shape formatting
+ properties of this data point, such as line and fill.
+ """
+ majorGridlines = self._xAx.get_or_add_majorGridlines()
+ return ChartFormat(majorGridlines)
+
+
+class TickLabels(object):
+ """A service class providing access to formatting of axis tick mark labels."""
+
+ def __init__(self, xAx_elm):
+ super(TickLabels, self).__init__()
+ self._element = xAx_elm
+
+ @lazyproperty
+ def font(self):
+ """
+ The |Font| object that provides access to the text properties for
+ these tick labels, such as bold, italic, etc.
+ """
+ defRPr = self._element.defRPr
+ font = Font(defRPr)
+ return font
+
+ @property
+ def number_format(self):
+ """
+ Read/write string (e.g. "$#,##0.00") specifying the format for the
+ numbers on this axis. The syntax for these strings is the same as it
+ appears in the PowerPoint or Excel UI. Returns 'General' if no number
+ format has been set. Note that this format string has no effect on
+ rendered tick labels when :meth:`number_format_is_linked` is |True|.
+ Assigning a format string to this property automatically sets
+ :meth:`number_format_is_linked` to |False|.
+ """
+ numFmt = self._element.numFmt
+ if numFmt is None:
+ return "General"
+ return numFmt.formatCode
+
+ @number_format.setter
+ def number_format(self, value):
+ numFmt = self._element.get_or_add_numFmt()
+ numFmt.formatCode = value
+ self.number_format_is_linked = False
+
+ @property
+ def number_format_is_linked(self):
+ """
+ Read/write boolean specifying whether number formatting should be
+ taken from the source spreadsheet rather than the value of
+ :meth:`number_format`.
+ """
+ numFmt = self._element.numFmt
+ if numFmt is None:
+ return False
+ souceLinked = numFmt.sourceLinked
+ if souceLinked is None:
+ return True
+ return numFmt.sourceLinked
+
+ @number_format_is_linked.setter
+ def number_format_is_linked(self, value):
+ numFmt = self._element.get_or_add_numFmt()
+ numFmt.sourceLinked = value
+
+ @property
+ def offset(self):
+ """
+ Read/write int value in range 0-1000 specifying the spacing between
+ the tick mark labels and the axis as a percentange of the default
+ value. 100 if no label offset setting is present.
+ """
+ lblOffset = self._element.lblOffset
+ if lblOffset is None:
+ return 100
+ return lblOffset.val
+
+ @offset.setter
+ def offset(self, value):
+ if self._element.tag != qn("c:catAx"):
+ raise ValueError("only a category axis has an offset")
+ self._element._remove_lblOffset()
+ if value == 100:
+ return
+ lblOffset = self._element._add_lblOffset()
+ lblOffset.val = value
+
+
+class ValueAxis(_BaseAxis):
+ """An axis having continuous (as opposed to discrete) values.
+
+ The vertical axis is generally a value axis, however both axes of an XY-type chart
+ are value axes.
+ """
+
+ @property
+ def crosses(self):
+ """
+ Member of :ref:`XlAxisCrosses` enumeration specifying the point on
+ this axis where the other axis crosses, such as auto/zero, minimum,
+ or maximum. Returns `XL_AXIS_CROSSES.CUSTOM` when a specific numeric
+ crossing point (e.g. 1.5) is defined.
+ """
+ crosses = self._cross_xAx.crosses
+ if crosses is None:
+ return XL_AXIS_CROSSES.CUSTOM
+ return crosses.val
+
+ @crosses.setter
+ def crosses(self, value):
+ cross_xAx = self._cross_xAx
+ if value == XL_AXIS_CROSSES.CUSTOM:
+ if cross_xAx.crossesAt is not None:
+ return
+ cross_xAx._remove_crosses()
+ cross_xAx._remove_crossesAt()
+ if value == XL_AXIS_CROSSES.CUSTOM:
+ cross_xAx._add_crossesAt(val=0.0)
+ else:
+ cross_xAx._add_crosses(val=value)
+
+ @property
+ def crosses_at(self):
+ """
+ Numeric value on this axis at which the perpendicular axis crosses.
+ Returns |None| if no crossing value is set.
+ """
+ crossesAt = self._cross_xAx.crossesAt
+ if crossesAt is None:
+ return None
+ return crossesAt.val
+
+ @crosses_at.setter
+ def crosses_at(self, value):
+ cross_xAx = self._cross_xAx
+ cross_xAx._remove_crosses()
+ cross_xAx._remove_crossesAt()
+ if value is None:
+ return
+ cross_xAx._add_crossesAt(val=value)
+
+ @property
+ def major_unit(self):
+ """
+ The float number of units between major tick marks on this value
+ axis. |None| corresponds to the 'Auto' setting in the UI, and
+ specifies the value should be calculated by PowerPoint based on the
+ underlying chart data.
+ """
+ majorUnit = self._element.majorUnit
+ if majorUnit is None:
+ return None
+ return majorUnit.val
+
+ @major_unit.setter
+ def major_unit(self, value):
+ self._element._remove_majorUnit()
+ if value is None:
+ return
+ self._element._add_majorUnit(val=value)
+
+ @property
+ def minor_unit(self):
+ """
+ The float number of units between minor tick marks on this value
+ axis. |None| corresponds to the 'Auto' setting in the UI, and
+ specifies the value should be calculated by PowerPoint based on the
+ underlying chart data.
+ """
+ minorUnit = self._element.minorUnit
+ if minorUnit is None:
+ return None
+ return minorUnit.val
+
+ @minor_unit.setter
+ def minor_unit(self, value):
+ self._element._remove_minorUnit()
+ if value is None:
+ return
+ self._element._add_minorUnit(val=value)
+
+ @property
+ def _cross_xAx(self):
+ """
+ The axis element in the same group (primary/secondary) that crosses
+ this axis.
+ """
+ crossAx_id = self._element.crossAx.val
+ expr = '(../c:catAx | ../c:valAx | ../c:dateAx)/c:axId[@val="%d"]' % crossAx_id
+ cross_axId = self._element.xpath(expr)[0]
+ return cross_axId.getparent()
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/category.py b/.venv/lib/python3.12/site-packages/pptx/chart/category.py
new file mode 100644
index 00000000..2c28aff5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/category.py
@@ -0,0 +1,200 @@
+"""Category-related objects.
+
+The |category.Categories| object is returned by ``Plot.categories`` and contains zero or
+more |category.Category| objects, each representing one of the category labels
+associated with the plot. Categories can be hierarchical, so there are members allowing
+discovery of the depth of that hierarchy and providing means to navigate it.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+
+class Categories(Sequence):
+ """
+ A sequence of |category.Category| objects, each representing a category
+ label on the chart. Provides properties for dealing with hierarchical
+ categories.
+ """
+
+ def __init__(self, xChart):
+ super(Categories, self).__init__()
+ self._xChart = xChart
+
+ def __getitem__(self, idx):
+ pt = self._xChart.cat_pts[idx]
+ return Category(pt, idx)
+
+ def __iter__(self):
+ cat_pts = self._xChart.cat_pts
+ for idx, pt in enumerate(cat_pts):
+ yield Category(pt, idx)
+
+ def __len__(self):
+ # a category can be "null", meaning the Excel cell for it is empty.
+ # In this case, there is no c:pt element for it. The "empty" category
+ # will, however, be accounted for in c:cat//c:ptCount/@val, which
+ # reflects the true length of the categories collection.
+ return self._xChart.cat_pt_count
+
+ @property
+ def depth(self):
+ """
+ Return an integer representing the number of hierarchical levels in
+ this category collection. Returns 1 for non-hierarchical categories
+ and 0 if no categories are present (generally meaning no series are
+ present).
+ """
+ cat = self._xChart.cat
+ if cat is None:
+ return 0
+ if cat.multiLvlStrRef is None:
+ return 1
+ return len(cat.lvls)
+
+ @property
+ def flattened_labels(self):
+ """
+ Return a sequence of tuples, each containing the flattened hierarchy
+ of category labels for a leaf category. Each tuple is in parent ->
+ child order, e.g. ``('US', 'CA', 'San Francisco')``, with the leaf
+ category appearing last. If this categories collection is
+ non-hierarchical, each tuple will contain only a leaf category label.
+ If the plot has no series (and therefore no categories), an empty
+ tuple is returned.
+ """
+ cat = self._xChart.cat
+ if cat is None:
+ return ()
+
+ if cat.multiLvlStrRef is None:
+ return tuple([(category.label,) for category in self])
+
+ return tuple(
+ [
+ tuple([category.label for category in reversed(flat_cat)])
+ for flat_cat in self._iter_flattened_categories()
+ ]
+ )
+
+ @property
+ def levels(self):
+ """
+ Return a sequence of |CategoryLevel| objects representing the
+ hierarchy of this category collection. The sequence is empty when the
+ category collection is not hierarchical, that is, contains only
+ leaf-level categories. The levels are ordered from the leaf level to
+ the root level; so the first level will contain the same categories
+ as this category collection.
+ """
+ cat = self._xChart.cat
+ if cat is None:
+ return []
+ return [CategoryLevel(lvl) for lvl in cat.lvls]
+
+ def _iter_flattened_categories(self):
+ """
+ Generate a ``tuple`` object for each leaf category in this
+ collection, containing the leaf category followed by its "parent"
+ categories, e.g. ``('San Francisco', 'CA', 'USA'). Each tuple will be
+ the same length as the number of levels (excepting certain edge
+ cases which I believe always indicate a chart construction error).
+ """
+ levels = self.levels
+ if not levels:
+ return
+ leaf_level, remaining_levels = levels[0], levels[1:]
+ for category in leaf_level:
+ yield self._parentage((category,), remaining_levels)
+
+ def _parentage(self, categories, levels):
+ """
+ Return a tuple formed by recursively concatenating *categories* with
+ its next ancestor from *levels*. The idx value of the first category
+ in *categories* determines parentage in all levels. The returned
+ sequence is in child -> parent order. A parent category is the
+ Category object in a next level having the maximum idx value not
+ exceeding that of the leaf category.
+ """
+ # exhausting levels is the expected recursion termination condition
+ if not levels:
+ return tuple(categories)
+
+ # guard against edge case where next level is present but empty. That
+ # situation is not prohibited for some reason.
+ if not levels[0]:
+ return tuple(categories)
+
+ parent_level, remaining_levels = levels[0], levels[1:]
+ leaf_node = categories[0]
+
+ # Make the first parent the default. A possible edge case is where no
+ # parent is defined for one or more leading values, e.g. idx > 0 for
+ # the first parent.
+ parent = parent_level[0]
+ for category in parent_level:
+ if category.idx > leaf_node.idx:
+ break
+ parent = category
+
+ extended_categories = tuple(categories) + (parent,)
+ return self._parentage(extended_categories, remaining_levels)
+
+
+class Category(str):
+ """
+ An extension of `str` that provides the category label as its string
+ value, and additional attributes representing other aspects of the
+ category.
+ """
+
+ def __new__(cls, pt, *args):
+ category_label = "" if pt is None else pt.v.text
+ return str.__new__(cls, category_label)
+
+ def __init__(self, pt, idx=None):
+ """
+ *idx* is a required attribute of a c:pt element, but must be
+ specified when pt is None, as when a "placeholder" category is
+ created to represent a missing c:pt element.
+ """
+ self._element = self._pt = pt
+ self._idx = idx
+
+ @property
+ def idx(self):
+ """
+ Return an integer representing the index reference of this category.
+ For a leaf node, the index identifies the category. For a parent (or
+ other ancestor) category, the index specifies the first leaf category
+ that ancestor encloses.
+ """
+ if self._pt is None:
+ return self._idx
+ return self._pt.idx
+
+ @property
+ def label(self):
+ """
+ Return the label of this category as a string.
+ """
+ return str(self)
+
+
+class CategoryLevel(Sequence):
+ """
+ A sequence of |category.Category| objects representing a single level in
+ a hierarchical category collection. This object is only used when the
+ categories are hierarchical, meaning they have more than one level and
+ higher level categories group those at lower levels.
+ """
+
+ def __init__(self, lvl):
+ self._element = self._lvl = lvl
+
+ def __getitem__(self, offset):
+ return Category(self._lvl.pt_lst[offset])
+
+ def __len__(self):
+ return len(self._lvl.pt_lst)
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/chart.py b/.venv/lib/python3.12/site-packages/pptx/chart/chart.py
new file mode 100644
index 00000000..d73aa933
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/chart.py
@@ -0,0 +1,280 @@
+"""Chart-related objects such as Chart and ChartTitle."""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+from pptx.chart.axis import CategoryAxis, DateAxis, ValueAxis
+from pptx.chart.legend import Legend
+from pptx.chart.plot import PlotFactory, PlotTypeInspector
+from pptx.chart.series import SeriesCollection
+from pptx.chart.xmlwriter import SeriesXmlRewriterFactory
+from pptx.dml.chtfmt import ChartFormat
+from pptx.shared import ElementProxy, PartElementProxy
+from pptx.text.text import Font, TextFrame
+from pptx.util import lazyproperty
+
+
+class Chart(PartElementProxy):
+ """A chart object."""
+
+ def __init__(self, chartSpace, chart_part):
+ super(Chart, self).__init__(chartSpace, chart_part)
+ self._chartSpace = chartSpace
+
+ @property
+ def category_axis(self):
+ """
+ The category axis of this chart. In the case of an XY or Bubble
+ chart, this is the X axis. Raises |ValueError| if no category
+ axis is defined (as is the case for a pie chart, for example).
+ """
+ catAx_lst = self._chartSpace.catAx_lst
+ if catAx_lst:
+ return CategoryAxis(catAx_lst[0])
+
+ dateAx_lst = self._chartSpace.dateAx_lst
+ if dateAx_lst:
+ return DateAxis(dateAx_lst[0])
+
+ valAx_lst = self._chartSpace.valAx_lst
+ if valAx_lst:
+ return ValueAxis(valAx_lst[0])
+
+ raise ValueError("chart has no category axis")
+
+ @property
+ def chart_style(self):
+ """
+ Read/write integer index of chart style used to format this chart.
+ Range is from 1 to 48. Value is |None| if no explicit style has been
+ assigned, in which case the default chart style is used. Assigning
+ |None| causes any explicit setting to be removed. The integer index
+ corresponds to the style's position in the chart style gallery in the
+ PowerPoint UI.
+ """
+ style = self._chartSpace.style
+ if style is None:
+ return None
+ return style.val
+
+ @chart_style.setter
+ def chart_style(self, value):
+ self._chartSpace._remove_style()
+ if value is None:
+ return
+ self._chartSpace._add_style(val=value)
+
+ @property
+ def chart_title(self):
+ """A |ChartTitle| object providing access to title properties.
+
+ Calling this property is destructive in the sense it adds a chart
+ title element (`c:title`) to the chart XML if one is not already
+ present. Use :attr:`has_title` to test for presence of a chart title
+ non-destructively.
+ """
+ return ChartTitle(self._element.get_or_add_title())
+
+ @property
+ def chart_type(self):
+ """Member of :ref:`XlChartType` enumeration specifying type of this chart.
+
+ If the chart has two plots, for example, a line plot overlayed on a bar plot,
+ the type reported is for the first (back-most) plot. Read-only.
+ """
+ first_plot = self.plots[0]
+ return PlotTypeInspector.chart_type(first_plot)
+
+ @lazyproperty
+ def font(self):
+ """Font object controlling text format defaults for this chart."""
+ defRPr = self._chartSpace.get_or_add_txPr().p_lst[0].get_or_add_pPr().get_or_add_defRPr()
+ return Font(defRPr)
+
+ @property
+ def has_legend(self):
+ """
+ Read/write boolean, |True| if the chart has a legend. Assigning
+ |True| causes a legend to be added to the chart if it doesn't already
+ have one. Assigning False removes any existing legend definition
+ along with any existing legend settings.
+ """
+ return self._chartSpace.chart.has_legend
+
+ @has_legend.setter
+ def has_legend(self, value):
+ self._chartSpace.chart.has_legend = bool(value)
+
+ @property
+ def has_title(self):
+ """Read/write boolean, specifying whether this chart has a title.
+
+ Assigning |True| causes a title to be added if not already present.
+ Assigning |False| removes any existing title along with its text and
+ settings.
+ """
+ title = self._chartSpace.chart.title
+ if title is None:
+ return False
+ return True
+
+ @has_title.setter
+ def has_title(self, value):
+ chart = self._chartSpace.chart
+ if bool(value) is False:
+ chart._remove_title()
+ autoTitleDeleted = chart.get_or_add_autoTitleDeleted()
+ autoTitleDeleted.val = True
+ return
+ chart.get_or_add_title()
+
+ @property
+ def legend(self):
+ """
+ A |Legend| object providing access to the properties of the legend
+ for this chart.
+ """
+ legend_elm = self._chartSpace.chart.legend
+ if legend_elm is None:
+ return None
+ return Legend(legend_elm)
+
+ @lazyproperty
+ def plots(self):
+ """
+ The sequence of plots in this chart. A plot, called a *chart group*
+ in the Microsoft API, is a distinct sequence of one or more series
+ depicted in a particular charting type. For example, a chart having
+ a series plotted as a line overlaid on three series plotted as
+ columns would have two plots; the first corresponding to the three
+ column series and the second to the line series. Plots are sequenced
+ in the order drawn, i.e. back-most to front-most. Supports *len()*,
+ membership (e.g. ``p in plots``), iteration, slicing, and indexed
+ access (e.g. ``plot = plots[i]``).
+ """
+ plotArea = self._chartSpace.chart.plotArea
+ return _Plots(plotArea, self)
+
+ def replace_data(self, chart_data):
+ """
+ Use the categories and series values in the |ChartData| object
+ *chart_data* to replace those in the XML and Excel worksheet for this
+ chart.
+ """
+ rewriter = SeriesXmlRewriterFactory(self.chart_type, chart_data)
+ rewriter.replace_series_data(self._chartSpace)
+ self._workbook.update_from_xlsx_blob(chart_data.xlsx_blob)
+
+ @lazyproperty
+ def series(self):
+ """
+ A |SeriesCollection| object containing all the series in this
+ chart. When the chart has multiple plots, all the series for the
+ first plot appear before all those for the second, and so on. Series
+ within a plot have an explicit ordering and appear in that sequence.
+ """
+ return SeriesCollection(self._chartSpace.plotArea)
+
+ @property
+ def value_axis(self):
+ """
+ The |ValueAxis| object providing access to properties of the value
+ axis of this chart. Raises |ValueError| if the chart has no value
+ axis.
+ """
+ valAx_lst = self._chartSpace.valAx_lst
+ if not valAx_lst:
+ raise ValueError("chart has no value axis")
+
+ idx = 1 if len(valAx_lst) > 1 else 0
+ return ValueAxis(valAx_lst[idx])
+
+ @property
+ def _workbook(self):
+ """
+ The |ChartWorkbook| object providing access to the Excel source data
+ for this chart.
+ """
+ return self.part.chart_workbook
+
+
+class ChartTitle(ElementProxy):
+ """Provides properties for manipulating a chart title."""
+
+ # This shares functionality with AxisTitle, which could be factored out
+ # into a base class, perhaps pptx.chart.shared.BaseTitle. I suspect they
+ # actually differ in certain fuller behaviors, but at present they're
+ # essentially identical.
+
+ def __init__(self, title):
+ super(ChartTitle, self).__init__(title)
+ self._title = title
+
+ @lazyproperty
+ def format(self):
+ """|ChartFormat| object providing access to line and fill formatting.
+
+ Return the |ChartFormat| object providing shape formatting properties
+ for this chart title, such as its line color and fill.
+ """
+ return ChartFormat(self._title)
+
+ @property
+ def has_text_frame(self):
+ """Read/write Boolean specifying whether this title has a text frame.
+
+ Return |True| if this chart title has a text frame, and |False|
+ otherwise. Assigning |True| causes a text frame to be added if not
+ already present. Assigning |False| causes any existing text frame to
+ be removed along with its text and formatting.
+ """
+ if self._title.tx_rich is None:
+ return False
+ return True
+
+ @has_text_frame.setter
+ def has_text_frame(self, value):
+ if bool(value) is False:
+ self._title._remove_tx()
+ return
+ self._title.get_or_add_tx_rich()
+
+ @property
+ def text_frame(self):
+ """|TextFrame| instance for this chart title.
+
+ Return a |TextFrame| instance allowing read/write access to the text
+ of this chart title and its text formatting properties. Accessing this
+ property is destructive in the sense it adds a text frame if one is
+ not present. Use :attr:`has_text_frame` to test for the presence of
+ a text frame non-destructively.
+ """
+ rich = self._title.get_or_add_tx_rich()
+ return TextFrame(rich, self)
+
+
+class _Plots(Sequence):
+ """
+ The sequence of plots in a chart, such as a bar plot or a line plot. Most
+ charts have only a single plot. The concept is necessary when two chart
+ types are displayed in a single set of axes, like a bar plot with
+ a superimposed line plot.
+ """
+
+ def __init__(self, plotArea, chart):
+ super(_Plots, self).__init__()
+ self._plotArea = plotArea
+ self._chart = chart
+
+ def __getitem__(self, index):
+ xCharts = self._plotArea.xCharts
+ if isinstance(index, slice):
+ plots = [PlotFactory(xChart, self._chart) for xChart in xCharts]
+ return plots[index]
+ else:
+ xChart = xCharts[index]
+ return PlotFactory(xChart, self._chart)
+
+ def __len__(self):
+ return len(self._plotArea.xCharts)
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/data.py b/.venv/lib/python3.12/site-packages/pptx/chart/data.py
new file mode 100644
index 00000000..ec6a61f3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/data.py
@@ -0,0 +1,864 @@
+"""ChartData and related objects."""
+
+from __future__ import annotations
+
+import datetime
+from collections.abc import Sequence
+from numbers import Number
+
+from pptx.chart.xlsx import (
+ BubbleWorkbookWriter,
+ CategoryWorkbookWriter,
+ XyWorkbookWriter,
+)
+from pptx.chart.xmlwriter import ChartXmlWriter
+from pptx.util import lazyproperty
+
+
+class _BaseChartData(Sequence):
+ """Base class providing common members for chart data objects.
+
+ A chart data object serves as a proxy for the chart data table that will be written to an
+ Excel worksheet; operating as a sequence of series as well as providing access to chart-level
+ attributes. A chart data object is used as a parameter in :meth:`shapes.add_chart` and
+ :meth:`Chart.replace_data`. The data structure varies between major chart categories such as
+ category charts and XY charts.
+ """
+
+ def __init__(self, number_format="General"):
+ super(_BaseChartData, self).__init__()
+ self._number_format = number_format
+ self._series = []
+
+ def __getitem__(self, index):
+ return self._series.__getitem__(index)
+
+ def __len__(self):
+ return self._series.__len__()
+
+ def append(self, series):
+ return self._series.append(series)
+
+ def data_point_offset(self, series):
+ """
+ The total integer number of data points appearing in the series of
+ this chart that are prior to *series* in this sequence.
+ """
+ count = 0
+ for this_series in self:
+ if series is this_series:
+ return count
+ count += len(this_series)
+ raise ValueError("series not in chart data object")
+
+ @property
+ def number_format(self):
+ """
+ The formatting template string, e.g. '#,##0.0', that determines how
+ X and Y values are formatted in this chart and in the Excel
+ spreadsheet. A number format specified on a series will override this
+ value for that series. Likewise, a distinct number format can be
+ specified for a particular data point within a series.
+ """
+ return self._number_format
+
+ def series_index(self, series):
+ """
+ Return the integer index of *series* in this sequence.
+ """
+ for idx, s in enumerate(self):
+ if series is s:
+ return idx
+ raise ValueError("series not in chart data object")
+
+ def series_name_ref(self, series):
+ """
+ Return the Excel worksheet reference to the cell containing the name
+ for *series*.
+ """
+ return self._workbook_writer.series_name_ref(series)
+
+ def x_values_ref(self, series):
+ """
+ The Excel worksheet reference to the X values for *series* (not
+ including the column label).
+ """
+ return self._workbook_writer.x_values_ref(series)
+
+ @property
+ def xlsx_blob(self):
+ """
+ Return a blob containing an Excel workbook file populated with the
+ contents of this chart data object.
+ """
+ return self._workbook_writer.xlsx_blob
+
+ def xml_bytes(self, chart_type):
+ """
+ Return a blob containing the XML for a chart of *chart_type*
+ containing the series in this chart data object, as bytes suitable
+ for writing directly to a file.
+ """
+ return self._xml(chart_type).encode("utf-8")
+
+ def y_values_ref(self, series):
+ """
+ The Excel worksheet reference to the Y values for *series* (not
+ including the column label).
+ """
+ return self._workbook_writer.y_values_ref(series)
+
+ @property
+ def _workbook_writer(self):
+ """
+ The worksheet writer object to which layout and writing of the Excel
+ worksheet for this chart will be delegated.
+ """
+ raise NotImplementedError("must be implemented by all subclasses")
+
+ def _xml(self, chart_type):
+ """
+ Return (as unicode text) the XML for a chart of *chart_type*
+ populated with the values in this chart data object. The XML is
+ a complete XML document, including an XML declaration specifying
+ UTF-8 encoding.
+ """
+ return ChartXmlWriter(chart_type, self).xml
+
+
+class _BaseSeriesData(Sequence):
+ """
+ Base class providing common members for series data objects. A series
+ data object serves as proxy for a series data column in the Excel
+ worksheet. It operates as a sequence of data points, as well as providing
+ access to series-level attributes like the series label.
+ """
+
+ def __init__(self, chart_data, name, number_format):
+ self._chart_data = chart_data
+ self._name = name
+ self._number_format = number_format
+ self._data_points = []
+
+ def __getitem__(self, index):
+ return self._data_points.__getitem__(index)
+
+ def __len__(self):
+ return self._data_points.__len__()
+
+ def append(self, data_point):
+ return self._data_points.append(data_point)
+
+ @property
+ def data_point_offset(self):
+ """
+ The integer count of data points that appear in all chart series
+ prior to this one.
+ """
+ return self._chart_data.data_point_offset(self)
+
+ @property
+ def index(self):
+ """
+ Zero-based integer indicating the sequence position of this series in
+ its chart. For example, the second of three series would return `1`.
+ """
+ return self._chart_data.series_index(self)
+
+ @property
+ def name(self):
+ """
+ The name of this series, e.g. 'Series 1'. This name is used as the
+ column heading for the y-values of this series and may also appear in
+ the chart legend and perhaps other chart locations.
+ """
+ return self._name if self._name is not None else ""
+
+ @property
+ def name_ref(self):
+ """
+ The Excel worksheet reference to the cell containing the name for
+ this series.
+ """
+ return self._chart_data.series_name_ref(self)
+
+ @property
+ def number_format(self):
+ """
+ The formatting template string that determines how a number in this
+ series is formatted, both in the chart and in the Excel spreadsheet;
+ for example '#,##0.0'. If not specified for this series, it is
+ inherited from the parent chart data object.
+ """
+ number_format = self._number_format
+ if number_format is None:
+ return self._chart_data.number_format
+ return number_format
+
+ @property
+ def x_values(self):
+ """
+ A sequence containing the X value of each datapoint in this series,
+ in data point order.
+ """
+ return [dp.x for dp in self._data_points]
+
+ @property
+ def x_values_ref(self):
+ """
+ The Excel worksheet reference to the X values for this chart (not
+ including the column heading).
+ """
+ return self._chart_data.x_values_ref(self)
+
+ @property
+ def y_values(self):
+ """
+ A sequence containing the Y value of each datapoint in this series,
+ in data point order.
+ """
+ return [dp.y for dp in self._data_points]
+
+ @property
+ def y_values_ref(self):
+ """
+ The Excel worksheet reference to the Y values for this chart (not
+ including the column heading).
+ """
+ return self._chart_data.y_values_ref(self)
+
+
+class _BaseDataPoint(object):
+ """
+ Base class providing common members for data point objects.
+ """
+
+ def __init__(self, series_data, number_format):
+ super(_BaseDataPoint, self).__init__()
+ self._series_data = series_data
+ self._number_format = number_format
+
+ @property
+ def number_format(self):
+ """
+ The formatting template string that determines how the value of this
+ data point is formatted, both in the chart and in the Excel
+ spreadsheet; for example '#,##0.0'. If not specified for this data
+ point, it is inherited from the parent series data object.
+ """
+ number_format = self._number_format
+ if number_format is None:
+ return self._series_data.number_format
+ return number_format
+
+
+class CategoryChartData(_BaseChartData):
+ """
+ Accumulates data specifying the categories and series values for a chart
+ and acts as a proxy for the chart data table that will be written to an
+ Excel worksheet. Used as a parameter in :meth:`shapes.add_chart` and
+ :meth:`Chart.replace_data`.
+
+ This object is suitable for use with category charts, i.e. all those
+ having a discrete set of label values (categories) as the range of their
+ independent variable (X-axis) values. Unlike the ChartData types for
+ charts supporting a continuous range of independent variable values (such
+ as XyChartData), CategoryChartData has a single collection of category
+ (X) values and each data point in its series specifies only the Y value.
+ The corresponding X value is inferred by its position in the sequence.
+ """
+
+ def add_category(self, label):
+ """
+ Return a newly created |data.Category| object having *label* and
+ appended to the end of the category collection for this chart.
+ *label* can be a string, a number, a datetime.date, or
+ datetime.datetime object. All category labels in a chart must be the
+ same type. All category labels in a chart having multi-level
+ categories must be strings.
+ """
+ return self.categories.add_category(label)
+
+ def add_series(self, name, values=(), number_format=None):
+ """
+ Add a series to this data set entitled *name* and having the data
+ points specified by *values*, an iterable of numeric values.
+ *number_format* specifies how the series values will be displayed,
+ and may be a string, e.g. '#,##0' corresponding to an Excel number
+ format.
+ """
+ series_data = CategorySeriesData(self, name, number_format)
+ self.append(series_data)
+ for value in values:
+ series_data.add_data_point(value)
+ return series_data
+
+ @property
+ def categories(self):
+ """|data.Categories| object providing access to category-object hierarchy.
+
+ Assigning an iterable of category labels (strings, numbers, or dates) replaces
+ the |data.Categories| object with a new one containing a category for each label
+ in the sequence.
+
+ Creating a chart from chart data having date categories will cause the chart to
+ have a |DateAxis| for its category axis.
+ """
+ if not getattr(self, "_categories", False):
+ self._categories = Categories()
+ return self._categories
+
+ @categories.setter
+ def categories(self, category_labels):
+ categories = Categories()
+ for label in category_labels:
+ categories.add_category(label)
+ self._categories = categories
+
+ @property
+ def categories_ref(self):
+ """
+ The Excel worksheet reference to the categories for this chart (not
+ including the column heading).
+ """
+ return self._workbook_writer.categories_ref
+
+ def values_ref(self, series):
+ """
+ The Excel worksheet reference to the values for *series* (not
+ including the column heading).
+ """
+ return self._workbook_writer.values_ref(series)
+
+ @lazyproperty
+ def _workbook_writer(self):
+ """
+ The worksheet writer object to which layout and writing of the Excel
+ worksheet for this chart will be delegated.
+ """
+ return CategoryWorkbookWriter(self)
+
+
+class Categories(Sequence):
+ """
+ A sequence of |data.Category| objects, also having certain hierarchical
+ graph behaviors for support of multi-level (nested) categories.
+ """
+
+ def __init__(self):
+ super(Categories, self).__init__()
+ self._categories = []
+ self._number_format = None
+
+ def __getitem__(self, idx):
+ return self._categories.__getitem__(idx)
+
+ def __len__(self):
+ """
+ Return the count of the highest level of category in this sequence.
+ If it contains hierarchical (multi-level) categories, this number
+ will differ from :attr:`category_count`, which is the number of leaf
+ nodes.
+ """
+ return self._categories.__len__()
+
+ def add_category(self, label):
+ """
+ Return a newly created |data.Category| object having *label* and
+ appended to the end of this category sequence. *label* can be
+ a string, a number, a datetime.date, or datetime.datetime object. All
+ category labels in a chart must be the same type. All category labels
+ in a chart having multi-level categories must be strings.
+
+ Creating a chart from chart data having date categories will cause
+ the chart to have a |DateAxis| for its category axis.
+ """
+ category = Category(label, self)
+ self._categories.append(category)
+ return category
+
+ @property
+ def are_dates(self):
+ """
+ Return |True| if the first category in this collection has a date
+ label (as opposed to str or numeric). A date label is one of type
+ datetime.date or datetime.datetime. Returns |False| otherwise,
+ including when this category collection is empty. It also returns
+ False when this category collection is hierarchical, because
+ hierarchical categories can only be written as string labels.
+ """
+ if self.depth != 1:
+ return False
+ first_cat_label = self[0].label
+ date_types = (datetime.date, datetime.datetime)
+ if isinstance(first_cat_label, date_types):
+ return True
+ return False
+
+ @property
+ def are_numeric(self):
+ """
+ Return |True| if the first category in this collection has a numeric
+ label (as opposed to a string label), including if that value is
+ a datetime.date or datetime.datetime object (as those are converted
+ to integers for storage in Excel). Returns |False| otherwise,
+ including when this category collection is empty. It also returns
+ False when this category collection is hierarchical, because
+ hierarchical categories can only be written as string labels.
+ """
+ if self.depth != 1:
+ return False
+ # This method only tests the first category. The categories must
+ # be of uniform type, and if they're not, there will be problems
+ # later in the process, but it's not this method's job to validate
+ # the caller's input.
+ first_cat_label = self[0].label
+ numeric_types = (Number, datetime.date, datetime.datetime)
+ if isinstance(first_cat_label, numeric_types):
+ return True
+ return False
+
+ @property
+ def depth(self):
+ """
+ The number of hierarchy levels in this category graph. Returns 0 if
+ it contains no categories.
+ """
+ categories = self._categories
+ if not categories:
+ return 0
+ first_depth = categories[0].depth
+ for category in categories[1:]:
+ if category.depth != first_depth:
+ raise ValueError("category depth not uniform")
+ return first_depth
+
+ def index(self, category):
+ """
+ The offset of *category* in the overall sequence of leaf categories.
+ A non-leaf category gets the index of its first sub-category.
+ """
+ index = 0
+ for this_category in self._categories:
+ if category is this_category:
+ return index
+ index += this_category.leaf_count
+ raise ValueError("category not in top-level categories")
+
+ @property
+ def leaf_count(self):
+ """
+ The number of leaf-level categories in this hierarchy. The return
+ value is the same as that of `len()` only when the hierarchy is
+ single level.
+ """
+ return sum(c.leaf_count for c in self._categories)
+
+ @property
+ def levels(self):
+ """
+ A generator of (idx, label) sequences representing the category
+ hierarchy from the bottom up. The first level contains all leaf
+ categories, and each subsequent is the next level up.
+ """
+
+ def levels(categories):
+ # yield all lower levels
+ sub_categories = [sc for c in categories for sc in c.sub_categories]
+ if sub_categories:
+ for level in levels(sub_categories):
+ yield level
+ # yield this level
+ yield [(cat.idx, cat.label) for cat in categories]
+
+ for level in levels(self):
+ yield level
+
+ @property
+ def number_format(self):
+ """
+ Read/write. Return a string representing the number format used in
+ Excel to format these category values, e.g. '0.0' or 'mm/dd/yyyy'.
+ This string is only relevant when the categories are numeric or date
+ type, although it returns 'General' without error when the categories
+ are string labels. Assigning |None| causes the default number format
+ to be used, based on the type of the category labels.
+ """
+ GENERAL = "General"
+
+ # defined value takes precedence
+ if self._number_format is not None:
+ return self._number_format
+
+ # multi-level (should) always be string labels
+ # zero depth means empty in which case we can't tell anyway
+ if self.depth != 1:
+ return GENERAL
+
+ # everything except dates gets 'General'
+ first_cat_label = self[0].label
+ if isinstance(first_cat_label, (datetime.date, datetime.datetime)):
+ return r"yyyy\-mm\-dd"
+ return GENERAL
+
+ @number_format.setter
+ def number_format(self, value):
+ self._number_format = value
+
+
+class Category(object):
+ """
+ A chart category, primarily having a label to be displayed on the
+ category axis, but also able to be configured in a hierarchy for support
+ of multi-level category charts.
+ """
+
+ def __init__(self, label, parent):
+ super(Category, self).__init__()
+ self._label = label
+ self._parent = parent
+ self._sub_categories = []
+
+ def add_sub_category(self, label):
+ """
+ Return a newly created |data.Category| object having *label* and
+ appended to the end of the sub-category sequence for this category.
+ """
+ category = Category(label, self)
+ self._sub_categories.append(category)
+ return category
+
+ @property
+ def depth(self):
+ """
+ The number of hierarchy levels rooted at this category node. Returns
+ 1 if this category has no sub-categories.
+ """
+ sub_categories = self._sub_categories
+ if not sub_categories:
+ return 1
+ first_depth = sub_categories[0].depth
+ for category in sub_categories[1:]:
+ if category.depth != first_depth:
+ raise ValueError("category depth not uniform")
+ return first_depth + 1
+
+ @property
+ def idx(self):
+ """
+ The offset of this category in the overall sequence of leaf
+ categories. A non-leaf category gets the index of its first
+ sub-category.
+ """
+ return self._parent.index(self)
+
+ def index(self, sub_category):
+ """
+ The offset of *sub_category* in the overall sequence of leaf
+ categories.
+ """
+ index = self._parent.index(self)
+ for this_sub_category in self._sub_categories:
+ if sub_category is this_sub_category:
+ return index
+ index += this_sub_category.leaf_count
+ raise ValueError("sub_category not in this category")
+
+ @property
+ def leaf_count(self):
+ """
+ The number of leaf category nodes under this category. Returns
+ 1 if this category has no sub-categories.
+ """
+ if not self._sub_categories:
+ return 1
+ return sum(category.leaf_count for category in self._sub_categories)
+
+ @property
+ def label(self):
+ """
+ The value that appears on the axis for this category. The label can
+ be a string, a number, or a datetime.date or datetime.datetime
+ object.
+ """
+ return self._label if self._label is not None else ""
+
+ def numeric_str_val(self, date_1904=False):
+ """
+ The string representation of the numeric (or date) label of this
+ category, suitable for use in the XML `c:pt` element for this
+ category. The optional *date_1904* parameter specifies the epoch used
+ for calculating Excel date numbers.
+ """
+ label = self._label
+ if isinstance(label, (datetime.date, datetime.datetime)):
+ return "%.1f" % self._excel_date_number(date_1904)
+ return str(self._label)
+
+ @property
+ def sub_categories(self):
+ """
+ The sequence of child categories for this category.
+ """
+ return self._sub_categories
+
+ def _excel_date_number(self, date_1904):
+ """
+ Return an integer representing the date label of this category as the
+ number of days since January 1, 1900 (or 1904 if date_1904 is
+ |True|).
+ """
+ date, label = datetime.date, self._label
+ # -- get date from label in type-independent-ish way
+ date_ = date(label.year, label.month, label.day)
+ epoch = date(1904, 1, 1) if date_1904 else date(1899, 12, 31)
+ delta = date_ - epoch
+ excel_day_number = delta.days
+
+ # -- adjust for Excel mistaking 1900 for a leap year --
+ if not date_1904 and excel_day_number > 59:
+ excel_day_number += 1
+
+ return excel_day_number
+
+
+class ChartData(CategoryChartData):
+ """
+ |ChartData| is simply an alias for |CategoryChartData| and may be removed
+ in a future release. All new development should use |CategoryChartData|
+ for creating or replacing the data in chart types other than XY and
+ Bubble.
+ """
+
+
+class CategorySeriesData(_BaseSeriesData):
+ """
+ The data specific to a particular category chart series. It provides
+ access to the series label, the series data points, and an optional
+ number format to be applied to each data point not having a specified
+ number format.
+ """
+
+ def add_data_point(self, value, number_format=None):
+ """
+ Return a CategoryDataPoint object newly created with value *value*,
+ an optional *number_format*, and appended to this sequence.
+ """
+ data_point = CategoryDataPoint(self, value, number_format)
+ self.append(data_point)
+ return data_point
+
+ @property
+ def categories(self):
+ """
+ The |data.Categories| object that provides access to the category
+ objects for this series.
+ """
+ return self._chart_data.categories
+
+ @property
+ def categories_ref(self):
+ """
+ The Excel worksheet reference to the categories for this chart (not
+ including the column heading).
+ """
+ return self._chart_data.categories_ref
+
+ @property
+ def values(self):
+ """
+ A sequence containing the (Y) value of each datapoint in this series,
+ in data point order.
+ """
+ return [dp.value for dp in self._data_points]
+
+ @property
+ def values_ref(self):
+ """
+ The Excel worksheet reference to the (Y) values for this series (not
+ including the column heading).
+ """
+ return self._chart_data.values_ref(self)
+
+
+class XyChartData(_BaseChartData):
+ """
+ A specialized ChartData object suitable for use with an XY (aka. scatter)
+ chart. Unlike ChartData, it has no category sequence. Rather, each data
+ point of each series specifies both an X and a Y value.
+ """
+
+ def add_series(self, name, number_format=None):
+ """
+ Return an |XySeriesData| object newly created and added at the end of
+ this sequence, identified by *name* and values formatted with
+ *number_format*.
+ """
+ series_data = XySeriesData(self, name, number_format)
+ self.append(series_data)
+ return series_data
+
+ @lazyproperty
+ def _workbook_writer(self):
+ """
+ The worksheet writer object to which layout and writing of the Excel
+ worksheet for this chart will be delegated.
+ """
+ return XyWorkbookWriter(self)
+
+
+class BubbleChartData(XyChartData):
+ """
+ A specialized ChartData object suitable for use with a bubble chart.
+ A bubble chart is essentially an XY chart where the markers are scaled to
+ provide a third quantitative dimension to the exhibit.
+ """
+
+ def add_series(self, name, number_format=None):
+ """
+ Return a |BubbleSeriesData| object newly created and added at the end
+ of this sequence, and having series named *name* and values formatted
+ with *number_format*.
+ """
+ series_data = BubbleSeriesData(self, name, number_format)
+ self.append(series_data)
+ return series_data
+
+ def bubble_sizes_ref(self, series):
+ """
+ The Excel worksheet reference for the range containing the bubble
+ sizes for *series*.
+ """
+ return self._workbook_writer.bubble_sizes_ref(series)
+
+ @lazyproperty
+ def _workbook_writer(self):
+ """
+ The worksheet writer object to which layout and writing of the Excel
+ worksheet for this chart will be delegated.
+ """
+ return BubbleWorkbookWriter(self)
+
+
+class XySeriesData(_BaseSeriesData):
+ """
+ The data specific to a particular XY chart series. It provides access to
+ the series label, the series data points, and an optional number format
+ to be applied to each data point not having a specified number format.
+
+ The sequence of data points in an XY series is significant; lines are
+ plotted following the sequence of points, even if that causes a line
+ segment to "travel backward" (implying a multi-valued function). The data
+ points are not automatically sorted into increasing order by X value.
+ """
+
+ def add_data_point(self, x, y, number_format=None):
+ """
+ Return an XyDataPoint object newly created with values *x* and *y*,
+ and appended to this sequence.
+ """
+ data_point = XyDataPoint(self, x, y, number_format)
+ self.append(data_point)
+ return data_point
+
+
+class BubbleSeriesData(XySeriesData):
+ """
+ The data specific to a particular Bubble chart series. It provides access
+ to the series label, the series data points, and an optional number
+ format to be applied to each data point not having a specified number
+ format.
+
+ The sequence of data points in a bubble chart series is maintained
+ throughout the chart building process because a data point has no unique
+ identifier and can only be retrieved by index.
+ """
+
+ def add_data_point(self, x, y, size, number_format=None):
+ """
+ Append a new BubbleDataPoint object having the values *x*, *y*, and
+ *size*. The optional *number_format* is used to format the Y value.
+ If not provided, the number format is inherited from the series data.
+ """
+ data_point = BubbleDataPoint(self, x, y, size, number_format)
+ self.append(data_point)
+ return data_point
+
+ @property
+ def bubble_sizes(self):
+ """
+ A sequence containing the bubble size for each datapoint in this
+ series, in data point order.
+ """
+ return [dp.bubble_size for dp in self._data_points]
+
+ @property
+ def bubble_sizes_ref(self):
+ """
+ The Excel worksheet reference for the range containing the bubble
+ sizes for this series.
+ """
+ return self._chart_data.bubble_sizes_ref(self)
+
+
+class CategoryDataPoint(_BaseDataPoint):
+ """
+ A data point in a category chart series. Provides access to the value of
+ the datapoint and the number format with which it should appear in the
+ Excel file.
+ """
+
+ def __init__(self, series_data, value, number_format):
+ super(CategoryDataPoint, self).__init__(series_data, number_format)
+ self._value = value
+
+ @property
+ def value(self):
+ """
+ The (Y) value for this category data point.
+ """
+ return self._value
+
+
+class XyDataPoint(_BaseDataPoint):
+ """
+ A data point in an XY chart series. Provides access to the x and y values
+ of the datapoint.
+ """
+
+ def __init__(self, series_data, x, y, number_format):
+ super(XyDataPoint, self).__init__(series_data, number_format)
+ self._x = x
+ self._y = y
+
+ @property
+ def x(self):
+ """
+ The X value for this XY data point.
+ """
+ return self._x
+
+ @property
+ def y(self):
+ """
+ The Y value for this XY data point.
+ """
+ return self._y
+
+
+class BubbleDataPoint(XyDataPoint):
+ """
+ A data point in a bubble chart series. Provides access to the x, y, and
+ size values of the datapoint.
+ """
+
+ def __init__(self, series_data, x, y, size, number_format):
+ super(BubbleDataPoint, self).__init__(series_data, x, y, number_format)
+ self._size = size
+
+ @property
+ def bubble_size(self):
+ """
+ The value representing the size of the bubble for this data point.
+ """
+ return self._size
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/datalabel.py b/.venv/lib/python3.12/site-packages/pptx/chart/datalabel.py
new file mode 100644
index 00000000..af7cdf5c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/datalabel.py
@@ -0,0 +1,288 @@
+"""Data label-related objects."""
+
+from __future__ import annotations
+
+from pptx.text.text import Font, TextFrame
+from pptx.util import lazyproperty
+
+
+class DataLabels(object):
+ """Provides access to properties of data labels for a plot or a series.
+
+ This is not a collection and does not provide access to individual data
+ labels. Access to individual labels is via the |Point| object. The
+ properties this object provides control formatting of *all* the data
+ labels in its scope.
+ """
+
+ def __init__(self, dLbls):
+ super(DataLabels, self).__init__()
+ self._element = dLbls
+
+ @lazyproperty
+ def font(self):
+ """
+ The |Font| object that provides access to the text properties for
+ these data labels, such as bold, italic, etc.
+ """
+ defRPr = self._element.defRPr
+ font = Font(defRPr)
+ return font
+
+ @property
+ def number_format(self):
+ """
+ Read/write string specifying the format for the numbers on this set
+ of data labels. Returns 'General' if no number format has been set.
+ Note that this format string has no effect on rendered data labels
+ when :meth:`number_format_is_linked` is |True|. Assigning a format
+ string to this property automatically sets
+ :meth:`number_format_is_linked` to |False|.
+ """
+ numFmt = self._element.numFmt
+ if numFmt is None:
+ return "General"
+ return numFmt.formatCode
+
+ @number_format.setter
+ def number_format(self, value):
+ self._element.get_or_add_numFmt().formatCode = value
+ self.number_format_is_linked = False
+
+ @property
+ def number_format_is_linked(self):
+ """
+ Read/write boolean specifying whether number formatting should be
+ taken from the source spreadsheet rather than the value of
+ :meth:`number_format`.
+ """
+ numFmt = self._element.numFmt
+ if numFmt is None:
+ return True
+ souceLinked = numFmt.sourceLinked
+ if souceLinked is None:
+ return True
+ return numFmt.sourceLinked
+
+ @number_format_is_linked.setter
+ def number_format_is_linked(self, value):
+ numFmt = self._element.get_or_add_numFmt()
+ numFmt.sourceLinked = value
+
+ @property
+ def position(self):
+ """
+ Read/write :ref:`XlDataLabelPosition` enumeration value specifying
+ the position of the data labels with respect to their data point, or
+ |None| if no position is specified. Assigning |None| causes
+ PowerPoint to choose the default position, which varies by chart
+ type.
+ """
+ dLblPos = self._element.dLblPos
+ if dLblPos is None:
+ return None
+ return dLblPos.val
+
+ @position.setter
+ def position(self, value):
+ if value is None:
+ self._element._remove_dLblPos()
+ return
+ self._element.get_or_add_dLblPos().val = value
+
+ @property
+ def show_category_name(self):
+ """Read/write. True when name of category should appear in label."""
+ return self._element.get_or_add_showCatName().val
+
+ @show_category_name.setter
+ def show_category_name(self, value):
+ self._element.get_or_add_showCatName().val = bool(value)
+
+ @property
+ def show_legend_key(self):
+ """Read/write. True when data label displays legend-color swatch."""
+ return self._element.get_or_add_showLegendKey().val
+
+ @show_legend_key.setter
+ def show_legend_key(self, value):
+ self._element.get_or_add_showLegendKey().val = bool(value)
+
+ @property
+ def show_percentage(self):
+ """Read/write. True when data label displays percentage.
+
+ This option is not operative on all chart types. Percentage appears
+ on polar charts such as pie and donut.
+ """
+ return self._element.get_or_add_showPercent().val
+
+ @show_percentage.setter
+ def show_percentage(self, value):
+ self._element.get_or_add_showPercent().val = bool(value)
+
+ @property
+ def show_series_name(self):
+ """Read/write. True when data label displays series name."""
+ return self._element.get_or_add_showSerName().val
+
+ @show_series_name.setter
+ def show_series_name(self, value):
+ self._element.get_or_add_showSerName().val = bool(value)
+
+ @property
+ def show_value(self):
+ """Read/write. True when label displays numeric value of datapoint."""
+ return self._element.get_or_add_showVal().val
+
+ @show_value.setter
+ def show_value(self, value):
+ self._element.get_or_add_showVal().val = bool(value)
+
+
+class DataLabel(object):
+ """
+ The data label associated with an individual data point.
+ """
+
+ def __init__(self, ser, idx):
+ super(DataLabel, self).__init__()
+ self._ser = self._element = ser
+ self._idx = idx
+
+ @lazyproperty
+ def font(self):
+ """The |Font| object providing text formatting for this data label.
+
+ This font object is used to customize the appearance of automatically
+ inserted text, such as the data point value. The font applies to the
+ entire data label. More granular control of the appearance of custom
+ data label text is controlled by a font object on runs in the text
+ frame.
+ """
+ txPr = self._get_or_add_txPr()
+ text_frame = TextFrame(txPr, self)
+ paragraph = text_frame.paragraphs[0]
+ return paragraph.font
+
+ @property
+ def has_text_frame(self):
+ """
+ Return |True| if this data label has a text frame (implying it has
+ custom data label text), and |False| otherwise. Assigning |True|
+ causes a text frame to be added if not already present. Assigning
+ |False| causes any existing text frame to be removed along with any
+ text contained in the text frame.
+ """
+ dLbl = self._dLbl
+ if dLbl is None:
+ return False
+ if dLbl.xpath("c:tx/c:rich"):
+ return True
+ return False
+
+ @has_text_frame.setter
+ def has_text_frame(self, value):
+ if bool(value) is True:
+ self._get_or_add_tx_rich()
+ else:
+ self._remove_tx_rich()
+
+ @property
+ def position(self):
+ """
+ Read/write :ref:`XlDataLabelPosition` member specifying the position
+ of this data label with respect to its data point, or |None| if no
+ position is specified. Assigning |None| causes PowerPoint to choose
+ the default position, which varies by chart type.
+ """
+ dLbl = self._dLbl
+ if dLbl is None:
+ return None
+ dLblPos = dLbl.dLblPos
+ if dLblPos is None:
+ return None
+ return dLblPos.val
+
+ @position.setter
+ def position(self, value):
+ if value is None:
+ dLbl = self._dLbl
+ if dLbl is None:
+ return
+ dLbl._remove_dLblPos()
+ return
+ dLbl = self._get_or_add_dLbl()
+ dLbl.get_or_add_dLblPos().val = value
+
+ @property
+ def text_frame(self):
+ """
+ |TextFrame| instance for this data label, containing the text of the
+ data label and providing access to its text formatting properties.
+ """
+ rich = self._get_or_add_rich()
+ return TextFrame(rich, self)
+
+ @property
+ def _dLbl(self):
+ """
+ Return the |CT_DLbl| instance referring specifically to this
+ individual data label (having the same index value), or |None| if not
+ present.
+ """
+ return self._ser.get_dLbl(self._idx)
+
+ def _get_or_add_dLbl(self):
+ """
+ The ``CT_DLbl`` instance referring specifically to this individual
+ data label, newly created if not yet present in the XML.
+ """
+ return self._ser.get_or_add_dLbl(self._idx)
+
+ def _get_or_add_rich(self):
+ """
+ Return the `c:rich` element representing the text frame for this data
+ label, newly created with its ancestors if not present.
+ """
+ dLbl = self._get_or_add_dLbl()
+
+ # having a c:spPr or c:txPr when a c:tx is present causes the "can't
+ # save" bug on bubble charts. Remove c:spPr and c:txPr when present.
+ dLbl._remove_spPr()
+ dLbl._remove_txPr()
+
+ return dLbl.get_or_add_rich()
+
+ def _get_or_add_tx_rich(self):
+ """
+ Return the `c:tx` element for this data label, with its `c:rich`
+ child and descendants, newly created if not yet present.
+ """
+ dLbl = self._get_or_add_dLbl()
+
+ # having a c:spPr or c:txPr when a c:tx is present causes the "can't
+ # save" bug on bubble charts. Remove c:spPr and c:txPr when present.
+ dLbl._remove_spPr()
+ dLbl._remove_txPr()
+
+ return dLbl.get_or_add_tx_rich()
+
+ def _get_or_add_txPr(self):
+ """Return the `c:txPr` element for this data label.
+
+ The `c:txPr` element and its parent `c:dLbl` element are created if
+ not yet present.
+ """
+ dLbl = self._get_or_add_dLbl()
+ return dLbl.get_or_add_txPr()
+
+ def _remove_tx_rich(self):
+ """
+ Remove any `c:tx/c:rich` child of the `c:dLbl` element for this data
+ label. Do nothing if that element is not present.
+ """
+ dLbl = self._dLbl
+ if dLbl is None:
+ return
+ dLbl.remove_tx_rich()
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/legend.py b/.venv/lib/python3.12/site-packages/pptx/chart/legend.py
new file mode 100644
index 00000000..9bc64dbf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/legend.py
@@ -0,0 +1,79 @@
+"""Legend of a chart."""
+
+from __future__ import annotations
+
+from pptx.enum.chart import XL_LEGEND_POSITION
+from pptx.text.text import Font
+from pptx.util import lazyproperty
+
+
+class Legend(object):
+ """
+ Represents the legend in a chart. A chart can have at most one legend.
+ """
+
+ def __init__(self, legend_elm):
+ super(Legend, self).__init__()
+ self._element = legend_elm
+
+ @lazyproperty
+ def font(self):
+ """
+ The |Font| object that provides access to the text properties for
+ this legend, such as bold, italic, etc.
+ """
+ defRPr = self._element.defRPr
+ font = Font(defRPr)
+ return font
+
+ @property
+ def horz_offset(self):
+ """
+ Adjustment of the x position of the legend from its default.
+ Expressed as a float between -1.0 and 1.0 representing a fraction of
+ the chart width. Negative values move the legend left, positive
+ values move it to the right. |None| if no setting is specified.
+ """
+ return self._element.horz_offset
+
+ @horz_offset.setter
+ def horz_offset(self, value):
+ self._element.horz_offset = value
+
+ @property
+ def include_in_layout(self):
+ """|True| if legend should be located inside plot area.
+
+ Read/write boolean specifying whether legend should be placed inside
+ the plot area. In many cases this will cause it to be superimposed on
+ the chart itself. Assigning |None| to this property causes any
+ `c:overlay` element to be removed, which is interpreted the same as
+ |True|. This use case should rarely be required and assigning
+ a boolean value is recommended.
+ """
+ overlay = self._element.overlay
+ if overlay is None:
+ return True
+ return overlay.val
+
+ @include_in_layout.setter
+ def include_in_layout(self, value):
+ if value is None:
+ self._element._remove_overlay()
+ return
+ self._element.get_or_add_overlay().val = bool(value)
+
+ @property
+ def position(self):
+ """
+ Read/write :ref:`XlLegendPosition` enumeration value specifying the
+ general region of the chart in which to place the legend.
+ """
+ legendPos = self._element.legendPos
+ if legendPos is None:
+ return XL_LEGEND_POSITION.RIGHT
+ return legendPos.val
+
+ @position.setter
+ def position(self, position):
+ self._element.get_or_add_legendPos().val = position
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/marker.py b/.venv/lib/python3.12/site-packages/pptx/chart/marker.py
new file mode 100644
index 00000000..cd4b7f02
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/marker.py
@@ -0,0 +1,70 @@
+"""Marker-related objects.
+
+Only the line-type charts Line, XY, and Radar have markers.
+"""
+
+from __future__ import annotations
+
+from pptx.dml.chtfmt import ChartFormat
+from pptx.shared import ElementProxy
+from pptx.util import lazyproperty
+
+
+class Marker(ElementProxy):
+ """
+ Represents a data point marker, such as a diamond or circle, on
+ a line-type chart.
+ """
+
+ @lazyproperty
+ def format(self):
+ """
+ The |ChartFormat| instance for this marker, providing access to shape
+ properties such as fill and line.
+ """
+ marker = self._element.get_or_add_marker()
+ return ChartFormat(marker)
+
+ @property
+ def size(self):
+ """
+ An integer between 2 and 72 inclusive indicating the size of this
+ marker in points. A value of |None| indicates no explicit value is
+ set and the size is inherited from a higher-level setting or the
+ PowerPoint default (which may be 9). Assigning |None| removes any
+ explicitly assigned size, causing this value to be inherited.
+ """
+ marker = self._element.marker
+ if marker is None:
+ return None
+ return marker.size_val
+
+ @size.setter
+ def size(self, value):
+ marker = self._element.get_or_add_marker()
+ marker._remove_size()
+ if value is None:
+ return
+ size = marker._add_size()
+ size.val = value
+
+ @property
+ def style(self):
+ """
+ A member of the :ref:`XlMarkerStyle` enumeration indicating the shape
+ of this marker. Returns |None| if no explicit style has been set,
+ which corresponds to the "Automatic" option in the PowerPoint UI.
+ """
+ marker = self._element.marker
+ if marker is None:
+ return None
+ return marker.symbol_val
+
+ @style.setter
+ def style(self, value):
+ marker = self._element.get_or_add_marker()
+ marker._remove_symbol()
+ if value is None:
+ return
+ symbol = marker._add_symbol()
+ symbol.val = value
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/plot.py b/.venv/lib/python3.12/site-packages/pptx/chart/plot.py
new file mode 100644
index 00000000..6e723585
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/plot.py
@@ -0,0 +1,412 @@
+"""Plot-related objects.
+
+A plot is known as a chart group in the MS API. A chart can have more than one plot overlayed on
+each other, such as a line plot layered over a bar plot.
+"""
+
+from __future__ import annotations
+
+from pptx.chart.category import Categories
+from pptx.chart.datalabel import DataLabels
+from pptx.chart.series import SeriesCollection
+from pptx.enum.chart import XL_CHART_TYPE as XL
+from pptx.oxml.ns import qn
+from pptx.oxml.simpletypes import ST_BarDir, ST_Grouping
+from pptx.util import lazyproperty
+
+
+class _BasePlot(object):
+ """
+ A distinct plot that appears in the plot area of a chart. A chart may
+ have more than one plot, in which case they appear as superimposed
+ layers, such as a line plot appearing on top of a bar chart.
+ """
+
+ def __init__(self, xChart, chart):
+ super(_BasePlot, self).__init__()
+ self._element = xChart
+ self._chart = chart
+
+ @lazyproperty
+ def categories(self):
+ """
+ Returns a |category.Categories| sequence object containing
+ a |category.Category| object for each of the category labels
+ associated with this plot. The |category.Category| class derives from
+ ``str``, so the returned value can be treated as a simple sequence of
+ strings for the common case where all you need is the labels in the
+ order they appear on the chart. |category.Categories| provides
+ additional properties for dealing with hierarchical categories when
+ required.
+ """
+ return Categories(self._element)
+
+ @property
+ def chart(self):
+ """
+ The |Chart| object containing this plot.
+ """
+ return self._chart
+
+ @property
+ def data_labels(self):
+ """
+ |DataLabels| instance providing properties and methods on the
+ collection of data labels associated with this plot.
+ """
+ dLbls = self._element.dLbls
+ if dLbls is None:
+ raise ValueError("plot has no data labels, set has_data_labels = True first")
+ return DataLabels(dLbls)
+
+ @property
+ def has_data_labels(self):
+ """
+ Read/write boolean, |True| if the series has data labels. Assigning
+ |True| causes data labels to be added to the plot. Assigning False
+ removes any existing data labels.
+ """
+ return self._element.dLbls is not None
+
+ @has_data_labels.setter
+ def has_data_labels(self, value):
+ """
+ Add, remove, or leave alone the ``<c:dLbls>`` child element depending
+ on current state and assigned *value*. If *value* is |True| and no
+ ``<c:dLbls>`` element is present, a new default element is added with
+ default child elements and settings. When |False|, any existing dLbls
+ element is removed.
+ """
+ if bool(value) is False:
+ self._element._remove_dLbls()
+ else:
+ if self._element.dLbls is None:
+ dLbls = self._element._add_dLbls()
+ dLbls.showVal.val = True
+
+ @lazyproperty
+ def series(self):
+ """
+ A sequence of |Series| objects representing the series in this plot,
+ in the order they appear in the plot.
+ """
+ return SeriesCollection(self._element)
+
+ @property
+ def vary_by_categories(self):
+ """
+ Read/write boolean value specifying whether to use a different color
+ for each of the points in this plot. Only effective when there is
+ a single series; PowerPoint automatically varies color by series when
+ more than one series is present.
+ """
+ varyColors = self._element.varyColors
+ if varyColors is None:
+ return True
+ return varyColors.val
+
+ @vary_by_categories.setter
+ def vary_by_categories(self, value):
+ self._element.get_or_add_varyColors().val = bool(value)
+
+
+class AreaPlot(_BasePlot):
+ """
+ An area plot.
+ """
+
+
+class Area3DPlot(_BasePlot):
+ """
+ A 3-dimensional area plot.
+ """
+
+
+class BarPlot(_BasePlot):
+ """
+ A bar chart-style plot.
+ """
+
+ @property
+ def gap_width(self):
+ """
+ Width of gap between bar(s) of each category, as an integer
+ percentage of the bar width. The default value for a new bar chart is
+ 150, representing 150% or 1.5 times the width of a single bar.
+ """
+ gapWidth = self._element.gapWidth
+ if gapWidth is None:
+ return 150
+ return gapWidth.val
+
+ @gap_width.setter
+ def gap_width(self, value):
+ gapWidth = self._element.get_or_add_gapWidth()
+ gapWidth.val = value
+
+ @property
+ def overlap(self):
+ """
+ Read/write int value in range -100..100 specifying a percentage of
+ the bar width by which to overlap adjacent bars in a multi-series bar
+ chart. Default is 0. A setting of -100 creates a gap of a full bar
+ width and a setting of 100 causes all the bars in a category to be
+ superimposed. A stacked bar plot has overlap of 100 by default.
+ """
+ overlap = self._element.overlap
+ if overlap is None:
+ return 0
+ return overlap.val
+
+ @overlap.setter
+ def overlap(self, value):
+ """
+ Set the value of the ``<c:overlap>`` child element to *int_value*,
+ or remove the overlap element if *int_value* is 0.
+ """
+ if value == 0:
+ self._element._remove_overlap()
+ return
+ self._element.get_or_add_overlap().val = value
+
+
+class BubblePlot(_BasePlot):
+ """
+ A bubble chart plot.
+ """
+
+ @property
+ def bubble_scale(self):
+ """
+ An integer between 0 and 300 inclusive indicating the percentage of
+ the default size at which bubbles should be displayed. Assigning
+ |None| produces the same behavior as assigning `100`.
+ """
+ bubbleScale = self._element.bubbleScale
+ if bubbleScale is None:
+ return 100
+ return bubbleScale.val
+
+ @bubble_scale.setter
+ def bubble_scale(self, value):
+ bubbleChart = self._element
+ bubbleChart._remove_bubbleScale()
+ if value is None:
+ return
+ bubbleScale = bubbleChart._add_bubbleScale()
+ bubbleScale.val = value
+
+
+class DoughnutPlot(_BasePlot):
+ """
+ An doughnut plot.
+ """
+
+
+class LinePlot(_BasePlot):
+ """
+ A line chart-style plot.
+ """
+
+
+class PiePlot(_BasePlot):
+ """
+ A pie chart-style plot.
+ """
+
+
+class RadarPlot(_BasePlot):
+ """
+ A radar-style plot.
+ """
+
+
+class XyPlot(_BasePlot):
+ """
+ An XY (scatter) plot.
+ """
+
+
+def PlotFactory(xChart, chart):
+ """
+ Return an instance of the appropriate subclass of _BasePlot based on the
+ tagname of *xChart*.
+ """
+ try:
+ PlotCls = {
+ qn("c:areaChart"): AreaPlot,
+ qn("c:area3DChart"): Area3DPlot,
+ qn("c:barChart"): BarPlot,
+ qn("c:bubbleChart"): BubblePlot,
+ qn("c:doughnutChart"): DoughnutPlot,
+ qn("c:lineChart"): LinePlot,
+ qn("c:pieChart"): PiePlot,
+ qn("c:radarChart"): RadarPlot,
+ qn("c:scatterChart"): XyPlot,
+ }[xChart.tag]
+ except KeyError:
+ raise ValueError("unsupported plot type %s" % xChart.tag)
+
+ return PlotCls(xChart, chart)
+
+
+class PlotTypeInspector(object):
+ """
+ "One-shot" service object that knows how to identify the type of a plot
+ as a member of the XL_CHART_TYPE enumeration.
+ """
+
+ @classmethod
+ def chart_type(cls, plot):
+ """
+ Return the member of :ref:`XlChartType` that corresponds to the chart
+ type of *plot*.
+ """
+ try:
+ chart_type_method = {
+ "AreaPlot": cls._differentiate_area_chart_type,
+ "Area3DPlot": cls._differentiate_area_3d_chart_type,
+ "BarPlot": cls._differentiate_bar_chart_type,
+ "BubblePlot": cls._differentiate_bubble_chart_type,
+ "DoughnutPlot": cls._differentiate_doughnut_chart_type,
+ "LinePlot": cls._differentiate_line_chart_type,
+ "PiePlot": cls._differentiate_pie_chart_type,
+ "RadarPlot": cls._differentiate_radar_chart_type,
+ "XyPlot": cls._differentiate_xy_chart_type,
+ }[plot.__class__.__name__]
+ except KeyError:
+ raise NotImplementedError(
+ "chart_type() not implemented for %s" % plot.__class__.__name__
+ )
+ return chart_type_method(plot)
+
+ @classmethod
+ def _differentiate_area_3d_chart_type(cls, plot):
+ return {
+ ST_Grouping.STANDARD: XL.THREE_D_AREA,
+ ST_Grouping.STACKED: XL.THREE_D_AREA_STACKED,
+ ST_Grouping.PERCENT_STACKED: XL.THREE_D_AREA_STACKED_100,
+ }[plot._element.grouping_val]
+
+ @classmethod
+ def _differentiate_area_chart_type(cls, plot):
+ return {
+ ST_Grouping.STANDARD: XL.AREA,
+ ST_Grouping.STACKED: XL.AREA_STACKED,
+ ST_Grouping.PERCENT_STACKED: XL.AREA_STACKED_100,
+ }[plot._element.grouping_val]
+
+ @classmethod
+ def _differentiate_bar_chart_type(cls, plot):
+ barChart = plot._element
+ if barChart.barDir.val == ST_BarDir.BAR:
+ return {
+ ST_Grouping.CLUSTERED: XL.BAR_CLUSTERED,
+ ST_Grouping.STACKED: XL.BAR_STACKED,
+ ST_Grouping.PERCENT_STACKED: XL.BAR_STACKED_100,
+ }[barChart.grouping_val]
+ if barChart.barDir.val == ST_BarDir.COL:
+ return {
+ ST_Grouping.CLUSTERED: XL.COLUMN_CLUSTERED,
+ ST_Grouping.STACKED: XL.COLUMN_STACKED,
+ ST_Grouping.PERCENT_STACKED: XL.COLUMN_STACKED_100,
+ }[barChart.grouping_val]
+ raise ValueError("invalid barChart.barDir value '%s'" % barChart.barDir.val)
+
+ @classmethod
+ def _differentiate_bubble_chart_type(cls, plot):
+ def first_bubble3D(bubbleChart):
+ results = bubbleChart.xpath("c:ser/c:bubble3D")
+ return results[0] if results else None
+
+ bubbleChart = plot._element
+ bubble3D = first_bubble3D(bubbleChart)
+
+ if bubble3D is None:
+ return XL.BUBBLE
+ if bubble3D.val:
+ return XL.BUBBLE_THREE_D_EFFECT
+ return XL.BUBBLE
+
+ @classmethod
+ def _differentiate_doughnut_chart_type(cls, plot):
+ doughnutChart = plot._element
+ explosion = doughnutChart.xpath("./c:ser/c:explosion")
+ return XL.DOUGHNUT_EXPLODED if explosion else XL.DOUGHNUT
+
+ @classmethod
+ def _differentiate_line_chart_type(cls, plot):
+ lineChart = plot._element
+
+ def has_line_markers():
+ matches = lineChart.xpath('c:ser/c:marker/c:symbol[@val="none"]')
+ if matches:
+ return False
+ return True
+
+ if has_line_markers():
+ return {
+ ST_Grouping.STANDARD: XL.LINE_MARKERS,
+ ST_Grouping.STACKED: XL.LINE_MARKERS_STACKED,
+ ST_Grouping.PERCENT_STACKED: XL.LINE_MARKERS_STACKED_100,
+ }[plot._element.grouping_val]
+ else:
+ return {
+ ST_Grouping.STANDARD: XL.LINE,
+ ST_Grouping.STACKED: XL.LINE_STACKED,
+ ST_Grouping.PERCENT_STACKED: XL.LINE_STACKED_100,
+ }[plot._element.grouping_val]
+
+ @classmethod
+ def _differentiate_pie_chart_type(cls, plot):
+ pieChart = plot._element
+ explosion = pieChart.xpath("./c:ser/c:explosion")
+ return XL.PIE_EXPLODED if explosion else XL.PIE
+
+ @classmethod
+ def _differentiate_radar_chart_type(cls, plot):
+ radarChart = plot._element
+ radar_style = radarChart.xpath("c:radarStyle")[0].get("val")
+
+ def noMarkers():
+ matches = radarChart.xpath("c:ser/c:marker/c:symbol")
+ if matches and matches[0].get("val") == "none":
+ return True
+ return False
+
+ if radar_style is None:
+ return XL.RADAR
+ if radar_style == "filled":
+ return XL.RADAR_FILLED
+ if noMarkers():
+ return XL.RADAR
+ return XL.RADAR_MARKERS
+
+ @classmethod
+ def _differentiate_xy_chart_type(cls, plot):
+ scatterChart = plot._element
+
+ def noLine():
+ return bool(scatterChart.xpath("c:ser/c:spPr/a:ln/a:noFill"))
+
+ def noMarkers():
+ symbols = scatterChart.xpath("c:ser/c:marker/c:symbol")
+ if symbols and symbols[0].get("val") == "none":
+ return True
+ return False
+
+ scatter_style = scatterChart.xpath("c:scatterStyle")[0].get("val")
+
+ if scatter_style == "lineMarker":
+ if noLine():
+ return XL.XY_SCATTER
+ if noMarkers():
+ return XL.XY_SCATTER_LINES_NO_MARKERS
+ return XL.XY_SCATTER_LINES
+
+ if scatter_style == "smoothMarker":
+ if noMarkers():
+ return XL.XY_SCATTER_SMOOTH_NO_MARKERS
+ return XL.XY_SCATTER_SMOOTH
+
+ return XL.XY_SCATTER
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/point.py b/.venv/lib/python3.12/site-packages/pptx/chart/point.py
new file mode 100644
index 00000000..2d42436c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/point.py
@@ -0,0 +1,101 @@
+"""Data point-related objects."""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+from pptx.chart.datalabel import DataLabel
+from pptx.chart.marker import Marker
+from pptx.dml.chtfmt import ChartFormat
+from pptx.util import lazyproperty
+
+
+class _BasePoints(Sequence):
+ """
+ Sequence providing access to the individual data points in a series.
+ """
+
+ def __init__(self, ser):
+ super(_BasePoints, self).__init__()
+ self._element = ser
+ self._ser = ser
+
+ def __getitem__(self, idx):
+ if idx < 0 or idx >= self.__len__():
+ raise IndexError("point index out of range")
+ return Point(self._ser, idx)
+
+
+class BubblePoints(_BasePoints):
+ """
+ Sequence providing access to the individual data points in
+ a |BubbleSeries| object.
+ """
+
+ def __len__(self):
+ return min(
+ self._ser.xVal_ptCount_val,
+ self._ser.yVal_ptCount_val,
+ self._ser.bubbleSize_ptCount_val,
+ )
+
+
+class CategoryPoints(_BasePoints):
+ """
+ Sequence providing access to individual |Point| objects, each
+ representing the visual properties of a data point in the specified
+ category series.
+ """
+
+ def __len__(self):
+ return self._ser.cat_ptCount_val
+
+
+class Point(object):
+ """
+ Provides access to the properties of an individual data point in
+ a series, such as the visual properties of its marker and the text and
+ font of its data label.
+ """
+
+ def __init__(self, ser, idx):
+ super(Point, self).__init__()
+ self._element = ser
+ self._ser = ser
+ self._idx = idx
+
+ @lazyproperty
+ def data_label(self):
+ """
+ The |DataLabel| object representing the label on this data point.
+ """
+ return DataLabel(self._ser, self._idx)
+
+ @lazyproperty
+ def format(self):
+ """
+ The |ChartFormat| object providing access to the shape formatting
+ properties of this data point, such as line and fill.
+ """
+ dPt = self._ser.get_or_add_dPt_for_point(self._idx)
+ return ChartFormat(dPt)
+
+ @lazyproperty
+ def marker(self):
+ """
+ The |Marker| instance for this point, providing access to the visual
+ properties of the data point marker, such as fill and line. Setting
+ these properties overrides any value set at the series level.
+ """
+ dPt = self._ser.get_or_add_dPt_for_point(self._idx)
+ return Marker(dPt)
+
+
+class XyPoints(_BasePoints):
+ """
+ Sequence providing access to the individual data points in an |XySeries|
+ object.
+ """
+
+ def __len__(self):
+ return min(self._ser.xVal_ptCount_val, self._ser.yVal_ptCount_val)
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/series.py b/.venv/lib/python3.12/site-packages/pptx/chart/series.py
new file mode 100644
index 00000000..16112eab
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/series.py
@@ -0,0 +1,258 @@
+"""Series-related objects."""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+from pptx.chart.datalabel import DataLabels
+from pptx.chart.marker import Marker
+from pptx.chart.point import BubblePoints, CategoryPoints, XyPoints
+from pptx.dml.chtfmt import ChartFormat
+from pptx.oxml.ns import qn
+from pptx.util import lazyproperty
+
+
+class _BaseSeries(object):
+ """
+ Base class for |BarSeries| and other series classes.
+ """
+
+ def __init__(self, ser):
+ super(_BaseSeries, self).__init__()
+ self._element = ser
+ self._ser = ser
+
+ @lazyproperty
+ def format(self):
+ """
+ The |ChartFormat| instance for this series, providing access to shape
+ properties such as fill and line.
+ """
+ return ChartFormat(self._ser)
+
+ @property
+ def index(self):
+ """
+ The zero-based integer index of this series as reported in its
+ `c:ser/c:idx` element.
+ """
+ return self._element.idx.val
+
+ @property
+ def name(self):
+ """
+ The string label given to this series, appears as the title of the
+ column for this series in the Excel worksheet. It also appears as the
+ label for this series in the legend.
+ """
+ names = self._element.xpath("./c:tx//c:pt/c:v/text()")
+ name = names[0] if names else ""
+ return name
+
+
+class _BaseCategorySeries(_BaseSeries):
+ """Base class for |BarSeries| and other category chart series classes."""
+
+ @lazyproperty
+ def data_labels(self):
+ """|DataLabels| object controlling data labels for this series."""
+ return DataLabels(self._ser.get_or_add_dLbls())
+
+ @lazyproperty
+ def points(self):
+ """
+ The |CategoryPoints| object providing access to individual data
+ points in this series.
+ """
+ return CategoryPoints(self._ser)
+
+ @property
+ def values(self):
+ """
+ Read-only. A sequence containing the float values for this series, in
+ the order they appear on the chart.
+ """
+
+ def iter_values():
+ val = self._element.val
+ if val is None:
+ return
+ for idx in range(val.ptCount_val):
+ yield val.pt_v(idx)
+
+ return tuple(iter_values())
+
+
+class _MarkerMixin(object):
+ """
+ Mixin class providing `.marker` property for line-type chart series. The
+ line-type charts are Line, XY, and Radar.
+ """
+
+ @lazyproperty
+ def marker(self):
+ """
+ The |Marker| instance for this series, providing access to data point
+ marker properties such as fill and line. Setting these properties
+ determines the appearance of markers for all points in this series
+ that are not overridden by settings at the point level.
+ """
+ return Marker(self._ser)
+
+
+class AreaSeries(_BaseCategorySeries):
+ """
+ A data point series belonging to an area plot.
+ """
+
+
+class BarSeries(_BaseCategorySeries):
+ """A data point series belonging to a bar plot."""
+
+ @property
+ def invert_if_negative(self):
+ """
+ |True| if a point having a value less than zero should appear with a
+ fill different than those with a positive value. |False| if the fill
+ should be the same regardless of the bar's value. When |True|, a bar
+ with a solid fill appears with white fill; in a bar with gradient
+ fill, the direction of the gradient is reversed, e.g. dark -> light
+ instead of light -> dark. The term "invert" here should be understood
+ to mean "invert the *direction* of the *fill gradient*".
+ """
+ invertIfNegative = self._element.invertIfNegative
+ if invertIfNegative is None:
+ return True
+ return invertIfNegative.val
+
+ @invert_if_negative.setter
+ def invert_if_negative(self, value):
+ invertIfNegative = self._element.get_or_add_invertIfNegative()
+ invertIfNegative.val = value
+
+
+class LineSeries(_BaseCategorySeries, _MarkerMixin):
+ """
+ A data point series belonging to a line plot.
+ """
+
+ @property
+ def smooth(self):
+ """
+ Read/write boolean specifying whether to use curve smoothing to
+ form the line connecting the data points in this series into
+ a continuous curve. If |False|, a series of straight line segments
+ are used to connect the points.
+ """
+ smooth = self._element.smooth
+ if smooth is None:
+ return True
+ return smooth.val
+
+ @smooth.setter
+ def smooth(self, value):
+ self._element.get_or_add_smooth().val = value
+
+
+class PieSeries(_BaseCategorySeries):
+ """
+ A data point series belonging to a pie plot.
+ """
+
+
+class RadarSeries(_BaseCategorySeries, _MarkerMixin):
+ """
+ A data point series belonging to a radar plot.
+ """
+
+
+class XySeries(_BaseSeries, _MarkerMixin):
+ """
+ A data point series belonging to an XY (scatter) plot.
+ """
+
+ def iter_values(self):
+ """
+ Generate each float Y value in this series, in the order they appear
+ on the chart. A value of `None` represents a missing Y value
+ (corresponding to a blank Excel cell).
+ """
+ yVal = self._element.yVal
+ if yVal is None:
+ return
+
+ for idx in range(yVal.ptCount_val):
+ yield yVal.pt_v(idx)
+
+ @lazyproperty
+ def points(self):
+ """
+ The |XyPoints| object providing access to individual data points in
+ this series.
+ """
+ return XyPoints(self._ser)
+
+ @property
+ def values(self):
+ """
+ Read-only. A sequence containing the float values for this series, in
+ the order they appear on the chart.
+ """
+ return tuple(self.iter_values())
+
+
+class BubbleSeries(XySeries):
+ """
+ A data point series belonging to a bubble plot.
+ """
+
+ @lazyproperty
+ def points(self):
+ """
+ The |BubblePoints| object providing access to individual data point
+ objects used to discover and adjust the formatting and data labels of
+ a data point.
+ """
+ return BubblePoints(self._ser)
+
+
+class SeriesCollection(Sequence):
+ """
+ A sequence of |Series| objects.
+ """
+
+ def __init__(self, parent_elm):
+ # *parent_elm* can be either a c:plotArea or xChart element
+ super(SeriesCollection, self).__init__()
+ self._element = parent_elm
+
+ def __getitem__(self, index):
+ ser = self._element.sers[index]
+ return _SeriesFactory(ser)
+
+ def __len__(self):
+ return len(self._element.sers)
+
+
+def _SeriesFactory(ser):
+ """
+ Return an instance of the appropriate subclass of _BaseSeries based on the
+ xChart element *ser* appears in.
+ """
+ xChart_tag = ser.getparent().tag
+
+ try:
+ SeriesCls = {
+ qn("c:areaChart"): AreaSeries,
+ qn("c:barChart"): BarSeries,
+ qn("c:bubbleChart"): BubbleSeries,
+ qn("c:doughnutChart"): PieSeries,
+ qn("c:lineChart"): LineSeries,
+ qn("c:pieChart"): PieSeries,
+ qn("c:radarChart"): RadarSeries,
+ qn("c:scatterChart"): XySeries,
+ }[xChart_tag]
+ except KeyError:
+ raise NotImplementedError("series class for %s not yet implemented" % xChart_tag)
+
+ return SeriesCls(ser)
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py b/.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py
new file mode 100644
index 00000000..30b21272
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/xlsx.py
@@ -0,0 +1,272 @@
+"""Chart builder and related objects."""
+
+from __future__ import annotations
+
+import io
+from contextlib import contextmanager
+
+from xlsxwriter import Workbook
+
+
+class _BaseWorkbookWriter(object):
+ """Base class for workbook writers, providing shared members."""
+
+ def __init__(self, chart_data):
+ super(_BaseWorkbookWriter, self).__init__()
+ self._chart_data = chart_data
+
+ @property
+ def xlsx_blob(self):
+ """bytes for Excel file containing chart_data."""
+ xlsx_file = io.BytesIO()
+ with self._open_worksheet(xlsx_file) as (workbook, worksheet):
+ self._populate_worksheet(workbook, worksheet)
+ return xlsx_file.getvalue()
+
+ @contextmanager
+ def _open_worksheet(self, xlsx_file):
+ """
+ Enable XlsxWriter Worksheet object to be opened, operated on, and
+ then automatically closed within a `with` statement. A filename or
+ stream object (such as an `io.BytesIO` instance) is expected as
+ *xlsx_file*.
+ """
+ workbook = Workbook(xlsx_file, {"in_memory": True})
+ worksheet = workbook.add_worksheet()
+ yield workbook, worksheet
+ workbook.close()
+
+ def _populate_worksheet(self, workbook, worksheet):
+ """
+ Must be overridden by each subclass to provide the particulars of
+ writing the spreadsheet data.
+ """
+ raise NotImplementedError("must be provided by each subclass")
+
+
+class CategoryWorkbookWriter(_BaseWorkbookWriter):
+ """
+ Determines Excel worksheet layout and can write an Excel workbook from
+ a CategoryChartData object. Serves as the authority for Excel worksheet
+ ranges.
+ """
+
+ @property
+ def categories_ref(self):
+ """
+ The Excel worksheet reference to the categories for this chart (not
+ including the column heading).
+ """
+ categories = self._chart_data.categories
+ if categories.depth == 0:
+ raise ValueError("chart data contains no categories")
+ right_col = chr(ord("A") + categories.depth - 1)
+ bottom_row = categories.leaf_count + 1
+ return "Sheet1!$A$2:$%s$%d" % (right_col, bottom_row)
+
+ def series_name_ref(self, series):
+ """
+ Return the Excel worksheet reference to the cell containing the name
+ for *series*. This also serves as the column heading for the series
+ values.
+ """
+ return "Sheet1!$%s$1" % self._series_col_letter(series)
+
+ def values_ref(self, series):
+ """
+ The Excel worksheet reference to the values for this series (not
+ including the column heading).
+ """
+ return "Sheet1!${col_letter}$2:${col_letter}${bottom_row}".format(
+ **{
+ "col_letter": self._series_col_letter(series),
+ "bottom_row": len(series) + 1,
+ }
+ )
+
+ @staticmethod
+ def _column_reference(column_number):
+ """Return str Excel column reference like 'BQ' for *column_number*.
+
+ *column_number* is an int in the range 1-16384 inclusive, where
+ 1 maps to column 'A'.
+ """
+ if column_number < 1 or column_number > 16384:
+ raise ValueError("column_number must be in range 1-16384")
+
+ # ---Work right-to-left, one order of magnitude at a time. Note there
+ # is no zero representation in Excel address scheme, so this is
+ # not just a conversion to base-26---
+
+ col_ref = ""
+ while column_number:
+ remainder = column_number % 26
+ if remainder == 0:
+ remainder = 26
+
+ col_letter = chr(ord("A") + remainder - 1)
+ col_ref = col_letter + col_ref
+
+ # ---Advance to next order of magnitude or terminate loop. The
+ # minus-one in this expression reflects the fact the next lower
+ # order of magnitude has a minumum value of 1 (not zero). This is
+ # essentially the complement to the "if it's 0 make it 26' step
+ # above.---
+ column_number = (column_number - 1) // 26
+
+ return col_ref
+
+ def _populate_worksheet(self, workbook, worksheet):
+ """
+ Write the chart data contents to *worksheet* in category chart
+ layout. Write categories starting in the first column starting in
+ the second row, and proceeding one column per category level (for
+ charts having multi-level categories). Write series as columns
+ starting in the next following column, placing the series title in
+ the first cell.
+ """
+ self._write_categories(workbook, worksheet)
+ self._write_series(workbook, worksheet)
+
+ def _series_col_letter(self, series):
+ """
+ The letter of the Excel worksheet column in which the data for a
+ series appears.
+ """
+ column_number = 1 + series.categories.depth + series.index
+ return self._column_reference(column_number)
+
+ def _write_categories(self, workbook, worksheet):
+ """
+ Write the categories column(s) to *worksheet*. Categories start in
+ the first column starting in the second row, and proceeding one
+ column per category level (for charts having multi-level categories).
+ A date category is formatted as a date. All others are formatted
+ `General`.
+ """
+ categories = self._chart_data.categories
+ num_format = workbook.add_format({"num_format": categories.number_format})
+ depth = categories.depth
+ for idx, level in enumerate(categories.levels):
+ col = depth - idx - 1
+ self._write_cat_column(worksheet, col, level, num_format)
+
+ def _write_cat_column(self, worksheet, col, level, num_format):
+ """
+ Write a category column defined by *level* to *worksheet* at offset
+ *col* and formatted with *num_format*.
+ """
+ worksheet.set_column(col, col, 10) # wide enough for a date
+ for off, name in level:
+ row = off + 1
+ worksheet.write(row, col, name, num_format)
+
+ def _write_series(self, workbook, worksheet):
+ """
+ Write the series column(s) to *worksheet*. Series start in the column
+ following the last categories column, placing the series title in the
+ first cell.
+ """
+ col_offset = self._chart_data.categories.depth
+ for idx, series in enumerate(self._chart_data):
+ num_format = workbook.add_format({"num_format": series.number_format})
+ series_col = idx + col_offset
+ worksheet.write(0, series_col, series.name)
+ worksheet.write_column(1, series_col, series.values, num_format)
+
+
+class XyWorkbookWriter(_BaseWorkbookWriter):
+ """
+ Determines Excel worksheet layout and can write an Excel workbook from XY
+ chart data. Serves as the authority for Excel worksheet ranges.
+ """
+
+ def series_name_ref(self, series):
+ """
+ Return the Excel worksheet reference to the cell containing the name
+ for *series*. This also serves as the column heading for the series
+ Y values.
+ """
+ row = self.series_table_row_offset(series) + 1
+ return "Sheet1!$B$%d" % row
+
+ def series_table_row_offset(self, series):
+ """
+ Return the number of rows preceding the data table for *series* in
+ the Excel worksheet.
+ """
+ title_and_spacer_rows = series.index * 2
+ data_point_rows = series.data_point_offset
+ return title_and_spacer_rows + data_point_rows
+
+ def x_values_ref(self, series):
+ """
+ The Excel worksheet reference to the X values for this chart (not
+ including the column label).
+ """
+ top_row = self.series_table_row_offset(series) + 2
+ bottom_row = top_row + len(series) - 1
+ return "Sheet1!$A$%d:$A$%d" % (top_row, bottom_row)
+
+ def y_values_ref(self, series):
+ """
+ The Excel worksheet reference to the Y values for this chart (not
+ including the column label).
+ """
+ top_row = self.series_table_row_offset(series) + 2
+ bottom_row = top_row + len(series) - 1
+ return "Sheet1!$B$%d:$B$%d" % (top_row, bottom_row)
+
+ def _populate_worksheet(self, workbook, worksheet):
+ """
+ Write chart data contents to *worksheet* in the standard XY chart
+ layout. Write the data for each series to a separate two-column
+ table, X values in column A and Y values in column B. Place the
+ series label in the first (heading) cell of the column.
+ """
+ chart_num_format = workbook.add_format({"num_format": self._chart_data.number_format})
+ for series in self._chart_data:
+ series_num_format = workbook.add_format({"num_format": series.number_format})
+ offset = self.series_table_row_offset(series)
+ # write X values
+ worksheet.write_column(offset + 1, 0, series.x_values, chart_num_format)
+ # write Y values
+ worksheet.write(offset, 1, series.name)
+ worksheet.write_column(offset + 1, 1, series.y_values, series_num_format)
+
+
+class BubbleWorkbookWriter(XyWorkbookWriter):
+ """
+ Service object that knows how to write an Excel workbook from bubble
+ chart data.
+ """
+
+ def bubble_sizes_ref(self, series):
+ """
+ The Excel worksheet reference to the range containing the bubble
+ sizes for *series* (not including the column heading cell).
+ """
+ top_row = self.series_table_row_offset(series) + 2
+ bottom_row = top_row + len(series) - 1
+ return "Sheet1!$C$%d:$C$%d" % (top_row, bottom_row)
+
+ def _populate_worksheet(self, workbook, worksheet):
+ """
+ Write chart data contents to *worksheet* in the bubble chart layout.
+ Write the data for each series to a separate three-column table with
+ X values in column A, Y values in column B, and bubble sizes in
+ column C. Place the series label in the first (heading) cell of the
+ values column.
+ """
+ chart_num_format = workbook.add_format({"num_format": self._chart_data.number_format})
+ for series in self._chart_data:
+ series_num_format = workbook.add_format({"num_format": series.number_format})
+ offset = self.series_table_row_offset(series)
+ # write X values
+ worksheet.write_column(offset + 1, 0, series.x_values, chart_num_format)
+ # write Y values
+ worksheet.write(offset, 1, series.name)
+ worksheet.write_column(offset + 1, 1, series.y_values, series_num_format)
+ # write bubble sizes
+ worksheet.write(offset, 2, "Size")
+ worksheet.write_column(offset + 1, 2, series.bubble_sizes, chart_num_format)
diff --git a/.venv/lib/python3.12/site-packages/pptx/chart/xmlwriter.py b/.venv/lib/python3.12/site-packages/pptx/chart/xmlwriter.py
new file mode 100644
index 00000000..703c53dd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/chart/xmlwriter.py
@@ -0,0 +1,1840 @@
+"""Composers for default chart XML for various chart types."""
+
+from __future__ import annotations
+
+from copy import deepcopy
+from xml.sax.saxutils import escape
+
+from pptx.enum.chart import XL_CHART_TYPE
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+
+
+def ChartXmlWriter(chart_type, chart_data):
+ """
+ Factory function returning appropriate XML writer object for
+ *chart_type*, loaded with *chart_type* and *chart_data*.
+ """
+ XL_CT = XL_CHART_TYPE
+ try:
+ BuilderCls = {
+ XL_CT.AREA: _AreaChartXmlWriter,
+ XL_CT.AREA_STACKED: _AreaChartXmlWriter,
+ XL_CT.AREA_STACKED_100: _AreaChartXmlWriter,
+ XL_CT.BAR_CLUSTERED: _BarChartXmlWriter,
+ XL_CT.BAR_STACKED: _BarChartXmlWriter,
+ XL_CT.BAR_STACKED_100: _BarChartXmlWriter,
+ XL_CT.BUBBLE: _BubbleChartXmlWriter,
+ XL_CT.BUBBLE_THREE_D_EFFECT: _BubbleChartXmlWriter,
+ XL_CT.COLUMN_CLUSTERED: _BarChartXmlWriter,
+ XL_CT.COLUMN_STACKED: _BarChartXmlWriter,
+ XL_CT.COLUMN_STACKED_100: _BarChartXmlWriter,
+ XL_CT.DOUGHNUT: _DoughnutChartXmlWriter,
+ XL_CT.DOUGHNUT_EXPLODED: _DoughnutChartXmlWriter,
+ XL_CT.LINE: _LineChartXmlWriter,
+ XL_CT.LINE_MARKERS: _LineChartXmlWriter,
+ XL_CT.LINE_MARKERS_STACKED: _LineChartXmlWriter,
+ XL_CT.LINE_MARKERS_STACKED_100: _LineChartXmlWriter,
+ XL_CT.LINE_STACKED: _LineChartXmlWriter,
+ XL_CT.LINE_STACKED_100: _LineChartXmlWriter,
+ XL_CT.PIE: _PieChartXmlWriter,
+ XL_CT.PIE_EXPLODED: _PieChartXmlWriter,
+ XL_CT.RADAR: _RadarChartXmlWriter,
+ XL_CT.RADAR_FILLED: _RadarChartXmlWriter,
+ XL_CT.RADAR_MARKERS: _RadarChartXmlWriter,
+ XL_CT.XY_SCATTER: _XyChartXmlWriter,
+ XL_CT.XY_SCATTER_LINES: _XyChartXmlWriter,
+ XL_CT.XY_SCATTER_LINES_NO_MARKERS: _XyChartXmlWriter,
+ XL_CT.XY_SCATTER_SMOOTH: _XyChartXmlWriter,
+ XL_CT.XY_SCATTER_SMOOTH_NO_MARKERS: _XyChartXmlWriter,
+ }[chart_type]
+ except KeyError:
+ raise NotImplementedError("XML writer for chart type %s not yet implemented" % chart_type)
+ return BuilderCls(chart_type, chart_data)
+
+
+def SeriesXmlRewriterFactory(chart_type, chart_data):
+ """
+ Return a |_BaseSeriesXmlRewriter| subclass appropriate to *chart_type*.
+ """
+ XL_CT = XL_CHART_TYPE
+
+ RewriterCls = {
+ # There are 73 distinct chart types, only specify non-category
+ # types, others default to _CategorySeriesXmlRewriter. Stock-type
+ # charts are multi-plot charts, so no guaratees on how they turn
+ # out.
+ XL_CT.BUBBLE: _BubbleSeriesXmlRewriter,
+ XL_CT.BUBBLE_THREE_D_EFFECT: _BubbleSeriesXmlRewriter,
+ XL_CT.XY_SCATTER: _XySeriesXmlRewriter,
+ XL_CT.XY_SCATTER_LINES: _XySeriesXmlRewriter,
+ XL_CT.XY_SCATTER_LINES_NO_MARKERS: _XySeriesXmlRewriter,
+ XL_CT.XY_SCATTER_SMOOTH: _XySeriesXmlRewriter,
+ XL_CT.XY_SCATTER_SMOOTH_NO_MARKERS: _XySeriesXmlRewriter,
+ }.get(chart_type, _CategorySeriesXmlRewriter)
+
+ return RewriterCls(chart_data)
+
+
+class _BaseChartXmlWriter(object):
+ """
+ Generates XML text (unicode) for a default chart, like the one added by
+ PowerPoint when you click the *Add Column Chart* button on the ribbon.
+ Differentiated XML for different chart types is provided by subclasses.
+ """
+
+ def __init__(self, chart_type, series_seq):
+ super(_BaseChartXmlWriter, self).__init__()
+ self._chart_type = chart_type
+ self._chart_data = series_seq
+ self._series_seq = list(series_seq)
+
+ @property
+ def xml(self):
+ """
+ The full XML stream for the chart specified by this chart builder, as
+ unicode text. This method must be overridden by each subclass.
+ """
+ raise NotImplementedError("must be implemented by all subclasses")
+
+
+class _BaseSeriesXmlWriter(object):
+ """
+ Provides shared members for series XML writers.
+ """
+
+ def __init__(self, series, date_1904=False):
+ super(_BaseSeriesXmlWriter, self).__init__()
+ self._series = series
+ self._date_1904 = date_1904
+
+ @property
+ def name(self):
+ """
+ The XML-escaped name for this series.
+ """
+ return escape(self._series.name)
+
+ def numRef_xml(self, wksht_ref, number_format, values):
+ """
+ Return the ``<c:numRef>`` element specified by the parameters as
+ unicode text.
+ """
+ pt_xml = self.pt_xml(values)
+ return (
+ " <c:numRef>\n"
+ " <c:f>{wksht_ref}</c:f>\n"
+ " <c:numCache>\n"
+ " <c:formatCode>{number_format}</c:formatCode>\n"
+ "{pt_xml}"
+ " </c:numCache>\n"
+ " </c:numRef>\n"
+ ).format(**{"wksht_ref": wksht_ref, "number_format": number_format, "pt_xml": pt_xml})
+
+ def pt_xml(self, values):
+ """
+ Return the ``<c:ptCount>`` and sequence of ``<c:pt>`` elements
+ corresponding to *values* as a single unicode text string.
+ `c:ptCount` refers to the number of `c:pt` elements in this sequence.
+ The `idx` attribute value for `c:pt` elements locates the data point
+ in the overall data point sequence of the chart and is started at
+ *offset*.
+ """
+ xml = (' <c:ptCount val="{pt_count}"/>\n').format(pt_count=len(values))
+
+ pt_tmpl = (
+ ' <c:pt idx="{idx}">\n'
+ " <c:v>{value}</c:v>\n"
+ " </c:pt>\n"
+ )
+ for idx, value in enumerate(values):
+ if value is None:
+ continue
+ xml += pt_tmpl.format(idx=idx, value=value)
+
+ return xml
+
+ @property
+ def tx(self):
+ """
+ Return a ``<c:tx>`` oxml element for this series, containing the
+ series name.
+ """
+ xml = self._tx_tmpl.format(
+ **{
+ "wksht_ref": self._series.name_ref,
+ "series_name": self.name,
+ "nsdecls": " %s" % nsdecls("c"),
+ }
+ )
+ return parse_xml(xml)
+
+ @property
+ def tx_xml(self):
+ """
+ Return the ``<c:tx>`` (tx is short for 'text') element for this
+ series as unicode text. This element contains the series name.
+ """
+ return self._tx_tmpl.format(
+ **{
+ "wksht_ref": self._series.name_ref,
+ "series_name": self.name,
+ "nsdecls": "",
+ }
+ )
+
+ @property
+ def _tx_tmpl(self):
+ """
+ The string formatting template for the ``<c:tx>`` element for this
+ series, containing the series title and spreadsheet range reference.
+ """
+ return (
+ " <c:tx{nsdecls}>\n"
+ " <c:strRef>\n"
+ " <c:f>{wksht_ref}</c:f>\n"
+ " <c:strCache>\n"
+ ' <c:ptCount val="1"/>\n'
+ ' <c:pt idx="0">\n'
+ " <c:v>{series_name}</c:v>\n"
+ " </c:pt>\n"
+ " </c:strCache>\n"
+ " </c:strRef>\n"
+ " </c:tx>\n"
+ )
+
+
+class _BaseSeriesXmlRewriter(object):
+ """
+ Base class for series XML rewriters.
+ """
+
+ def __init__(self, chart_data):
+ super(_BaseSeriesXmlRewriter, self).__init__()
+ self._chart_data = chart_data
+
+ def replace_series_data(self, chartSpace):
+ """
+ Rewrite the series data under *chartSpace* using the chart data
+ contents. All series-level formatting is left undisturbed. If
+ the chart data contains fewer series than *chartSpace*, the extra
+ series in *chartSpace* are deleted. If *chart_data* contains more
+ series than the *chartSpace* element, new series are added to the
+ last plot in the chart and series formatting is "cloned" from the
+ last series in that plot.
+ """
+ plotArea, date_1904 = chartSpace.plotArea, chartSpace.date_1904
+ chart_data = self._chart_data
+ self._adjust_ser_count(plotArea, len(chart_data))
+ for ser, series_data in zip(plotArea.sers, chart_data):
+ self._rewrite_ser_data(ser, series_data, date_1904)
+
+ def _add_cloned_sers(self, plotArea, count):
+ """
+ Add `c:ser` elements to the last xChart element in *plotArea*, cloned
+ from the last `c:ser` child of that last xChart.
+ """
+
+ def clone_ser(ser):
+ new_ser = deepcopy(ser)
+ new_ser.idx.val = plotArea.next_idx
+ new_ser.order.val = plotArea.next_order
+ ser.addnext(new_ser)
+ return new_ser
+
+ last_ser = plotArea.last_ser
+ for _ in range(count):
+ last_ser = clone_ser(last_ser)
+
+ def _adjust_ser_count(self, plotArea, new_ser_count):
+ """
+ Adjust the number of c:ser elements in *plotArea* to *new_ser_count*.
+ Excess c:ser elements are deleted from the end, along with any xChart
+ elements that are left empty as a result. Series elements are
+ considered in xChart + series order. Any new c:ser elements required
+ are added to the last xChart element and cloned from the last c:ser
+ element in that xChart.
+ """
+ ser_count_diff = new_ser_count - len(plotArea.sers)
+ if ser_count_diff > 0:
+ self._add_cloned_sers(plotArea, ser_count_diff)
+ elif ser_count_diff < 0:
+ self._trim_ser_count_by(plotArea, abs(ser_count_diff))
+
+ def _rewrite_ser_data(self, ser, series_data, date_1904):
+ """
+ Rewrite selected child elements of *ser* based on the values in
+ *series_data*.
+ """
+ raise NotImplementedError("must be implemented by each subclass")
+
+ def _trim_ser_count_by(self, plotArea, count):
+ """
+ Remove the last *count* ser elements from *plotArea*. Any xChart
+ elements having no ser child elements after trimming are also
+ removed.
+ """
+ extra_sers = plotArea.sers[-count:]
+ for ser in extra_sers:
+ parent = ser.getparent()
+ parent.remove(ser)
+ extra_xCharts = [xChart for xChart in plotArea.iter_xCharts() if len(xChart.sers) == 0]
+ for xChart in extra_xCharts:
+ parent = xChart.getparent()
+ parent.remove(xChart)
+
+
+class _AreaChartXmlWriter(_BaseChartXmlWriter):
+ """
+ Provides specialized methods particular to the ``<c:areaChart>`` element.
+ """
+
+ @property
+ def xml(self):
+ return (
+ "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n"
+ '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin'
+ 'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw'
+ 'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off'
+ 'iceDocument/2006/relationships">\n'
+ ' <c:date1904 val="0"/>\n'
+ ' <c:roundedCorners val="0"/>\n'
+ " <c:chart>\n"
+ ' <c:autoTitleDeleted val="0"/>\n'
+ " <c:plotArea>\n"
+ " <c:layout/>\n"
+ " <c:areaChart>\n"
+ "{grouping_xml}"
+ ' <c:varyColors val="0"/>\n'
+ "{ser_xml}"
+ " <c:dLbls>\n"
+ ' <c:showLegendKey val="0"/>\n'
+ ' <c:showVal val="0"/>\n'
+ ' <c:showCatName val="0"/>\n'
+ ' <c:showSerName val="0"/>\n'
+ ' <c:showPercent val="0"/>\n'
+ ' <c:showBubbleSize val="0"/>\n'
+ " </c:dLbls>\n"
+ ' <c:axId val="-2101159928"/>\n'
+ ' <c:axId val="-2100718248"/>\n'
+ " </c:areaChart>\n"
+ "{cat_ax_xml}"
+ " <c:valAx>\n"
+ ' <c:axId val="-2100718248"/>\n'
+ " <c:scaling>\n"
+ ' <c:orientation val="minMax"/>\n'
+ " </c:scaling>\n"
+ ' <c:delete val="0"/>\n'
+ ' <c:axPos val="l"/>\n'
+ " <c:majorGridlines/>\n"
+ ' <c:numFmt formatCode="General" sourceLinked="1"/>\n'
+ ' <c:majorTickMark val="out"/>\n'
+ ' <c:minorTickMark val="none"/>\n'
+ ' <c:tickLblPos val="nextTo"/>\n'
+ ' <c:crossAx val="-2101159928"/>\n'
+ ' <c:crosses val="autoZero"/>\n'
+ ' <c:crossBetween val="midCat"/>\n'
+ " </c:valAx>\n"
+ " </c:plotArea>\n"
+ " <c:legend>\n"
+ ' <c:legendPos val="r"/>\n'
+ " <c:layout/>\n"
+ ' <c:overlay val="0"/>\n'
+ " </c:legend>\n"
+ ' <c:plotVisOnly val="1"/>\n'
+ ' <c:dispBlanksAs val="zero"/>\n'
+ ' <c:showDLblsOverMax val="0"/>\n'
+ " </c:chart>\n"
+ " <c:txPr>\n"
+ " <a:bodyPr/>\n"
+ " <a:lstStyle/>\n"
+ " <a:p>\n"
+ " <a:pPr>\n"
+ ' <a:defRPr sz="1800"/>\n'
+ " </a:pPr>\n"
+ " <a:endParaRPr/>\n"
+ " </a:p>\n"
+ " </c:txPr>\n"
+ "</c:chartSpace>\n"
+ ).format(
+ **{
+ "grouping_xml": self._grouping_xml,
+ "ser_xml": self._ser_xml,
+ "cat_ax_xml": self._cat_ax_xml,
+ }
+ )
+
+ @property
+ def _cat_ax_xml(self):
+ categories = self._chart_data.categories
+
+ if categories.are_dates:
+ return (
+ " <c:dateAx>\n"
+ ' <c:axId val="-2101159928"/>\n'
+ " <c:scaling>\n"
+ ' <c:orientation val="minMax"/>\n'
+ " </c:scaling>\n"
+ ' <c:delete val="0"/>\n'
+ ' <c:axPos val="b"/>\n'
+ ' <c:numFmt formatCode="{nf}" sourceLinked="1"/>\n'
+ ' <c:majorTickMark val="out"/>\n'
+ ' <c:minorTickMark val="none"/>\n'
+ ' <c:tickLblPos val="nextTo"/>\n'
+ ' <c:crossAx val="-2100718248"/>\n'
+ ' <c:crosses val="autoZero"/>\n'
+ ' <c:auto val="1"/>\n'
+ ' <c:lblOffset val="100"/>\n'
+ ' <c:baseTimeUnit val="days"/>\n'
+ " </c:dateAx>\n"
+ ).format(**{"nf": categories.number_format})
+
+ return (
+ " <c:catAx>\n"
+ ' <c:axId val="-2101159928"/>\n'
+ " <c:scaling>\n"
+ ' <c:orientation val="minMax"/>\n'
+ " </c:scaling>\n"
+ ' <c:delete val="0"/>\n'
+ ' <c:axPos val="b"/>\n'
+ ' <c:numFmt formatCode="General" sourceLinked="1"/>\n'
+ ' <c:majorTickMark val="out"/>\n'
+ ' <c:minorTickMark val="none"/>\n'
+ ' <c:tickLblPos val="nextTo"/>\n'
+ ' <c:crossAx val="-2100718248"/>\n'
+ ' <c:crosses val="autoZero"/>\n'
+ ' <c:auto val="1"/>\n'
+ ' <c:lblAlgn val="ctr"/>\n'
+ ' <c:lblOffset val="100"/>\n'
+ ' <c:noMultiLvlLbl val="0"/>\n'
+ " </c:catAx>\n"
+ )
+
+ @property
+ def _grouping_xml(self):
+ val = {
+ XL_CHART_TYPE.AREA: "standard",
+ XL_CHART_TYPE.AREA_STACKED: "stacked",
+ XL_CHART_TYPE.AREA_STACKED_100: "percentStacked",
+ }[self._chart_type]
+ return ' <c:grouping val="%s"/>\n' % val
+
+ @property
+ def _ser_xml(self):
+ xml = ""
+ for series in self._chart_data:
+ xml_writer = _CategorySeriesXmlWriter(series)
+ xml += (
+ " <c:ser>\n"
+ ' <c:idx val="{ser_idx}"/>\n'
+ ' <c:order val="{ser_order}"/>\n'
+ "{tx_xml}"
+ "{cat_xml}"
+ "{val_xml}"
+ " </c:ser>\n"
+ ).format(
+ **{
+ "ser_idx": series.index,
+ "ser_order": series.index,
+ "tx_xml": xml_writer.tx_xml,
+ "cat_xml": xml_writer.cat_xml,
+ "val_xml": xml_writer.val_xml,
+ }
+ )
+ return xml
+
+
+class _BarChartXmlWriter(_BaseChartXmlWriter):
+ """
+ Provides specialized methods particular to the ``<c:barChart>`` element.
+ """
+
+ @property
+ def xml(self):
+ return (
+ "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n"
+ '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin'
+ 'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw'
+ 'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off'
+ 'iceDocument/2006/relationships">\n'
+ ' <c:date1904 val="0"/>\n'
+ " <c:chart>\n"
+ ' <c:autoTitleDeleted val="0"/>\n'
+ " <c:plotArea>\n"
+ " <c:barChart>\n"
+ "{barDir_xml}"
+ "{grouping_xml}"
+ "{ser_xml}"
+ "{overlap_xml}"
+ ' <c:axId val="-2068027336"/>\n'
+ ' <c:axId val="-2113994440"/>\n'
+ " </c:barChart>\n"
+ "{cat_ax_xml}"
+ " <c:valAx>\n"
+ ' <c:axId val="-2113994440"/>\n'
+ " <c:scaling/>\n"
+ ' <c:delete val="0"/>\n'
+ ' <c:axPos val="{val_ax_pos}"/>\n'
+ " <c:majorGridlines/>\n"
+ ' <c:majorTickMark val="out"/>\n'
+ ' <c:minorTickMark val="none"/>\n'
+ ' <c:tickLblPos val="nextTo"/>\n'
+ ' <c:crossAx val="-2068027336"/>\n'
+ ' <c:crosses val="autoZero"/>\n'
+ " </c:valAx>\n"
+ " </c:plotArea>\n"
+ ' <c:dispBlanksAs val="gap"/>\n'
+ " </c:chart>\n"
+ " <c:txPr>\n"
+ " <a:bodyPr/>\n"
+ " <a:lstStyle/>\n"
+ " <a:p>\n"
+ " <a:pPr>\n"
+ ' <a:defRPr sz="1800"/>\n'
+ " </a:pPr>\n"
+ ' <a:endParaRPr lang="en-US"/>\n'
+ " </a:p>\n"
+ " </c:txPr>\n"
+ "</c:chartSpace>\n"
+ ).format(
+ **{
+ "barDir_xml": self._barDir_xml,
+ "grouping_xml": self._grouping_xml,
+ "ser_xml": self._ser_xml,
+ "overlap_xml": self._overlap_xml,
+ "cat_ax_xml": self._cat_ax_xml,
+ "val_ax_pos": self._val_ax_pos,
+ }
+ )
+
+ @property
+ def _barDir_xml(self):
+ XL = XL_CHART_TYPE
+ bar_types = (XL.BAR_CLUSTERED, XL.BAR_STACKED, XL.BAR_STACKED_100)
+ col_types = (XL.COLUMN_CLUSTERED, XL.COLUMN_STACKED, XL.COLUMN_STACKED_100)
+ if self._chart_type in bar_types:
+ return ' <c:barDir val="bar"/>\n'
+ elif self._chart_type in col_types:
+ return ' <c:barDir val="col"/>\n'
+ raise NotImplementedError("no _barDir_xml() for chart type %s" % self._chart_type)
+
+ @property
+ def _cat_ax_pos(self):
+ return {
+ XL_CHART_TYPE.BAR_CLUSTERED: "l",
+ XL_CHART_TYPE.BAR_STACKED: "l",
+ XL_CHART_TYPE.BAR_STACKED_100: "l",
+ XL_CHART_TYPE.COLUMN_CLUSTERED: "b",
+ XL_CHART_TYPE.COLUMN_STACKED: "b",
+ XL_CHART_TYPE.COLUMN_STACKED_100: "b",
+ }[self._chart_type]
+
+ @property
+ def _cat_ax_xml(self):
+ categories = self._chart_data.categories
+
+ if categories.are_dates:
+ return (
+ " <c:dateAx>\n"
+ ' <c:axId val="-2068027336"/>\n'
+ " <c:scaling>\n"
+ ' <c:orientation val="minMax"/>\n'
+ " </c:scaling>\n"
+ ' <c:delete val="0"/>\n'
+ ' <c:axPos val="{cat_ax_pos}"/>\n'
+ ' <c:numFmt formatCode="{nf}" sourceLinked="1"/>\n'
+ ' <c:majorTickMark val="out"/>\n'
+ ' <c:minorTickMark val="none"/>\n'
+ ' <c:tickLblPos val="nextTo"/>\n'
+ ' <c:crossAx val="-2113994440"/>\n'
+ ' <c:crosses val="autoZero"/>\n'
+ ' <c:auto val="1"/>\n'
+ ' <c:lblOffset val="100"/>\n'
+ ' <c:baseTimeUnit val="days"/>\n'
+ " </c:dateAx>\n"
+ ).format(**{"cat_ax_pos": self._cat_ax_pos, "nf": categories.number_format})
+
+ return (
+ " <c:catAx>\n"
+ ' <c:axId val="-2068027336"/>\n'
+ " <c:scaling>\n"
+ ' <c:orientation val="minMax"/>\n'
+ " </c:scaling>\n"
+ ' <c:delete val="0"/>\n'
+ ' <c:axPos val="{cat_ax_pos}"/>\n'
+ ' <c:majorTickMark val="out"/>\n'
+ ' <c:minorTickMark val="none"/>\n'
+ ' <c:tickLblPos val="nextTo"/>\n'
+ ' <c:crossAx val="-2113994440"/>\n'
+ ' <c:crosses val="autoZero"/>\n'
+ ' <c:auto val="1"/>\n'
+ ' <c:lblAlgn val="ctr"/>\n'
+ ' <c:lblOffset val="100"/>\n'
+ ' <c:noMultiLvlLbl val="0"/>\n'
+ " </c:catAx>\n"
+ ).format(**{"cat_ax_pos": self._cat_ax_pos})
+
+ @property
+ def _grouping_xml(self):
+ XL = XL_CHART_TYPE
+ clustered_types = (XL.BAR_CLUSTERED, XL.COLUMN_CLUSTERED)
+ stacked_types = (XL.BAR_STACKED, XL.COLUMN_STACKED)
+ percentStacked_types = (XL.BAR_STACKED_100, XL.COLUMN_STACKED_100)
+ if self._chart_type in clustered_types:
+ return ' <c:grouping val="clustered"/>\n'
+ elif self._chart_type in stacked_types:
+ return ' <c:grouping val="stacked"/>\n'
+ elif self._chart_type in percentStacked_types:
+ return ' <c:grouping val="percentStacked"/>\n'
+ raise NotImplementedError("no _grouping_xml() for chart type %s" % self._chart_type)
+
+ @property
+ def _overlap_xml(self):
+ XL = XL_CHART_TYPE
+ percentStacked_types = (
+ XL.BAR_STACKED,
+ XL.BAR_STACKED_100,
+ XL.COLUMN_STACKED,
+ XL.COLUMN_STACKED_100,
+ )
+ if self._chart_type in percentStacked_types:
+ return ' <c:overlap val="100"/>\n'
+ return ""
+
+ @property
+ def _ser_xml(self):
+ xml = ""
+ for series in self._chart_data:
+ xml_writer = _CategorySeriesXmlWriter(series)
+ xml += (
+ " <c:ser>\n"
+ ' <c:idx val="{ser_idx}"/>\n'
+ ' <c:order val="{ser_order}"/>\n'
+ "{tx_xml}"
+ "{cat_xml}"
+ "{val_xml}"
+ " </c:ser>\n"
+ ).format(
+ **{
+ "ser_idx": series.index,
+ "ser_order": series.index,
+ "tx_xml": xml_writer.tx_xml,
+ "cat_xml": xml_writer.cat_xml,
+ "val_xml": xml_writer.val_xml,
+ }
+ )
+ return xml
+
+ @property
+ def _val_ax_pos(self):
+ return {
+ XL_CHART_TYPE.BAR_CLUSTERED: "b",
+ XL_CHART_TYPE.BAR_STACKED: "b",
+ XL_CHART_TYPE.BAR_STACKED_100: "b",
+ XL_CHART_TYPE.COLUMN_CLUSTERED: "l",
+ XL_CHART_TYPE.COLUMN_STACKED: "l",
+ XL_CHART_TYPE.COLUMN_STACKED_100: "l",
+ }[self._chart_type]
+
+
+class _DoughnutChartXmlWriter(_BaseChartXmlWriter):
+ """
+ Provides specialized methods particular to the ``<c:doughnutChart>``
+ element.
+ """
+
+ @property
+ def xml(self):
+ return (
+ "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n"
+ '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin'
+ 'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw'
+ 'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off'
+ 'iceDocument/2006/relationships">\n'
+ ' <c:date1904 val="0"/>\n'
+ ' <c:roundedCorners val="0"/>\n'
+ " <c:chart>\n"
+ ' <c:autoTitleDeleted val="0"/>\n'
+ " <c:plotArea>\n"
+ " <c:layout/>\n"
+ " <c:doughnutChart>\n"
+ ' <c:varyColors val="1"/>\n'
+ "{ser_xml}"
+ " <c:dLbls>\n"
+ ' <c:showLegendKey val="0"/>\n'
+ ' <c:showVal val="0"/>\n'
+ ' <c:showCatName val="0"/>\n'
+ ' <c:showSerName val="0"/>\n'
+ ' <c:showPercent val="0"/>\n'
+ ' <c:showBubbleSize val="0"/>\n'
+ ' <c:showLeaderLines val="1"/>\n'
+ " </c:dLbls>\n"
+ ' <c:firstSliceAng val="0"/>\n'
+ ' <c:holeSize val="50"/>\n'
+ " </c:doughnutChart>\n"
+ " </c:plotArea>\n"
+ " <c:legend>\n"
+ ' <c:legendPos val="r"/>\n'
+ " <c:layout/>\n"
+ ' <c:overlay val="0"/>\n'
+ " </c:legend>\n"
+ ' <c:plotVisOnly val="1"/>\n'
+ ' <c:dispBlanksAs val="gap"/>\n'
+ ' <c:showDLblsOverMax val="0"/>\n'
+ " </c:chart>\n"
+ " <c:txPr>\n"
+ " <a:bodyPr/>\n"
+ " <a:lstStyle/>\n"
+ " <a:p>\n"
+ " <a:pPr>\n"
+ ' <a:defRPr sz="1800"/>\n'
+ " </a:pPr>\n"
+ " <a:endParaRPr/>\n"
+ " </a:p>\n"
+ " </c:txPr>\n"
+ "</c:chartSpace>\n"
+ ).format(**{"ser_xml": self._ser_xml})
+
+ @property
+ def _explosion_xml(self):
+ if self._chart_type == XL_CHART_TYPE.DOUGHNUT_EXPLODED:
+ return ' <c:explosion val="25"/>\n'
+ return ""
+
+ @property
+ def _ser_xml(self):
+ xml = ""
+ for series in self._chart_data:
+ xml_writer = _CategorySeriesXmlWriter(series)
+ xml += (
+ " <c:ser>\n"
+ ' <c:idx val="{ser_idx}"/>\n'
+ ' <c:order val="{ser_order}"/>\n'
+ "{tx_xml}"
+ "{explosion_xml}"
+ "{cat_xml}"
+ "{val_xml}"
+ " </c:ser>\n"
+ ).format(
+ **{
+ "ser_idx": series.index,
+ "ser_order": series.index,
+ "tx_xml": xml_writer.tx_xml,
+ "explosion_xml": self._explosion_xml,
+ "cat_xml": xml_writer.cat_xml,
+ "val_xml": xml_writer.val_xml,
+ }
+ )
+ return xml
+
+
+class _LineChartXmlWriter(_BaseChartXmlWriter):
+ """
+ Provides specialized methods particular to the ``<c:lineChart>`` element.
+ """
+
+ @property
+ def xml(self):
+ return (
+ "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n"
+ '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin'
+ 'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw'
+ 'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off'
+ 'iceDocument/2006/relationships">\n'
+ ' <c:date1904 val="0"/>\n'
+ " <c:chart>\n"
+ ' <c:autoTitleDeleted val="0"/>\n'
+ " <c:plotArea>\n"
+ " <c:lineChart>\n"
+ "{grouping_xml}"
+ ' <c:varyColors val="0"/>\n'
+ "{ser_xml}"
+ ' <c:marker val="1"/>\n'
+ ' <c:smooth val="0"/>\n'
+ ' <c:axId val="2118791784"/>\n'
+ ' <c:axId val="2140495176"/>\n'
+ " </c:lineChart>\n"
+ "{cat_ax_xml}"
+ " <c:valAx>\n"
+ ' <c:axId val="2140495176"/>\n'
+ " <c:scaling/>\n"
+ ' <c:delete val="0"/>\n'
+ ' <c:axPos val="l"/>\n'
+ " <c:majorGridlines/>\n"
+ ' <c:majorTickMark val="out"/>\n'
+ ' <c:minorTickMark val="none"/>\n'
+ ' <c:tickLblPos val="nextTo"/>\n'
+ ' <c:crossAx val="2118791784"/>\n'
+ ' <c:crosses val="autoZero"/>\n'
+ " </c:valAx>\n"
+ " </c:plotArea>\n"
+ " <c:legend>\n"
+ ' <c:legendPos val="r"/>\n'
+ " <c:layout/>\n"
+ ' <c:overlay val="0"/>\n'
+ " </c:legend>\n"
+ ' <c:plotVisOnly val="1"/>\n'
+ ' <c:dispBlanksAs val="gap"/>\n'
+ ' <c:showDLblsOverMax val="0"/>\n'
+ " </c:chart>\n"
+ " <c:txPr>\n"
+ " <a:bodyPr/>\n"
+ " <a:lstStyle/>\n"
+ " <a:p>\n"
+ " <a:pPr>\n"
+ ' <a:defRPr sz="1800"/>\n'
+ " </a:pPr>\n"
+ ' <a:endParaRPr lang="en-US"/>\n'
+ " </a:p>\n"
+ " </c:txPr>\n"
+ "</c:chartSpace>\n"
+ ).format(
+ **{
+ "grouping_xml": self._grouping_xml,
+ "ser_xml": self._ser_xml,
+ "cat_ax_xml": self._cat_ax_xml,
+ }
+ )
+
+ @property
+ def _cat_ax_xml(self):
+ categories = self._chart_data.categories
+
+ if categories.are_dates:
+ return (
+ " <c:dateAx>\n"
+ ' <c:axId val="2118791784"/>\n'
+ " <c:scaling>\n"
+ ' <c:orientation val="minMax"/>\n'
+ " </c:scaling>\n"
+ ' <c:delete val="0"/>\n'
+ ' <c:axPos val="b"/>\n'
+ ' <c:numFmt formatCode="{nf}" sourceLinked="1"/>\n'
+ ' <c:majorTickMark val="out"/>\n'
+ ' <c:minorTickMark val="none"/>\n'
+ ' <c:tickLblPos val="nextTo"/>\n'
+ ' <c:crossAx val="2140495176"/>\n'
+ ' <c:crosses val="autoZero"/>\n'
+ ' <c:auto val="1"/>\n'
+ ' <c:lblOffset val="100"/>\n'
+ ' <c:baseTimeUnit val="days"/>\n'
+ " </c:dateAx>\n"
+ ).format(**{"nf": categories.number_format})
+
+ return (
+ " <c:catAx>\n"
+ ' <c:axId val="2118791784"/>\n'
+ " <c:scaling>\n"
+ ' <c:orientation val="minMax"/>\n'
+ " </c:scaling>\n"
+ ' <c:delete val="0"/>\n'
+ ' <c:axPos val="b"/>\n'
+ ' <c:majorTickMark val="out"/>\n'
+ ' <c:minorTickMark val="none"/>\n'
+ ' <c:tickLblPos val="nextTo"/>\n'
+ ' <c:crossAx val="2140495176"/>\n'
+ ' <c:crosses val="autoZero"/>\n'
+ ' <c:auto val="1"/>\n'
+ ' <c:lblAlgn val="ctr"/>\n'
+ ' <c:lblOffset val="100"/>\n'
+ ' <c:noMultiLvlLbl val="0"/>\n'
+ " </c:catAx>\n"
+ )
+
+ @property
+ def _grouping_xml(self):
+ XL = XL_CHART_TYPE
+ standard_types = (XL.LINE, XL.LINE_MARKERS)
+ stacked_types = (XL.LINE_STACKED, XL.LINE_MARKERS_STACKED)
+ percentStacked_types = (XL.LINE_STACKED_100, XL.LINE_MARKERS_STACKED_100)
+ if self._chart_type in standard_types:
+ return ' <c:grouping val="standard"/>\n'
+ elif self._chart_type in stacked_types:
+ return ' <c:grouping val="stacked"/>\n'
+ elif self._chart_type in percentStacked_types:
+ return ' <c:grouping val="percentStacked"/>\n'
+ raise NotImplementedError("no _grouping_xml() for chart type %s" % self._chart_type)
+
+ @property
+ def _marker_xml(self):
+ XL = XL_CHART_TYPE
+ no_marker_types = (XL.LINE, XL.LINE_STACKED, XL.LINE_STACKED_100)
+ if self._chart_type in no_marker_types:
+ return (
+ " <c:marker>\n"
+ ' <c:symbol val="none"/>\n'
+ " </c:marker>\n"
+ )
+ return ""
+
+ @property
+ def _ser_xml(self):
+ xml = ""
+ for series in self._chart_data:
+ xml_writer = _CategorySeriesXmlWriter(series)
+ xml += (
+ " <c:ser>\n"
+ ' <c:idx val="{ser_idx}"/>\n'
+ ' <c:order val="{ser_order}"/>\n'
+ "{tx_xml}"
+ "{marker_xml}"
+ "{cat_xml}"
+ "{val_xml}"
+ ' <c:smooth val="0"/>\n'
+ " </c:ser>\n"
+ ).format(
+ **{
+ "ser_idx": series.index,
+ "ser_order": series.index,
+ "tx_xml": xml_writer.tx_xml,
+ "marker_xml": self._marker_xml,
+ "cat_xml": xml_writer.cat_xml,
+ "val_xml": xml_writer.val_xml,
+ }
+ )
+ return xml
+
+
+class _PieChartXmlWriter(_BaseChartXmlWriter):
+ """
+ Provides specialized methods particular to the ``<c:pieChart>`` element.
+ """
+
+ @property
+ def xml(self):
+ return (
+ "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n"
+ '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin'
+ 'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw'
+ 'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off'
+ 'iceDocument/2006/relationships">\n'
+ " <c:chart>\n"
+ ' <c:autoTitleDeleted val="0"/>\n'
+ " <c:plotArea>\n"
+ " <c:pieChart>\n"
+ ' <c:varyColors val="1"/>\n'
+ "{ser_xml}"
+ " </c:pieChart>\n"
+ " </c:plotArea>\n"
+ ' <c:dispBlanksAs val="gap"/>\n'
+ " </c:chart>\n"
+ " <c:txPr>\n"
+ " <a:bodyPr/>\n"
+ " <a:lstStyle/>\n"
+ " <a:p>\n"
+ " <a:pPr>\n"
+ ' <a:defRPr sz="1800"/>\n'
+ " </a:pPr>\n"
+ ' <a:endParaRPr lang="en-US"/>\n'
+ " </a:p>\n"
+ " </c:txPr>\n"
+ "</c:chartSpace>\n"
+ ).format(**{"ser_xml": self._ser_xml})
+
+ @property
+ def _explosion_xml(self):
+ if self._chart_type == XL_CHART_TYPE.PIE_EXPLODED:
+ return ' <c:explosion val="25"/>\n'
+ return ""
+
+ @property
+ def _ser_xml(self):
+ xml_writer = _CategorySeriesXmlWriter(self._chart_data[0])
+ xml = (
+ " <c:ser>\n"
+ ' <c:idx val="0"/>\n'
+ ' <c:order val="0"/>\n'
+ "{tx_xml}"
+ "{explosion_xml}"
+ "{cat_xml}"
+ "{val_xml}"
+ " </c:ser>\n"
+ ).format(
+ **{
+ "tx_xml": xml_writer.tx_xml,
+ "explosion_xml": self._explosion_xml,
+ "cat_xml": xml_writer.cat_xml,
+ "val_xml": xml_writer.val_xml,
+ }
+ )
+ return xml
+
+
+class _RadarChartXmlWriter(_BaseChartXmlWriter):
+ """
+ Generates XML for the ``<c:radarChart>`` element.
+ """
+
+ @property
+ def xml(self):
+ return (
+ "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n"
+ '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin'
+ 'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw'
+ 'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off'
+ 'iceDocument/2006/relationships">\n'
+ ' <c:date1904 val="0"/>\n'
+ ' <c:roundedCorners val="0"/>\n'
+ ' <mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.'
+ 'org/markup-compatibility/2006">\n'
+ ' <mc:Choice xmlns:c14="http://schemas.microsoft.com/office/d'
+ 'rawing/2007/8/2/chart" Requires="c14">\n'
+ ' <c14:style val="118"/>\n'
+ " </mc:Choice>\n"
+ " <mc:Fallback>\n"
+ ' <c:style val="18"/>\n'
+ " </mc:Fallback>\n"
+ " </mc:AlternateContent>\n"
+ " <c:chart>\n"
+ ' <c:autoTitleDeleted val="0"/>\n'
+ " <c:plotArea>\n"
+ " <c:layout/>\n"
+ " <c:radarChart>\n"
+ ' <c:radarStyle val="{radar_style}"/>\n'
+ ' <c:varyColors val="0"/>\n'
+ "{ser_xml}"
+ ' <c:axId val="2073612648"/>\n'
+ ' <c:axId val="-2112772216"/>\n'
+ " </c:radarChart>\n"
+ " <c:catAx>\n"
+ ' <c:axId val="2073612648"/>\n'
+ " <c:scaling>\n"
+ ' <c:orientation val="minMax"/>\n'
+ " </c:scaling>\n"
+ ' <c:delete val="0"/>\n'
+ ' <c:axPos val="b"/>\n'
+ " <c:majorGridlines/>\n"
+ ' <c:numFmt formatCode="m/d/yy" sourceLinked="1"/>\n'
+ ' <c:majorTickMark val="out"/>\n'
+ ' <c:minorTickMark val="none"/>\n'
+ ' <c:tickLblPos val="nextTo"/>\n'
+ ' <c:crossAx val="-2112772216"/>\n'
+ ' <c:crosses val="autoZero"/>\n'
+ ' <c:auto val="1"/>\n'
+ ' <c:lblAlgn val="ctr"/>\n'
+ ' <c:lblOffset val="100"/>\n'
+ ' <c:noMultiLvlLbl val="0"/>\n'
+ " </c:catAx>\n"
+ " <c:valAx>\n"
+ ' <c:axId val="-2112772216"/>\n'
+ " <c:scaling>\n"
+ ' <c:orientation val="minMax"/>\n'
+ " </c:scaling>\n"
+ ' <c:delete val="0"/>\n'
+ ' <c:axPos val="l"/>\n'
+ " <c:majorGridlines/>\n"
+ ' <c:numFmt formatCode="General" sourceLinked="1"/>\n'
+ ' <c:majorTickMark val="cross"/>\n'
+ ' <c:minorTickMark val="none"/>\n'
+ ' <c:tickLblPos val="nextTo"/>\n'
+ ' <c:crossAx val="2073612648"/>\n'
+ ' <c:crosses val="autoZero"/>\n'
+ ' <c:crossBetween val="between"/>\n'
+ " </c:valAx>\n"
+ " </c:plotArea>\n"
+ ' <c:plotVisOnly val="1"/>\n'
+ ' <c:dispBlanksAs val="gap"/>\n'
+ ' <c:showDLblsOverMax val="0"/>\n'
+ " </c:chart>\n"
+ " <c:txPr>\n"
+ " <a:bodyPr/>\n"
+ " <a:lstStyle/>\n"
+ " <a:p>\n"
+ " <a:pPr>\n"
+ ' <a:defRPr sz="1800"/>\n'
+ " </a:pPr>\n"
+ ' <a:endParaRPr lang="en-US"/>\n'
+ " </a:p>\n"
+ " </c:txPr>\n"
+ "</c:chartSpace>\n"
+ ).format(**{"radar_style": self._radar_style, "ser_xml": self._ser_xml})
+
+ @property
+ def _marker_xml(self):
+ if self._chart_type == XL_CHART_TYPE.RADAR:
+ return (
+ " <c:marker>\n"
+ ' <c:symbol val="none"/>\n'
+ " </c:marker>\n"
+ )
+ return ""
+
+ @property
+ def _radar_style(self):
+ if self._chart_type == XL_CHART_TYPE.RADAR_FILLED:
+ return "filled"
+ return "marker"
+
+ @property
+ def _ser_xml(self):
+ xml = ""
+ for series in self._chart_data:
+ xml_writer = _CategorySeriesXmlWriter(series)
+ xml += (
+ " <c:ser>\n"
+ ' <c:idx val="{ser_idx}"/>\n'
+ ' <c:order val="{ser_order}"/>\n'
+ "{tx_xml}"
+ "{marker_xml}"
+ "{cat_xml}"
+ "{val_xml}"
+ ' <c:smooth val="0"/>\n'
+ " </c:ser>\n"
+ ).format(
+ **{
+ "ser_idx": series.index,
+ "ser_order": series.index,
+ "tx_xml": xml_writer.tx_xml,
+ "marker_xml": self._marker_xml,
+ "cat_xml": xml_writer.cat_xml,
+ "val_xml": xml_writer.val_xml,
+ }
+ )
+ return xml
+
+
+class _XyChartXmlWriter(_BaseChartXmlWriter):
+ """
+ Generates XML for the ``<c:scatterChart>`` element.
+ """
+
+ @property
+ def xml(self):
+ xml = (
+ "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n"
+ '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin'
+ 'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw'
+ 'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off'
+ 'iceDocument/2006/relationships">\n'
+ " <c:chart>\n"
+ " <c:plotArea>\n"
+ " <c:scatterChart>\n"
+ ' <c:scatterStyle val="%s"/>\n'
+ ' <c:varyColors val="0"/>\n'
+ "%s"
+ ' <c:axId val="-2128940872"/>\n'
+ ' <c:axId val="-2129643912"/>\n'
+ " </c:scatterChart>\n"
+ " <c:valAx>\n"
+ ' <c:axId val="-2128940872"/>\n'
+ " <c:scaling>\n"
+ ' <c:orientation val="minMax"/>\n'
+ " </c:scaling>\n"
+ ' <c:delete val="0"/>\n'
+ ' <c:axPos val="b"/>\n'
+ ' <c:numFmt formatCode="General" sourceLinked="1"/>\n'
+ ' <c:majorTickMark val="out"/>\n'
+ ' <c:minorTickMark val="none"/>\n'
+ ' <c:tickLblPos val="nextTo"/>\n'
+ ' <c:crossAx val="-2129643912"/>\n'
+ ' <c:crosses val="autoZero"/>\n'
+ ' <c:crossBetween val="midCat"/>\n'
+ " </c:valAx>\n"
+ " <c:valAx>\n"
+ ' <c:axId val="-2129643912"/>\n'
+ " <c:scaling>\n"
+ ' <c:orientation val="minMax"/>\n'
+ " </c:scaling>\n"
+ ' <c:delete val="0"/>\n'
+ ' <c:axPos val="l"/>\n'
+ " <c:majorGridlines/>\n"
+ ' <c:numFmt formatCode="General" sourceLinked="1"/>\n'
+ ' <c:majorTickMark val="out"/>\n'
+ ' <c:minorTickMark val="none"/>\n'
+ ' <c:tickLblPos val="nextTo"/>\n'
+ ' <c:crossAx val="-2128940872"/>\n'
+ ' <c:crosses val="autoZero"/>\n'
+ ' <c:crossBetween val="midCat"/>\n'
+ " </c:valAx>\n"
+ " </c:plotArea>\n"
+ " <c:legend>\n"
+ ' <c:legendPos val="r"/>\n'
+ " <c:layout/>\n"
+ ' <c:overlay val="0"/>\n'
+ " </c:legend>\n"
+ ' <c:plotVisOnly val="1"/>\n'
+ ' <c:dispBlanksAs val="gap"/>\n'
+ ' <c:showDLblsOverMax val="0"/>\n'
+ " </c:chart>\n"
+ " <c:txPr>\n"
+ " <a:bodyPr/>\n"
+ " <a:lstStyle/>\n"
+ " <a:p>\n"
+ " <a:pPr>\n"
+ ' <a:defRPr sz="1800"/>\n'
+ " </a:pPr>\n"
+ ' <a:endParaRPr lang="en-US"/>\n'
+ " </a:p>\n"
+ " </c:txPr>\n"
+ "</c:chartSpace>\n"
+ ) % (self._scatterStyle_val, self._ser_xml)
+ return xml
+
+ @property
+ def _marker_xml(self):
+ no_marker_types = (
+ XL_CHART_TYPE.XY_SCATTER_LINES_NO_MARKERS,
+ XL_CHART_TYPE.XY_SCATTER_SMOOTH_NO_MARKERS,
+ )
+ if self._chart_type in no_marker_types:
+ return (
+ " <c:marker>\n"
+ ' <c:symbol val="none"/>\n'
+ " </c:marker>\n"
+ )
+ return ""
+
+ @property
+ def _scatterStyle_val(self):
+ smooth_types = (
+ XL_CHART_TYPE.XY_SCATTER_SMOOTH,
+ XL_CHART_TYPE.XY_SCATTER_SMOOTH_NO_MARKERS,
+ )
+ if self._chart_type in smooth_types:
+ return "smoothMarker"
+ return "lineMarker"
+
+ @property
+ def _ser_xml(self):
+ xml = ""
+ for series in self._chart_data:
+ xml_writer = _XySeriesXmlWriter(series)
+ xml += (
+ " <c:ser>\n"
+ ' <c:idx val="{ser_idx}"/>\n'
+ ' <c:order val="{ser_order}"/>\n'
+ "{tx_xml}"
+ "{spPr_xml}"
+ "{marker_xml}"
+ "{xVal_xml}"
+ "{yVal_xml}"
+ ' <c:smooth val="0"/>\n'
+ " </c:ser>\n"
+ ).format(
+ **{
+ "ser_idx": series.index,
+ "ser_order": series.index,
+ "tx_xml": xml_writer.tx_xml,
+ "spPr_xml": self._spPr_xml,
+ "marker_xml": self._marker_xml,
+ "xVal_xml": xml_writer.xVal_xml,
+ "yVal_xml": xml_writer.yVal_xml,
+ }
+ )
+ return xml
+
+ @property
+ def _spPr_xml(self):
+ if self._chart_type == XL_CHART_TYPE.XY_SCATTER:
+ return (
+ " <c:spPr>\n"
+ ' <a:ln w="47625">\n'
+ " <a:noFill/>\n"
+ " </a:ln>\n"
+ " </c:spPr>\n"
+ )
+ return ""
+
+
+class _BubbleChartXmlWriter(_XyChartXmlWriter):
+ """
+ Provides specialized methods particular to the ``<c:bubbleChart>``
+ element.
+ """
+
+ @property
+ def xml(self):
+ xml = (
+ "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>\n"
+ '<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawin'
+ 'gml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/draw'
+ 'ingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/off'
+ 'iceDocument/2006/relationships">\n'
+ " <c:chart>\n"
+ ' <c:autoTitleDeleted val="0"/>\n'
+ " <c:plotArea>\n"
+ " <c:layout/>\n"
+ " <c:bubbleChart>\n"
+ ' <c:varyColors val="0"/>\n'
+ "%s"
+ " <c:dLbls>\n"
+ ' <c:showLegendKey val="0"/>\n'
+ ' <c:showVal val="0"/>\n'
+ ' <c:showCatName val="0"/>\n'
+ ' <c:showSerName val="0"/>\n'
+ ' <c:showPercent val="0"/>\n'
+ ' <c:showBubbleSize val="0"/>\n'
+ " </c:dLbls>\n"
+ ' <c:bubbleScale val="100"/>\n'
+ ' <c:showNegBubbles val="0"/>\n'
+ ' <c:axId val="-2115720072"/>\n'
+ ' <c:axId val="-2115723560"/>\n'
+ " </c:bubbleChart>\n"
+ " <c:valAx>\n"
+ ' <c:axId val="-2115720072"/>\n'
+ " <c:scaling>\n"
+ ' <c:orientation val="minMax"/>\n'
+ " </c:scaling>\n"
+ ' <c:delete val="0"/>\n'
+ ' <c:axPos val="b"/>\n'
+ ' <c:numFmt formatCode="General" sourceLinked="1"/>\n'
+ ' <c:majorTickMark val="out"/>\n'
+ ' <c:minorTickMark val="none"/>\n'
+ ' <c:tickLblPos val="nextTo"/>\n'
+ ' <c:crossAx val="-2115723560"/>\n'
+ ' <c:crosses val="autoZero"/>\n'
+ ' <c:crossBetween val="midCat"/>\n'
+ " </c:valAx>\n"
+ " <c:valAx>\n"
+ ' <c:axId val="-2115723560"/>\n'
+ " <c:scaling>\n"
+ ' <c:orientation val="minMax"/>\n'
+ " </c:scaling>\n"
+ ' <c:delete val="0"/>\n'
+ ' <c:axPos val="l"/>\n'
+ " <c:majorGridlines/>\n"
+ ' <c:numFmt formatCode="General" sourceLinked="1"/>\n'
+ ' <c:majorTickMark val="out"/>\n'
+ ' <c:minorTickMark val="none"/>\n'
+ ' <c:tickLblPos val="nextTo"/>\n'
+ ' <c:crossAx val="-2115720072"/>\n'
+ ' <c:crosses val="autoZero"/>\n'
+ ' <c:crossBetween val="midCat"/>\n'
+ " </c:valAx>\n"
+ " </c:plotArea>\n"
+ " <c:legend>\n"
+ ' <c:legendPos val="r"/>\n'
+ " <c:layout/>\n"
+ ' <c:overlay val="0"/>\n'
+ " </c:legend>\n"
+ ' <c:plotVisOnly val="1"/>\n'
+ ' <c:dispBlanksAs val="gap"/>\n'
+ ' <c:showDLblsOverMax val="0"/>\n'
+ " </c:chart>\n"
+ " <c:txPr>\n"
+ " <a:bodyPr/>\n"
+ " <a:lstStyle/>\n"
+ " <a:p>\n"
+ " <a:pPr>\n"
+ ' <a:defRPr sz="1800"/>\n'
+ " </a:pPr>\n"
+ ' <a:endParaRPr lang="en-US"/>\n'
+ " </a:p>\n"
+ " </c:txPr>\n"
+ "</c:chartSpace>\n"
+ ) % self._ser_xml
+ return xml
+
+ @property
+ def _bubble3D_val(self):
+ if self._chart_type == XL_CHART_TYPE.BUBBLE_THREE_D_EFFECT:
+ return "1"
+ return "0"
+
+ @property
+ def _ser_xml(self):
+ xml = ""
+ for series in self._chart_data:
+ xml_writer = _BubbleSeriesXmlWriter(series)
+ xml += (
+ " <c:ser>\n"
+ ' <c:idx val="{ser_idx}"/>\n'
+ ' <c:order val="{ser_order}"/>\n'
+ "{tx_xml}"
+ ' <c:invertIfNegative val="0"/>\n'
+ "{xVal_xml}"
+ "{yVal_xml}"
+ "{bubbleSize_xml}"
+ ' <c:bubble3D val="{bubble3D_val}"/>\n'
+ " </c:ser>\n"
+ ).format(
+ **{
+ "ser_idx": series.index,
+ "ser_order": series.index,
+ "tx_xml": xml_writer.tx_xml,
+ "xVal_xml": xml_writer.xVal_xml,
+ "yVal_xml": xml_writer.yVal_xml,
+ "bubbleSize_xml": xml_writer.bubbleSize_xml,
+ "bubble3D_val": self._bubble3D_val,
+ }
+ )
+ return xml
+
+
+class _CategorySeriesXmlWriter(_BaseSeriesXmlWriter):
+ """
+ Generates XML snippets particular to a category chart series.
+ """
+
+ @property
+ def cat(self):
+ """
+ Return the ``<c:cat>`` element XML for this series, as an oxml
+ element.
+ """
+ categories = self._series.categories
+
+ if categories.are_numeric:
+ return parse_xml(
+ self._numRef_cat_tmpl.format(
+ **{
+ "wksht_ref": self._series.categories_ref,
+ "number_format": categories.number_format,
+ "cat_count": categories.leaf_count,
+ "cat_pt_xml": self._cat_num_pt_xml,
+ "nsdecls": " %s" % nsdecls("c"),
+ }
+ )
+ )
+
+ if categories.depth == 1:
+ return parse_xml(
+ self._cat_tmpl.format(
+ **{
+ "wksht_ref": self._series.categories_ref,
+ "cat_count": categories.leaf_count,
+ "cat_pt_xml": self._cat_pt_xml,
+ "nsdecls": " %s" % nsdecls("c"),
+ }
+ )
+ )
+
+ return parse_xml(
+ self._multiLvl_cat_tmpl.format(
+ **{
+ "wksht_ref": self._series.categories_ref,
+ "cat_count": categories.leaf_count,
+ "lvl_xml": self._lvl_xml(categories),
+ "nsdecls": " %s" % nsdecls("c"),
+ }
+ )
+ )
+
+ @property
+ def cat_xml(self):
+ """
+ The unicode XML snippet for the ``<c:cat>`` element for this series,
+ containing the category labels and spreadsheet reference.
+ """
+ categories = self._series.categories
+
+ if categories.are_numeric:
+ return self._numRef_cat_tmpl.format(
+ **{
+ "wksht_ref": self._series.categories_ref,
+ "number_format": categories.number_format,
+ "cat_count": categories.leaf_count,
+ "cat_pt_xml": self._cat_num_pt_xml,
+ "nsdecls": "",
+ }
+ )
+
+ if categories.depth == 1:
+ return self._cat_tmpl.format(
+ **{
+ "wksht_ref": self._series.categories_ref,
+ "cat_count": categories.leaf_count,
+ "cat_pt_xml": self._cat_pt_xml,
+ "nsdecls": "",
+ }
+ )
+
+ return self._multiLvl_cat_tmpl.format(
+ **{
+ "wksht_ref": self._series.categories_ref,
+ "cat_count": categories.leaf_count,
+ "lvl_xml": self._lvl_xml(categories),
+ "nsdecls": "",
+ }
+ )
+
+ @property
+ def val(self):
+ """
+ The ``<c:val>`` XML for this series, as an oxml element.
+ """
+ xml = self._val_tmpl.format(
+ **{
+ "nsdecls": " %s" % nsdecls("c"),
+ "values_ref": self._series.values_ref,
+ "number_format": self._series.number_format,
+ "val_count": len(self._series),
+ "val_pt_xml": self._val_pt_xml,
+ }
+ )
+ return parse_xml(xml)
+
+ @property
+ def val_xml(self):
+ """
+ Return the unicode XML snippet for the ``<c:val>`` element describing
+ this series, containing the series values and their spreadsheet range
+ reference.
+ """
+ return self._val_tmpl.format(
+ **{
+ "nsdecls": "",
+ "values_ref": self._series.values_ref,
+ "number_format": self._series.number_format,
+ "val_count": len(self._series),
+ "val_pt_xml": self._val_pt_xml,
+ }
+ )
+
+ @property
+ def _cat_num_pt_xml(self):
+ """
+ The unicode XML snippet for the ``<c:pt>`` elements when category
+ labels are numeric (including date type).
+ """
+ xml = ""
+ for idx, category in enumerate(self._series.categories):
+ xml += (
+ ' <c:pt idx="{cat_idx}">\n'
+ " <c:v>{cat_lbl_str}</c:v>\n"
+ " </c:pt>\n"
+ ).format(
+ **{
+ "cat_idx": idx,
+ "cat_lbl_str": category.numeric_str_val(self._date_1904),
+ }
+ )
+ return xml
+
+ @property
+ def _cat_pt_xml(self):
+ """
+ The unicode XML snippet for the ``<c:pt>`` elements containing the
+ category names for this series.
+ """
+ xml = ""
+ for idx, category in enumerate(self._series.categories):
+ xml += (
+ ' <c:pt idx="{cat_idx}">\n'
+ " <c:v>{cat_label}</c:v>\n"
+ " </c:pt>\n"
+ ).format(**{"cat_idx": idx, "cat_label": escape(str(category.label))})
+ return xml
+
+ @property
+ def _cat_tmpl(self):
+ """
+ The template for the ``<c:cat>`` element for this series, containing
+ the category labels and spreadsheet reference.
+ """
+ return (
+ " <c:cat{nsdecls}>\n"
+ " <c:strRef>\n"
+ " <c:f>{wksht_ref}</c:f>\n"
+ " <c:strCache>\n"
+ ' <c:ptCount val="{cat_count}"/>\n'
+ "{cat_pt_xml}"
+ " </c:strCache>\n"
+ " </c:strRef>\n"
+ " </c:cat>\n"
+ )
+
+ def _lvl_xml(self, categories):
+ """
+ The unicode XML snippet for the ``<c:lvl>`` elements containing
+ multi-level category names.
+ """
+
+ def lvl_pt_xml(level):
+ xml = ""
+ for idx, name in level:
+ xml += (
+ ' <c:pt idx="%d">\n'
+ " <c:v>%s</c:v>\n"
+ " </c:pt>\n"
+ ) % (idx, escape("%s" % name))
+ return xml
+
+ xml = ""
+ for level in categories.levels:
+ xml += (" <c:lvl>\n" "{lvl_pt_xml}" " </c:lvl>\n").format(
+ **{"lvl_pt_xml": lvl_pt_xml(level)}
+ )
+ return xml
+
+ @property
+ def _multiLvl_cat_tmpl(self):
+ """
+ The template for the ``<c:cat>`` element for this series when there
+ are multi-level (nested) categories.
+ """
+ return (
+ " <c:cat{nsdecls}>\n"
+ " <c:multiLvlStrRef>\n"
+ " <c:f>{wksht_ref}</c:f>\n"
+ " <c:multiLvlStrCache>\n"
+ ' <c:ptCount val="{cat_count}"/>\n'
+ "{lvl_xml}"
+ " </c:multiLvlStrCache>\n"
+ " </c:multiLvlStrRef>\n"
+ " </c:cat>\n"
+ )
+
+ @property
+ def _numRef_cat_tmpl(self):
+ """
+ The template for the ``<c:cat>`` element for this series when the
+ labels are numeric (or date) values.
+ """
+ return (
+ " <c:cat{nsdecls}>\n"
+ " <c:numRef>\n"
+ " <c:f>{wksht_ref}</c:f>\n"
+ " <c:numCache>\n"
+ " <c:formatCode>{number_format}</c:formatCode>\n"
+ ' <c:ptCount val="{cat_count}"/>\n'
+ "{cat_pt_xml}"
+ " </c:numCache>\n"
+ " </c:numRef>\n"
+ " </c:cat>\n"
+ )
+
+ @property
+ def _val_pt_xml(self):
+ """
+ The unicode XML snippet containing the ``<c:pt>`` elements containing
+ the values for this series.
+ """
+ xml = ""
+ for idx, value in enumerate(self._series.values):
+ if value is None:
+ continue
+ xml += (
+ ' <c:pt idx="{val_idx:d}">\n'
+ " <c:v>{value}</c:v>\n"
+ " </c:pt>\n"
+ ).format(**{"val_idx": idx, "value": value})
+ return xml
+
+ @property
+ def _val_tmpl(self):
+ """
+ The template for the ``<c:val>`` element for this series, containing
+ the series values and their spreadsheet range reference.
+ """
+ return (
+ " <c:val{nsdecls}>\n"
+ " <c:numRef>\n"
+ " <c:f>{values_ref}</c:f>\n"
+ " <c:numCache>\n"
+ " <c:formatCode>{number_format}</c:formatCode>\n"
+ ' <c:ptCount val="{val_count}"/>\n'
+ "{val_pt_xml}"
+ " </c:numCache>\n"
+ " </c:numRef>\n"
+ " </c:val>\n"
+ )
+
+
+class _XySeriesXmlWriter(_BaseSeriesXmlWriter):
+ """
+ Generates XML snippets particular to an XY series.
+ """
+
+ @property
+ def xVal(self):
+ """
+ Return the ``<c:xVal>`` element for this series as an oxml element.
+ This element contains the X values for this series.
+ """
+ xml = self._xVal_tmpl.format(
+ **{
+ "nsdecls": " %s" % nsdecls("c"),
+ "numRef_xml": self.numRef_xml(
+ self._series.x_values_ref,
+ self._series.number_format,
+ self._series.x_values,
+ ),
+ }
+ )
+ return parse_xml(xml)
+
+ @property
+ def xVal_xml(self):
+ """
+ Return the ``<c:xVal>`` element for this series as unicode text. This
+ element contains the X values for this series.
+ """
+ return self._xVal_tmpl.format(
+ **{
+ "nsdecls": "",
+ "numRef_xml": self.numRef_xml(
+ self._series.x_values_ref,
+ self._series.number_format,
+ self._series.x_values,
+ ),
+ }
+ )
+
+ @property
+ def yVal(self):
+ """
+ Return the ``<c:yVal>`` element for this series as an oxml element.
+ This element contains the Y values for this series.
+ """
+ xml = self._yVal_tmpl.format(
+ **{
+ "nsdecls": " %s" % nsdecls("c"),
+ "numRef_xml": self.numRef_xml(
+ self._series.y_values_ref,
+ self._series.number_format,
+ self._series.y_values,
+ ),
+ }
+ )
+ return parse_xml(xml)
+
+ @property
+ def yVal_xml(self):
+ """
+ Return the ``<c:yVal>`` element for this series as unicode text. This
+ element contains the Y values for this series.
+ """
+ return self._yVal_tmpl.format(
+ **{
+ "nsdecls": "",
+ "numRef_xml": self.numRef_xml(
+ self._series.y_values_ref,
+ self._series.number_format,
+ self._series.y_values,
+ ),
+ }
+ )
+
+ @property
+ def _xVal_tmpl(self):
+ """
+ The template for the ``<c:xVal>`` element for this series, containing
+ the X values and their spreadsheet range reference.
+ """
+ return " <c:xVal{nsdecls}>\n" "{numRef_xml}" " </c:xVal>\n"
+
+ @property
+ def _yVal_tmpl(self):
+ """
+ The template for the ``<c:yVal>`` element for this series, containing
+ the Y values and their spreadsheet range reference.
+ """
+ return " <c:yVal{nsdecls}>\n" "{numRef_xml}" " </c:yVal>\n"
+
+
+class _BubbleSeriesXmlWriter(_XySeriesXmlWriter):
+ """
+ Generates XML snippets particular to a bubble chart series.
+ """
+
+ @property
+ def bubbleSize(self):
+ """
+ Return the ``<c:bubbleSize>`` element for this series as an oxml
+ element. This element contains the bubble size values for this
+ series.
+ """
+ xml = self._bubbleSize_tmpl.format(
+ **{
+ "nsdecls": " %s" % nsdecls("c"),
+ "numRef_xml": self.numRef_xml(
+ self._series.bubble_sizes_ref,
+ self._series.number_format,
+ self._series.bubble_sizes,
+ ),
+ }
+ )
+ return parse_xml(xml)
+
+ @property
+ def bubbleSize_xml(self):
+ """
+ Return the ``<c:bubbleSize>`` element for this series as unicode
+ text. This element contains the bubble size values for all the
+ data points in the chart.
+ """
+ return self._bubbleSize_tmpl.format(
+ **{
+ "nsdecls": "",
+ "numRef_xml": self.numRef_xml(
+ self._series.bubble_sizes_ref,
+ self._series.number_format,
+ self._series.bubble_sizes,
+ ),
+ }
+ )
+
+ @property
+ def _bubbleSize_tmpl(self):
+ """
+ The template for the ``<c:bubbleSize>`` element for this series,
+ containing the bubble size values and their spreadsheet range
+ reference.
+ """
+ return " <c:bubbleSize{nsdecls}>\n" "{numRef_xml}" " </c:bubbleSize>\n"
+
+
+class _BubbleSeriesXmlRewriter(_BaseSeriesXmlRewriter):
+ """
+ A series rewriter suitable for bubble charts.
+ """
+
+ def _rewrite_ser_data(self, ser, series_data, date_1904):
+ """
+ Rewrite the ``<c:tx>``, ``<c:cat>`` and ``<c:val>`` child elements
+ of *ser* based on the values in *series_data*.
+ """
+ ser._remove_tx()
+ ser._remove_xVal()
+ ser._remove_yVal()
+ ser._remove_bubbleSize()
+
+ xml_writer = _BubbleSeriesXmlWriter(series_data)
+
+ ser._insert_tx(xml_writer.tx)
+ ser._insert_xVal(xml_writer.xVal)
+ ser._insert_yVal(xml_writer.yVal)
+ ser._insert_bubbleSize(xml_writer.bubbleSize)
+
+
+class _CategorySeriesXmlRewriter(_BaseSeriesXmlRewriter):
+ """
+ A series rewriter suitable for category charts.
+ """
+
+ def _rewrite_ser_data(self, ser, series_data, date_1904):
+ """
+ Rewrite the ``<c:tx>``, ``<c:cat>`` and ``<c:val>`` child elements
+ of *ser* based on the values in *series_data*.
+ """
+ ser._remove_tx()
+ ser._remove_cat()
+ ser._remove_val()
+
+ xml_writer = _CategorySeriesXmlWriter(series_data, date_1904)
+
+ ser._insert_tx(xml_writer.tx)
+ ser._insert_cat(xml_writer.cat)
+ ser._insert_val(xml_writer.val)
+
+
+class _XySeriesXmlRewriter(_BaseSeriesXmlRewriter):
+ """
+ A series rewriter suitable for XY (aka. scatter) charts.
+ """
+
+ def _rewrite_ser_data(self, ser, series_data, date_1904):
+ """
+ Rewrite the ``<c:tx>``, ``<c:xVal>`` and ``<c:yVal>`` child elements
+ of *ser* based on the values in *series_data*.
+ """
+ ser._remove_tx()
+ ser._remove_xVal()
+ ser._remove_yVal()
+
+ xml_writer = _XySeriesXmlWriter(series_data)
+
+ ser._insert_tx(xml_writer.tx)
+ ser._insert_xVal(xml_writer.xVal)
+ ser._insert_yVal(xml_writer.yVal)
diff --git a/.venv/lib/python3.12/site-packages/pptx/dml/__init__.py b/.venv/lib/python3.12/site-packages/pptx/dml/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/dml/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/dml/chtfmt.py b/.venv/lib/python3.12/site-packages/pptx/dml/chtfmt.py
new file mode 100644
index 00000000..c37e4844
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/dml/chtfmt.py
@@ -0,0 +1,40 @@
+"""|ChartFormat| and related objects.
+
+|ChartFormat| acts as proxy for the `spPr` element, which provides visual shape properties such as
+line and fill for chart elements.
+"""
+
+from __future__ import annotations
+
+from pptx.dml.fill import FillFormat
+from pptx.dml.line import LineFormat
+from pptx.shared import ElementProxy
+from pptx.util import lazyproperty
+
+
+class ChartFormat(ElementProxy):
+ """
+ The |ChartFormat| object provides access to visual shape properties for
+ chart elements like |Axis|, |Series|, and |MajorGridlines|. It has two
+ properties, :attr:`fill` and :attr:`line`, which return a |FillFormat|
+ and |LineFormat| object respectively. The |ChartFormat| object is
+ provided by the :attr:`format` property on the target axis, series, etc.
+ """
+
+ @lazyproperty
+ def fill(self):
+ """
+ |FillFormat| instance for this object, providing access to fill
+ properties such as fill color.
+ """
+ spPr = self._element.get_or_add_spPr()
+ return FillFormat.from_fill_parent(spPr)
+
+ @lazyproperty
+ def line(self):
+ """
+ The |LineFormat| object providing access to the visual properties of
+ this object, such as line color and line style.
+ """
+ spPr = self._element.get_or_add_spPr()
+ return LineFormat(spPr)
diff --git a/.venv/lib/python3.12/site-packages/pptx/dml/color.py b/.venv/lib/python3.12/site-packages/pptx/dml/color.py
new file mode 100644
index 00000000..54155823
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/dml/color.py
@@ -0,0 +1,301 @@
+"""DrawingML objects related to color, ColorFormat being the most prominent."""
+
+from __future__ import annotations
+
+from pptx.enum.dml import MSO_COLOR_TYPE, MSO_THEME_COLOR
+from pptx.oxml.dml.color import (
+ CT_HslColor,
+ CT_PresetColor,
+ CT_SchemeColor,
+ CT_ScRgbColor,
+ CT_SRgbColor,
+ CT_SystemColor,
+)
+
+
+class ColorFormat(object):
+ """
+ Provides access to color settings such as RGB color, theme color, and
+ luminance adjustments.
+ """
+
+ def __init__(self, eg_colorChoice_parent, color):
+ super(ColorFormat, self).__init__()
+ self._xFill = eg_colorChoice_parent
+ self._color = color
+
+ @property
+ def brightness(self):
+ """
+ Read/write float value between -1.0 and 1.0 indicating the brightness
+ adjustment for this color, e.g. -0.25 is 25% darker and 0.4 is 40%
+ lighter. 0 means no brightness adjustment.
+ """
+ return self._color.brightness
+
+ @brightness.setter
+ def brightness(self, value):
+ self._validate_brightness_value(value)
+ self._color.brightness = value
+
+ @classmethod
+ def from_colorchoice_parent(cls, eg_colorChoice_parent):
+ xClr = eg_colorChoice_parent.eg_colorChoice
+ color = _Color(xClr)
+ color_format = cls(eg_colorChoice_parent, color)
+ return color_format
+
+ @property
+ def rgb(self):
+ """
+ |RGBColor| value of this color, or None if no RGB color is explicitly
+ defined for this font. Setting this value to an |RGBColor| instance
+ causes its type to change to MSO_COLOR_TYPE.RGB. If the color was a
+ theme color with a brightness adjustment, the brightness adjustment
+ is removed when changing it to an RGB color.
+ """
+ return self._color.rgb
+
+ @rgb.setter
+ def rgb(self, rgb):
+ if not isinstance(rgb, RGBColor):
+ raise ValueError("assigned value must be type RGBColor")
+ # change to rgb color format if not already
+ if not isinstance(self._color, _SRgbColor):
+ srgbClr = self._xFill.get_or_change_to_srgbClr()
+ self._color = _SRgbColor(srgbClr)
+ # call _SRgbColor instance to do the setting
+ self._color.rgb = rgb
+
+ @property
+ def theme_color(self):
+ """Theme color value of this color.
+
+ Value is a member of :ref:`MsoThemeColorIndex`, e.g.
+ ``MSO_THEME_COLOR.ACCENT_1``. Raises AttributeError on access if the
+ color is not type ``MSO_COLOR_TYPE.SCHEME``. Assigning a member of
+ :ref:`MsoThemeColorIndex` causes the color's type to change to
+ ``MSO_COLOR_TYPE.SCHEME``.
+ """
+ return self._color.theme_color
+
+ @theme_color.setter
+ def theme_color(self, mso_theme_color_idx):
+ # change to theme color format if not already
+ if not isinstance(self._color, _SchemeColor):
+ schemeClr = self._xFill.get_or_change_to_schemeClr()
+ self._color = _SchemeColor(schemeClr)
+ self._color.theme_color = mso_theme_color_idx
+
+ @property
+ def type(self):
+ """
+ Read-only. A value from :ref:`MsoColorType`, either RGB or SCHEME,
+ corresponding to the way this color is defined, or None if no color
+ is defined at the level of this font.
+ """
+ return self._color.color_type
+
+ def _validate_brightness_value(self, value):
+ if value < -1.0 or value > 1.0:
+ raise ValueError("brightness must be number in range -1.0 to 1.0")
+ if isinstance(self._color, _NoneColor):
+ msg = (
+ "can't set brightness when color.type is None. Set color.rgb"
+ " or .theme_color first."
+ )
+ raise ValueError(msg)
+
+
+class _Color(object):
+ """
+ Object factory for color object of the appropriate type, also the base
+ class for all color type classes such as SRgbColor.
+ """
+
+ def __new__(cls, xClr):
+ color_cls = {
+ type(None): _NoneColor,
+ CT_HslColor: _HslColor,
+ CT_PresetColor: _PrstColor,
+ CT_SchemeColor: _SchemeColor,
+ CT_ScRgbColor: _ScRgbColor,
+ CT_SRgbColor: _SRgbColor,
+ CT_SystemColor: _SysColor,
+ }[type(xClr)]
+ return super(_Color, cls).__new__(color_cls)
+
+ def __init__(self, xClr):
+ super(_Color, self).__init__()
+ self._xClr = xClr
+
+ @property
+ def brightness(self):
+ lumMod, lumOff = self._xClr.lumMod, self._xClr.lumOff
+ # a tint is lighter, a shade is darker
+ # only tints have lumOff child
+ if lumOff is not None:
+ brightness = lumOff.val
+ return brightness
+ # which leaves shades, if lumMod is present
+ if lumMod is not None:
+ brightness = lumMod.val - 1.0
+ return brightness
+ # there's no brightness adjustment if no lum{Mod|Off} elements
+ return 0
+
+ @brightness.setter
+ def brightness(self, value):
+ if value > 0:
+ self._tint(value)
+ elif value < 0:
+ self._shade(value)
+ else:
+ self._xClr.clear_lum()
+
+ @property
+ def color_type(self): # pragma: no cover
+ tmpl = ".color_type property must be implemented on %s"
+ raise NotImplementedError(tmpl % self.__class__.__name__)
+
+ @property
+ def rgb(self):
+ """
+ Raises TypeError on access unless overridden by subclass.
+ """
+ tmpl = "no .rgb property on color type '%s'"
+ raise AttributeError(tmpl % self.__class__.__name__)
+
+ @property
+ def theme_color(self):
+ """
+ Raises TypeError on access unless overridden by subclass.
+ """
+ return MSO_THEME_COLOR.NOT_THEME_COLOR
+
+ def _shade(self, value):
+ lumMod_val = 1.0 - abs(value)
+ color_elm = self._xClr.clear_lum()
+ color_elm.add_lumMod(lumMod_val)
+
+ def _tint(self, value):
+ lumOff_val = value
+ lumMod_val = 1.0 - lumOff_val
+ color_elm = self._xClr.clear_lum()
+ color_elm.add_lumMod(lumMod_val)
+ color_elm.add_lumOff(lumOff_val)
+
+
+class _HslColor(_Color):
+ @property
+ def color_type(self):
+ return MSO_COLOR_TYPE.HSL
+
+
+class _NoneColor(_Color):
+ @property
+ def color_type(self):
+ return None
+
+ @property
+ def theme_color(self):
+ """
+ Raise TypeError on attempt to access .theme_color when no color
+ choice is present.
+ """
+ tmpl = "no .theme_color property on color type '%s'"
+ raise AttributeError(tmpl % self.__class__.__name__)
+
+
+class _PrstColor(_Color):
+ @property
+ def color_type(self):
+ return MSO_COLOR_TYPE.PRESET
+
+
+class _SchemeColor(_Color):
+ def __init__(self, schemeClr):
+ super(_SchemeColor, self).__init__(schemeClr)
+ self._schemeClr = schemeClr
+
+ @property
+ def color_type(self):
+ return MSO_COLOR_TYPE.SCHEME
+
+ @property
+ def theme_color(self):
+ """
+ Theme color value of this color, one of those defined in the
+ MSO_THEME_COLOR enumeration, e.g. MSO_THEME_COLOR.ACCENT_1. None if
+ no theme color is explicitly defined for this font. Setting this to a
+ value in MSO_THEME_COLOR causes the color's type to change to
+ ``MSO_COLOR_TYPE.SCHEME``.
+ """
+ return self._schemeClr.val
+
+ @theme_color.setter
+ def theme_color(self, mso_theme_color_idx):
+ self._schemeClr.val = mso_theme_color_idx
+
+
+class _ScRgbColor(_Color):
+ @property
+ def color_type(self):
+ return MSO_COLOR_TYPE.SCRGB
+
+
+class _SRgbColor(_Color):
+ def __init__(self, srgbClr):
+ super(_SRgbColor, self).__init__(srgbClr)
+ self._srgbClr = srgbClr
+
+ @property
+ def color_type(self):
+ return MSO_COLOR_TYPE.RGB
+
+ @property
+ def rgb(self):
+ """
+ |RGBColor| value of this color, corresponding to the value in the
+ required ``val`` attribute of the ``<a:srgbColr>`` element.
+ """
+ return RGBColor.from_string(self._srgbClr.val)
+
+ @rgb.setter
+ def rgb(self, rgb):
+ self._srgbClr.val = str(rgb)
+
+
+class _SysColor(_Color):
+ @property
+ def color_type(self):
+ return MSO_COLOR_TYPE.SYSTEM
+
+
+class RGBColor(tuple):
+ """
+ Immutable value object defining a particular RGB color.
+ """
+
+ def __new__(cls, r, g, b):
+ msg = "RGBColor() takes three integer values 0-255"
+ for val in (r, g, b):
+ if not isinstance(val, int) or val < 0 or val > 255:
+ raise ValueError(msg)
+ return super(RGBColor, cls).__new__(cls, (r, g, b))
+
+ def __str__(self):
+ """
+ Return a hex string rgb value, like '3C2F80'
+ """
+ return "%02X%02X%02X" % self
+
+ @classmethod
+ def from_string(cls, rgb_hex_str):
+ """
+ Return a new instance from an RGB color hex string like ``'3C2F80'``.
+ """
+ r = int(rgb_hex_str[:2], 16)
+ g = int(rgb_hex_str[2:4], 16)
+ b = int(rgb_hex_str[4:], 16)
+ return cls(r, g, b)
diff --git a/.venv/lib/python3.12/site-packages/pptx/dml/effect.py b/.venv/lib/python3.12/site-packages/pptx/dml/effect.py
new file mode 100644
index 00000000..9df69ce4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/dml/effect.py
@@ -0,0 +1,41 @@
+"""Visual effects on a shape such as shadow, glow, and reflection."""
+
+from __future__ import annotations
+
+
+class ShadowFormat(object):
+ """Provides access to shadow effect on a shape."""
+
+ def __init__(self, spPr):
+ # ---spPr may also be a grpSpPr; both have a:effectLst child---
+ self._element = spPr
+
+ @property
+ def inherit(self):
+ """True if shape inherits shadow settings.
+
+ Read/write. An explicitly-defined shadow setting on a shape causes
+ this property to return |False|. A shape with no explicitly-defined
+ shadow setting inherits its shadow settings from the style hierarchy
+ (and so returns |True|).
+
+ Assigning |True| causes any explicitly-defined shadow setting to be
+ removed and inheritance is restored. Note this has the side-effect of
+ removing **all** explicitly-defined effects, such as glow and
+ reflection, and restoring inheritance for all effects on the shape.
+ Assigning |False| causes the inheritance link to be broken and **no**
+ effects to appear on the shape.
+ """
+ if self._element.effectLst is None:
+ return True
+ return False
+
+ @inherit.setter
+ def inherit(self, value):
+ inherit = bool(value)
+ if inherit:
+ # ---remove any explicitly-defined effects
+ self._element._remove_effectLst()
+ else:
+ # ---ensure at least the effectLst element is present
+ self._element.get_or_add_effectLst()
diff --git a/.venv/lib/python3.12/site-packages/pptx/dml/fill.py b/.venv/lib/python3.12/site-packages/pptx/dml/fill.py
new file mode 100644
index 00000000..8212af9e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/dml/fill.py
@@ -0,0 +1,398 @@
+"""DrawingML objects related to fill."""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+from pptx.dml.color import ColorFormat
+from pptx.enum.dml import MSO_FILL
+from pptx.oxml.dml.fill import (
+ CT_BlipFillProperties,
+ CT_GradientFillProperties,
+ CT_GroupFillProperties,
+ CT_NoFillProperties,
+ CT_PatternFillProperties,
+ CT_SolidColorFillProperties,
+)
+from pptx.oxml.xmlchemy import BaseOxmlElement
+from pptx.shared import ElementProxy
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.enum.dml import MSO_FILL_TYPE
+ from pptx.oxml.xmlchemy import BaseOxmlElement
+
+
+class FillFormat(object):
+ """Provides access to the current fill properties.
+
+ Also provides methods to change the fill type.
+ """
+
+ def __init__(self, eg_fill_properties_parent: BaseOxmlElement, fill_obj: _Fill):
+ super(FillFormat, self).__init__()
+ self._xPr = eg_fill_properties_parent
+ self._fill = fill_obj
+
+ @classmethod
+ def from_fill_parent(cls, eg_fillProperties_parent: BaseOxmlElement) -> FillFormat:
+ """
+ Return a |FillFormat| instance initialized to the settings contained
+ in *eg_fillProperties_parent*, which must be an element having
+ EG_FillProperties in its child element sequence in the XML schema.
+ """
+ fill_elm = eg_fillProperties_parent.eg_fillProperties
+ fill = _Fill(fill_elm)
+ fill_format = cls(eg_fillProperties_parent, fill)
+ return fill_format
+
+ @property
+ def back_color(self):
+ """Return a |ColorFormat| object representing background color.
+
+ This property is only applicable to pattern fills and lines.
+ """
+ return self._fill.back_color
+
+ def background(self):
+ """
+ Sets the fill type to noFill, i.e. transparent.
+ """
+ noFill = self._xPr.get_or_change_to_noFill()
+ self._fill = _NoFill(noFill)
+
+ @property
+ def fore_color(self):
+ """
+ Return a |ColorFormat| instance representing the foreground color of
+ this fill.
+ """
+ return self._fill.fore_color
+
+ def gradient(self):
+ """Sets the fill type to gradient.
+
+ If the fill is not already a gradient, a default gradient is added.
+ The default gradient corresponds to the default in the built-in
+ PowerPoint "White" template. This gradient is linear at angle
+ 90-degrees (upward), with two stops. The first stop is Accent-1 with
+ tint 100%, shade 100%, and satMod 130%. The second stop is Accent-1
+ with tint 50%, shade 100%, and satMod 350%.
+ """
+ gradFill = self._xPr.get_or_change_to_gradFill()
+ self._fill = _GradFill(gradFill)
+
+ @property
+ def gradient_angle(self):
+ """Angle in float degrees of line of a linear gradient.
+
+ Read/Write. May be |None|, indicating the angle should be inherited
+ from the style hierarchy. An angle of 0.0 corresponds to
+ a left-to-right gradient. Increasing angles represent
+ counter-clockwise rotation of the line, for example 90.0 represents
+ a bottom-to-top gradient. Raises |TypeError| when the fill type is
+ not MSO_FILL_TYPE.GRADIENT. Raises |ValueError| for a non-linear
+ gradient (e.g. a radial gradient).
+ """
+ if self.type != MSO_FILL.GRADIENT:
+ raise TypeError("Fill is not of type MSO_FILL_TYPE.GRADIENT")
+ return self._fill.gradient_angle
+
+ @gradient_angle.setter
+ def gradient_angle(self, value):
+ if self.type != MSO_FILL.GRADIENT:
+ raise TypeError("Fill is not of type MSO_FILL_TYPE.GRADIENT")
+ self._fill.gradient_angle = value
+
+ @property
+ def gradient_stops(self):
+ """|GradientStops| object providing access to stops of this gradient.
+
+ Raises |TypeError| when fill is not gradient (call `fill.gradient()`
+ first). Each stop represents a color between which the gradient
+ smoothly transitions.
+ """
+ if self.type != MSO_FILL.GRADIENT:
+ raise TypeError("Fill is not of type MSO_FILL_TYPE.GRADIENT")
+ return self._fill.gradient_stops
+
+ @property
+ def pattern(self):
+ """Return member of :ref:`MsoPatternType` indicating fill pattern.
+
+ Raises |TypeError| when fill is not patterned (call
+ `fill.patterned()` first). Returns |None| if no pattern has been set;
+ PowerPoint may display the default `PERCENT_5` pattern in this case.
+ Assigning |None| will remove any explicit pattern setting, although
+ relying on the default behavior is discouraged and may produce
+ rendering differences across client applications.
+ """
+ return self._fill.pattern
+
+ @pattern.setter
+ def pattern(self, pattern_type):
+ self._fill.pattern = pattern_type
+
+ def patterned(self):
+ """Selects the pattern fill type.
+
+ Note that calling this method does not by itself set a foreground or
+ background color of the pattern. Rather it enables subsequent
+ assignments to properties like fore_color to set the pattern and
+ colors.
+ """
+ pattFill = self._xPr.get_or_change_to_pattFill()
+ self._fill = _PattFill(pattFill)
+
+ def solid(self):
+ """
+ Sets the fill type to solid, i.e. a solid color. Note that calling
+ this method does not set a color or by itself cause the shape to
+ appear with a solid color fill; rather it enables subsequent
+ assignments to properties like fore_color to set the color.
+ """
+ solidFill = self._xPr.get_or_change_to_solidFill()
+ self._fill = _SolidFill(solidFill)
+
+ @property
+ def type(self) -> MSO_FILL_TYPE:
+ """The type of this fill, e.g. `MSO_FILL_TYPE.SOLID`."""
+ return self._fill.type
+
+
+class _Fill(object):
+ """
+ Object factory for fill object of class matching fill element, such as
+ _SolidFill for ``<a:solidFill>``; also serves as the base class for all
+ fill classes
+ """
+
+ def __new__(cls, xFill):
+ if xFill is None:
+ fill_cls = _NoneFill
+ elif isinstance(xFill, CT_BlipFillProperties):
+ fill_cls = _BlipFill
+ elif isinstance(xFill, CT_GradientFillProperties):
+ fill_cls = _GradFill
+ elif isinstance(xFill, CT_GroupFillProperties):
+ fill_cls = _GrpFill
+ elif isinstance(xFill, CT_NoFillProperties):
+ fill_cls = _NoFill
+ elif isinstance(xFill, CT_PatternFillProperties):
+ fill_cls = _PattFill
+ elif isinstance(xFill, CT_SolidColorFillProperties):
+ fill_cls = _SolidFill
+ else:
+ fill_cls = _Fill
+ return super(_Fill, cls).__new__(fill_cls)
+
+ @property
+ def back_color(self):
+ """Raise TypeError for types that do not override this property."""
+ tmpl = "fill type %s has no background color, call .patterned() first"
+ raise TypeError(tmpl % self.__class__.__name__)
+
+ @property
+ def fore_color(self):
+ """Raise TypeError for types that do not override this property."""
+ tmpl = "fill type %s has no foreground color, call .solid() or .pattern" "ed() first"
+ raise TypeError(tmpl % self.__class__.__name__)
+
+ @property
+ def pattern(self):
+ """Raise TypeError for fills that do not override this property."""
+ tmpl = "fill type %s has no pattern, call .patterned() first"
+ raise TypeError(tmpl % self.__class__.__name__)
+
+ @property
+ def type(self) -> MSO_FILL_TYPE: # pragma: no cover
+ raise NotImplementedError(
+ f".type property must be implemented on {self.__class__.__name__}"
+ )
+
+
+class _BlipFill(_Fill):
+ @property
+ def type(self):
+ return MSO_FILL.PICTURE
+
+
+class _GradFill(_Fill):
+ """Proxies an `a:gradFill` element."""
+
+ def __init__(self, gradFill):
+ self._element = self._gradFill = gradFill
+
+ @property
+ def gradient_angle(self):
+ """Angle in float degrees of line of a linear gradient.
+
+ Read/Write. May be |None|, indicating the angle is inherited from the
+ style hierarchy. An angle of 0.0 corresponds to a left-to-right
+ gradient. Increasing angles represent clockwise rotation of the line,
+ for example 90.0 represents a top-to-bottom gradient. Raises
+ |TypeError| when the fill type is not MSO_FILL_TYPE.GRADIENT. Raises
+ |ValueError| for a non-linear gradient (e.g. a radial gradient).
+ """
+ # ---case 1: gradient path is explicit, but not linear---
+ path = self._gradFill.path
+ if path is not None:
+ raise ValueError("not a linear gradient")
+
+ # ---case 2: gradient path is inherited (no a:lin OR a:path)---
+ lin = self._gradFill.lin
+ if lin is None:
+ return None
+
+ # ---case 3: gradient path is explicitly linear---
+ # angle is stored in XML as a clockwise angle, whereas the UI
+ # reports it as counter-clockwise from horizontal-pointing-right.
+ # Since the UI is consistent with trigonometry conventions, we
+ # respect that in the API.
+ clockwise_angle = lin.ang
+ counter_clockwise_angle = 0.0 if clockwise_angle == 0.0 else (360.0 - clockwise_angle)
+ return counter_clockwise_angle
+
+ @gradient_angle.setter
+ def gradient_angle(self, value):
+ lin = self._gradFill.lin
+ if lin is None:
+ raise ValueError("not a linear gradient")
+ lin.ang = 360.0 - value
+
+ @lazyproperty
+ def gradient_stops(self):
+ """|_GradientStops| object providing access to gradient colors.
+
+ Each stop represents a color between which the gradient smoothly
+ transitions.
+ """
+ return _GradientStops(self._gradFill.get_or_add_gsLst())
+
+ @property
+ def type(self):
+ return MSO_FILL.GRADIENT
+
+
+class _GrpFill(_Fill):
+ @property
+ def type(self):
+ return MSO_FILL.GROUP
+
+
+class _NoFill(_Fill):
+ @property
+ def type(self):
+ return MSO_FILL.BACKGROUND
+
+
+class _NoneFill(_Fill):
+ @property
+ def type(self):
+ return None
+
+
+class _PattFill(_Fill):
+ """Provides access to patterned fill properties."""
+
+ def __init__(self, pattFill):
+ super(_PattFill, self).__init__()
+ self._element = self._pattFill = pattFill
+
+ @lazyproperty
+ def back_color(self):
+ """Return |ColorFormat| object that controls background color."""
+ bgClr = self._pattFill.get_or_add_bgClr()
+ return ColorFormat.from_colorchoice_parent(bgClr)
+
+ @lazyproperty
+ def fore_color(self):
+ """Return |ColorFormat| object that controls foreground color."""
+ fgClr = self._pattFill.get_or_add_fgClr()
+ return ColorFormat.from_colorchoice_parent(fgClr)
+
+ @property
+ def pattern(self):
+ """Return member of :ref:`MsoPatternType` indicating fill pattern.
+
+ Returns |None| if no pattern has been set; PowerPoint may display the
+ default `PERCENT_5` pattern in this case. Assigning |None| will
+ remove any explicit pattern setting.
+ """
+ return self._pattFill.prst
+
+ @pattern.setter
+ def pattern(self, pattern_type):
+ self._pattFill.prst = pattern_type
+
+ @property
+ def type(self):
+ return MSO_FILL.PATTERNED
+
+
+class _SolidFill(_Fill):
+ """Provides access to fill properties such as color for solid fills."""
+
+ def __init__(self, solidFill):
+ super(_SolidFill, self).__init__()
+ self._solidFill = solidFill
+
+ @lazyproperty
+ def fore_color(self):
+ """Return |ColorFormat| object controlling fill color."""
+ return ColorFormat.from_colorchoice_parent(self._solidFill)
+
+ @property
+ def type(self):
+ return MSO_FILL.SOLID
+
+
+class _GradientStops(Sequence):
+ """Collection of |GradientStop| objects defining gradient colors.
+
+ A gradient must have a minimum of two stops, but can have as many more
+ than that as required to achieve the desired effect (three is perhaps
+ most common). Stops are sequenced in the order they are transitioned
+ through.
+ """
+
+ def __init__(self, gsLst):
+ self._gsLst = gsLst
+
+ def __getitem__(self, idx):
+ return _GradientStop(self._gsLst[idx])
+
+ def __len__(self):
+ return len(self._gsLst)
+
+
+class _GradientStop(ElementProxy):
+ """A single gradient stop.
+
+ A gradient stop defines a color and a position.
+ """
+
+ def __init__(self, gs):
+ super(_GradientStop, self).__init__(gs)
+ self._gs = gs
+
+ @lazyproperty
+ def color(self):
+ """Return |ColorFormat| object controlling stop color."""
+ return ColorFormat.from_colorchoice_parent(self._gs)
+
+ @property
+ def position(self):
+ """Location of stop in gradient path as float between 0.0 and 1.0.
+
+ The value represents a percentage, where 0.0 (0%) represents the
+ start of the path and 1.0 (100%) represents the end of the path. For
+ a linear gradient, these would represent opposing extents of the
+ filled area.
+ """
+ return self._gs.pos
+
+ @position.setter
+ def position(self, value):
+ self._gs.pos = float(value)
diff --git a/.venv/lib/python3.12/site-packages/pptx/dml/line.py b/.venv/lib/python3.12/site-packages/pptx/dml/line.py
new file mode 100644
index 00000000..82be47a4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/dml/line.py
@@ -0,0 +1,100 @@
+"""DrawingML objects related to line formatting."""
+
+from __future__ import annotations
+
+from pptx.dml.fill import FillFormat
+from pptx.enum.dml import MSO_FILL
+from pptx.util import Emu, lazyproperty
+
+
+class LineFormat(object):
+ """Provides access to line properties such as color, style, and width.
+
+ A LineFormat object is typically accessed via the ``.line`` property of
+ a shape such as |Shape| or |Picture|.
+ """
+
+ def __init__(self, parent):
+ super(LineFormat, self).__init__()
+ self._parent = parent
+
+ @lazyproperty
+ def color(self):
+ """
+ The |ColorFormat| instance that provides access to the color settings
+ for this line. Essentially a shortcut for ``line.fill.fore_color``.
+ As a side-effect, accessing this property causes the line fill type
+ to be set to ``MSO_FILL.SOLID``. If this sounds risky for your use
+ case, use ``line.fill.type`` to non-destructively discover the
+ existing fill type.
+ """
+ if self.fill.type != MSO_FILL.SOLID:
+ self.fill.solid()
+ return self.fill.fore_color
+
+ @property
+ def dash_style(self):
+ """Return value indicating line style.
+
+ Returns a member of :ref:`MsoLineDashStyle` indicating line style, or
+ |None| if no explicit value has been set. When no explicit value has
+ been set, the line dash style is inherited from the style hierarchy.
+
+ Assigning |None| removes any existing explicitly-defined dash style.
+ """
+ ln = self._ln
+ if ln is None:
+ return None
+ return ln.prstDash_val
+
+ @dash_style.setter
+ def dash_style(self, dash_style):
+ if dash_style is None:
+ ln = self._ln
+ if ln is None:
+ return
+ ln._remove_prstDash()
+ ln._remove_custDash()
+ return
+ ln = self._get_or_add_ln()
+ ln.prstDash_val = dash_style
+
+ @lazyproperty
+ def fill(self):
+ """
+ |FillFormat| instance for this line, providing access to fill
+ properties such as foreground color.
+ """
+ ln = self._get_or_add_ln()
+ return FillFormat.from_fill_parent(ln)
+
+ @property
+ def width(self):
+ """
+ The width of the line expressed as an integer number of :ref:`English
+ Metric Units <EMU>`. The returned value is an instance of |Length|,
+ a value class having properties such as `.inches`, `.cm`, and `.pt`
+ for converting the value into convenient units.
+ """
+ ln = self._ln
+ if ln is None:
+ return Emu(0)
+ return ln.w
+
+ @width.setter
+ def width(self, emu):
+ if emu is None:
+ emu = 0
+ ln = self._get_or_add_ln()
+ ln.w = emu
+
+ def _get_or_add_ln(self):
+ """
+ Return the ``<a:ln>`` element containing the line format properties
+ in the XML.
+ """
+ return self._parent.get_or_add_ln()
+
+ @property
+ def _ln(self):
+ return self._parent.ln
diff --git a/.venv/lib/python3.12/site-packages/pptx/enum/__init__.py b/.venv/lib/python3.12/site-packages/pptx/enum/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/enum/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/enum/action.py b/.venv/lib/python3.12/site-packages/pptx/enum/action.py
new file mode 100644
index 00000000..bc447226
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/enum/action.py
@@ -0,0 +1,71 @@
+"""Enumerations that describe click-action settings."""
+
+from __future__ import annotations
+
+from pptx.enum.base import BaseEnum
+
+
+class PP_ACTION_TYPE(BaseEnum):
+ """
+ Specifies the type of a mouse action (click or hover action).
+
+ Alias: ``PP_ACTION``
+
+ Example::
+
+ from pptx.enum.action import PP_ACTION
+
+ assert shape.click_action.action == PP_ACTION.HYPERLINK
+
+ MS API name: `PpActionType`
+
+ https://msdn.microsoft.com/EN-US/library/office/ff744895.aspx
+ """
+
+ END_SHOW = (6, "Slide show ends.")
+ """Slide show ends."""
+
+ FIRST_SLIDE = (3, "Returns to the first slide.")
+ """Returns to the first slide."""
+
+ HYPERLINK = (7, "Hyperlink.")
+ """Hyperlink."""
+
+ LAST_SLIDE = (4, "Moves to the last slide.")
+ """Moves to the last slide."""
+
+ LAST_SLIDE_VIEWED = (5, "Moves to the last slide viewed.")
+ """Moves to the last slide viewed."""
+
+ NAMED_SLIDE = (101, "Moves to slide specified by slide number.")
+ """Moves to slide specified by slide number."""
+
+ NAMED_SLIDE_SHOW = (10, "Runs the slideshow.")
+ """Runs the slideshow."""
+
+ NEXT_SLIDE = (1, "Moves to the next slide.")
+ """Moves to the next slide."""
+
+ NONE = (0, "No action is performed.")
+ """No action is performed."""
+
+ OPEN_FILE = (102, "Opens the specified file.")
+ """Opens the specified file."""
+
+ OLE_VERB = (11, "OLE Verb.")
+ """OLE Verb."""
+
+ PLAY = (12, "Begins the slideshow.")
+ """Begins the slideshow."""
+
+ PREVIOUS_SLIDE = (2, "Moves to the previous slide.")
+ """Moves to the previous slide."""
+
+ RUN_MACRO = (8, "Runs a macro.")
+ """Runs a macro."""
+
+ RUN_PROGRAM = (9, "Runs a program.")
+ """Runs a program."""
+
+
+PP_ACTION = PP_ACTION_TYPE
diff --git a/.venv/lib/python3.12/site-packages/pptx/enum/base.py b/.venv/lib/python3.12/site-packages/pptx/enum/base.py
new file mode 100644
index 00000000..1d49b9c1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/enum/base.py
@@ -0,0 +1,175 @@
+"""Base classes and other objects used by enumerations."""
+
+from __future__ import annotations
+
+import enum
+import textwrap
+from typing import TYPE_CHECKING, Any, Type, TypeVar
+
+if TYPE_CHECKING:
+ from typing_extensions import Self
+
+_T = TypeVar("_T", bound="BaseXmlEnum")
+
+
+class BaseEnum(int, enum.Enum):
+ """Base class for Enums that do not map XML attr values.
+
+ The enum's value will be an integer, corresponding to the integer assigned the
+ corresponding member in the MS API enum of the same name.
+ """
+
+ def __new__(cls, ms_api_value: int, docstr: str):
+ self = int.__new__(cls, ms_api_value)
+ self._value_ = ms_api_value
+ self.__doc__ = docstr.strip()
+ return self
+
+ def __str__(self):
+ """The symbolic name and string value of this member, e.g. 'MIDDLE (3)'."""
+ return f"{self.name} ({self.value})"
+
+
+class BaseXmlEnum(int, enum.Enum):
+ """Base class for Enums that also map XML attr values.
+
+ The enum's value will be an integer, corresponding to the integer assigned the
+ corresponding member in the MS API enum of the same name.
+ """
+
+ xml_value: str | None
+
+ def __new__(cls, ms_api_value: int, xml_value: str | None, docstr: str):
+ self = int.__new__(cls, ms_api_value)
+ self._value_ = ms_api_value
+ self.xml_value = xml_value
+ self.__doc__ = docstr.strip()
+ return self
+
+ def __str__(self):
+ """The symbolic name and string value of this member, e.g. 'MIDDLE (3)'."""
+ return f"{self.name} ({self.value})"
+
+ @classmethod
+ def from_xml(cls, xml_value: str) -> Self:
+ """Enumeration member corresponding to XML attribute value `xml_value`.
+
+ Raises `ValueError` if `xml_value` is the empty string ("") or is not an XML attribute
+ value registered on the enumeration. Note that enum members that do not correspond to one
+ of the defined values for an XML attribute have `xml_value == ""`. These
+ "return-value only" members cannot be automatically mapped from an XML attribute value and
+ must be selected explicitly by code, based on the appropriate conditions.
+
+ Example::
+
+ >>> WD_PARAGRAPH_ALIGNMENT.from_xml("center")
+ WD_PARAGRAPH_ALIGNMENT.CENTER
+
+ """
+ # -- the empty string never maps to a member --
+ member = (
+ next((member for member in cls if member.xml_value == xml_value), None)
+ if xml_value
+ else None
+ )
+
+ if member is None:
+ raise ValueError(f"{cls.__name__} has no XML mapping for {repr(xml_value)}")
+
+ return member
+
+ @classmethod
+ def to_xml(cls: Type[_T], value: int | _T) -> str:
+ """XML value of this enum member, generally an XML attribute value."""
+ # -- presence of multi-arg `__new__()` method fools type-checker, but getting a
+ # -- member by its value using EnumCls(val) works as usual.
+ member = cls(value)
+ xml_value = member.xml_value
+ if not xml_value:
+ raise ValueError(f"{cls.__name__}.{member.name} has no XML representation")
+ return xml_value
+
+ @classmethod
+ def validate(cls: Type[_T], value: _T):
+ """Raise |ValueError| if `value` is not an assignable value."""
+ if value not in cls:
+ raise ValueError(f"{value} not a member of {cls.__name__} enumeration")
+
+
+class DocsPageFormatter(object):
+ """Formats a reStructuredText documention page (string) for an enumeration."""
+
+ def __init__(self, clsname: str, clsdict: dict[str, Any]):
+ self._clsname = clsname
+ self._clsdict = clsdict
+
+ @property
+ def page_str(self):
+ """
+ The RestructuredText documentation page for the enumeration. This is
+ the only API member for the class.
+ """
+ tmpl = ".. _%s:\n\n%s\n\n%s\n\n----\n\n%s"
+ components = (
+ self._ms_name,
+ self._page_title,
+ self._intro_text,
+ self._member_defs,
+ )
+ return tmpl % components
+
+ @property
+ def _intro_text(self):
+ """
+ The docstring of the enumeration, formatted for use at the top of the
+ documentation page
+ """
+ try:
+ cls_docstring = self._clsdict["__doc__"]
+ except KeyError:
+ cls_docstring = ""
+
+ if cls_docstring is None:
+ return ""
+
+ return textwrap.dedent(cls_docstring).strip()
+
+ def _member_def(self, member: BaseEnum | BaseXmlEnum):
+ """Return an individual member definition formatted as an RST glossary entry.
+
+ Output is wrapped to fit within 78 columns.
+ """
+ member_docstring = textwrap.dedent(member.__doc__ or "").strip()
+ member_docstring = textwrap.fill(
+ member_docstring,
+ width=78,
+ initial_indent=" " * 4,
+ subsequent_indent=" " * 4,
+ )
+ return "%s\n%s\n" % (member.name, member_docstring)
+
+ @property
+ def _member_defs(self):
+ """
+ A single string containing the aggregated member definitions section
+ of the documentation page
+ """
+ members = self._clsdict["__members__"]
+ member_defs = [self._member_def(member) for member in members if member.name is not None]
+ return "\n".join(member_defs)
+
+ @property
+ def _ms_name(self):
+ """
+ The Microsoft API name for this enumeration
+ """
+ return self._clsdict["__ms_name__"]
+
+ @property
+ def _page_title(self):
+ """
+ The title for the documentation page, formatted as code (surrounded
+ in double-backtics) and underlined with '=' characters
+ """
+ title_underscore = "=" * (len(self._clsname) + 4)
+ return "``%s``\n%s" % (self._clsname, title_underscore)
diff --git a/.venv/lib/python3.12/site-packages/pptx/enum/chart.py b/.venv/lib/python3.12/site-packages/pptx/enum/chart.py
new file mode 100644
index 00000000..2599cf4d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/enum/chart.py
@@ -0,0 +1,492 @@
+"""Enumerations used by charts and related objects."""
+
+from __future__ import annotations
+
+from pptx.enum.base import BaseEnum, BaseXmlEnum
+
+
+class XL_AXIS_CROSSES(BaseXmlEnum):
+ """Specifies the point on an axis where the other axis crosses.
+
+ Example::
+
+ from pptx.enum.chart import XL_AXIS_CROSSES
+
+ value_axis.crosses = XL_AXIS_CROSSES.MAXIMUM
+
+ MS API Name: `XlAxisCrosses`
+
+ https://msdn.microsoft.com/en-us/library/office/ff745402.aspx
+ """
+
+ AUTOMATIC = (-4105, "autoZero", "The axis crossing point is set automatically, often at zero.")
+ """The axis crossing point is set automatically, often at zero."""
+
+ CUSTOM = (-4114, "", "The .crosses_at property specifies the axis crossing point.")
+ """The .crosses_at property specifies the axis crossing point."""
+
+ MAXIMUM = (2, "max", "The axis crosses at the maximum value.")
+ """The axis crosses at the maximum value."""
+
+ MINIMUM = (4, "min", "The axis crosses at the minimum value.")
+ """The axis crosses at the minimum value."""
+
+
+class XL_CATEGORY_TYPE(BaseEnum):
+ """Specifies the type of the category axis.
+
+ Example::
+
+ from pptx.enum.chart import XL_CATEGORY_TYPE
+
+ date_axis = chart.category_axis
+ assert date_axis.category_type == XL_CATEGORY_TYPE.TIME_SCALE
+
+ MS API Name: `XlCategoryType`
+
+ https://msdn.microsoft.com/EN-US/library/office/ff746136.aspx
+ """
+
+ AUTOMATIC_SCALE = (-4105, "The application controls the axis type.")
+ """The application controls the axis type."""
+
+ CATEGORY_SCALE = (2, "Axis groups data by an arbitrary set of categories")
+ """Axis groups data by an arbitrary set of categories"""
+
+ TIME_SCALE = (3, "Axis groups data on a time scale of days, months, or years.")
+ """Axis groups data on a time scale of days, months, or years."""
+
+
+class XL_CHART_TYPE(BaseEnum):
+ """Specifies the type of a chart.
+
+ Example::
+
+ from pptx.enum.chart import XL_CHART_TYPE
+
+ assert chart.chart_type == XL_CHART_TYPE.BAR_STACKED
+
+ MS API Name: `XlChartType`
+
+ http://msdn.microsoft.com/en-us/library/office/ff838409.aspx
+ """
+
+ THREE_D_AREA = (-4098, "3D Area.")
+ """3D Area."""
+
+ THREE_D_AREA_STACKED = (78, "3D Stacked Area.")
+ """3D Stacked Area."""
+
+ THREE_D_AREA_STACKED_100 = (79, "100% Stacked Area.")
+ """100% Stacked Area."""
+
+ THREE_D_BAR_CLUSTERED = (60, "3D Clustered Bar.")
+ """3D Clustered Bar."""
+
+ THREE_D_BAR_STACKED = (61, "3D Stacked Bar.")
+ """3D Stacked Bar."""
+
+ THREE_D_BAR_STACKED_100 = (62, "3D 100% Stacked Bar.")
+ """3D 100% Stacked Bar."""
+
+ THREE_D_COLUMN = (-4100, "3D Column.")
+ """3D Column."""
+
+ THREE_D_COLUMN_CLUSTERED = (54, "3D Clustered Column.")
+ """3D Clustered Column."""
+
+ THREE_D_COLUMN_STACKED = (55, "3D Stacked Column.")
+ """3D Stacked Column."""
+
+ THREE_D_COLUMN_STACKED_100 = (56, "3D 100% Stacked Column.")
+ """3D 100% Stacked Column."""
+
+ THREE_D_LINE = (-4101, "3D Line.")
+ """3D Line."""
+
+ THREE_D_PIE = (-4102, "3D Pie.")
+ """3D Pie."""
+
+ THREE_D_PIE_EXPLODED = (70, "Exploded 3D Pie.")
+ """Exploded 3D Pie."""
+
+ AREA = (1, "Area")
+ """Area"""
+
+ AREA_STACKED = (76, "Stacked Area.")
+ """Stacked Area."""
+
+ AREA_STACKED_100 = (77, "100% Stacked Area.")
+ """100% Stacked Area."""
+
+ BAR_CLUSTERED = (57, "Clustered Bar.")
+ """Clustered Bar."""
+
+ BAR_OF_PIE = (71, "Bar of Pie.")
+ """Bar of Pie."""
+
+ BAR_STACKED = (58, "Stacked Bar.")
+ """Stacked Bar."""
+
+ BAR_STACKED_100 = (59, "100% Stacked Bar.")
+ """100% Stacked Bar."""
+
+ BUBBLE = (15, "Bubble.")
+ """Bubble."""
+
+ BUBBLE_THREE_D_EFFECT = (87, "Bubble with 3D effects.")
+ """Bubble with 3D effects."""
+
+ COLUMN_CLUSTERED = (51, "Clustered Column.")
+ """Clustered Column."""
+
+ COLUMN_STACKED = (52, "Stacked Column.")
+ """Stacked Column."""
+
+ COLUMN_STACKED_100 = (53, "100% Stacked Column.")
+ """100% Stacked Column."""
+
+ CONE_BAR_CLUSTERED = (102, "Clustered Cone Bar.")
+ """Clustered Cone Bar."""
+
+ CONE_BAR_STACKED = (103, "Stacked Cone Bar.")
+ """Stacked Cone Bar."""
+
+ CONE_BAR_STACKED_100 = (104, "100% Stacked Cone Bar.")
+ """100% Stacked Cone Bar."""
+
+ CONE_COL = (105, "3D Cone Column.")
+ """3D Cone Column."""
+
+ CONE_COL_CLUSTERED = (99, "Clustered Cone Column.")
+ """Clustered Cone Column."""
+
+ CONE_COL_STACKED = (100, "Stacked Cone Column.")
+ """Stacked Cone Column."""
+
+ CONE_COL_STACKED_100 = (101, "100% Stacked Cone Column.")
+ """100% Stacked Cone Column."""
+
+ CYLINDER_BAR_CLUSTERED = (95, "Clustered Cylinder Bar.")
+ """Clustered Cylinder Bar."""
+
+ CYLINDER_BAR_STACKED = (96, "Stacked Cylinder Bar.")
+ """Stacked Cylinder Bar."""
+
+ CYLINDER_BAR_STACKED_100 = (97, "100% Stacked Cylinder Bar.")
+ """100% Stacked Cylinder Bar."""
+
+ CYLINDER_COL = (98, "3D Cylinder Column.")
+ """3D Cylinder Column."""
+
+ CYLINDER_COL_CLUSTERED = (92, "Clustered Cone Column.")
+ """Clustered Cone Column."""
+
+ CYLINDER_COL_STACKED = (93, "Stacked Cone Column.")
+ """Stacked Cone Column."""
+
+ CYLINDER_COL_STACKED_100 = (94, "100% Stacked Cylinder Column.")
+ """100% Stacked Cylinder Column."""
+
+ DOUGHNUT = (-4120, "Doughnut.")
+ """Doughnut."""
+
+ DOUGHNUT_EXPLODED = (80, "Exploded Doughnut.")
+ """Exploded Doughnut."""
+
+ LINE = (4, "Line.")
+ """Line."""
+
+ LINE_MARKERS = (65, "Line with Markers.")
+ """Line with Markers."""
+
+ LINE_MARKERS_STACKED = (66, "Stacked Line with Markers.")
+ """Stacked Line with Markers."""
+
+ LINE_MARKERS_STACKED_100 = (67, "100% Stacked Line with Markers.")
+ """100% Stacked Line with Markers."""
+
+ LINE_STACKED = (63, "Stacked Line.")
+ """Stacked Line."""
+
+ LINE_STACKED_100 = (64, "100% Stacked Line.")
+ """100% Stacked Line."""
+
+ PIE = (5, "Pie.")
+ """Pie."""
+
+ PIE_EXPLODED = (69, "Exploded Pie.")
+ """Exploded Pie."""
+
+ PIE_OF_PIE = (68, "Pie of Pie.")
+ """Pie of Pie."""
+
+ PYRAMID_BAR_CLUSTERED = (109, "Clustered Pyramid Bar.")
+ """Clustered Pyramid Bar."""
+
+ PYRAMID_BAR_STACKED = (110, "Stacked Pyramid Bar.")
+ """Stacked Pyramid Bar."""
+
+ PYRAMID_BAR_STACKED_100 = (111, "100% Stacked Pyramid Bar.")
+ """100% Stacked Pyramid Bar."""
+
+ PYRAMID_COL = (112, "3D Pyramid Column.")
+ """3D Pyramid Column."""
+
+ PYRAMID_COL_CLUSTERED = (106, "Clustered Pyramid Column.")
+ """Clustered Pyramid Column."""
+
+ PYRAMID_COL_STACKED = (107, "Stacked Pyramid Column.")
+ """Stacked Pyramid Column."""
+
+ PYRAMID_COL_STACKED_100 = (108, "100% Stacked Pyramid Column.")
+ """100% Stacked Pyramid Column."""
+
+ RADAR = (-4151, "Radar.")
+ """Radar."""
+
+ RADAR_FILLED = (82, "Filled Radar.")
+ """Filled Radar."""
+
+ RADAR_MARKERS = (81, "Radar with Data Markers.")
+ """Radar with Data Markers."""
+
+ STOCK_HLC = (88, "High-Low-Close.")
+ """High-Low-Close."""
+
+ STOCK_OHLC = (89, "Open-High-Low-Close.")
+ """Open-High-Low-Close."""
+
+ STOCK_VHLC = (90, "Volume-High-Low-Close.")
+ """Volume-High-Low-Close."""
+
+ STOCK_VOHLC = (91, "Volume-Open-High-Low-Close.")
+ """Volume-Open-High-Low-Close."""
+
+ SURFACE = (83, "3D Surface.")
+ """3D Surface."""
+
+ SURFACE_TOP_VIEW = (85, "Surface (Top View).")
+ """Surface (Top View)."""
+
+ SURFACE_TOP_VIEW_WIREFRAME = (86, "Surface (Top View wireframe).")
+ """Surface (Top View wireframe)."""
+
+ SURFACE_WIREFRAME = (84, "3D Surface (wireframe).")
+ """3D Surface (wireframe)."""
+
+ XY_SCATTER = (-4169, "Scatter.")
+ """Scatter."""
+
+ XY_SCATTER_LINES = (74, "Scatter with Lines.")
+ """Scatter with Lines."""
+
+ XY_SCATTER_LINES_NO_MARKERS = (75, "Scatter with Lines and No Data Markers.")
+ """Scatter with Lines and No Data Markers."""
+
+ XY_SCATTER_SMOOTH = (72, "Scatter with Smoothed Lines.")
+ """Scatter with Smoothed Lines."""
+
+ XY_SCATTER_SMOOTH_NO_MARKERS = (73, "Scatter with Smoothed Lines and No Data Markers.")
+ """Scatter with Smoothed Lines and No Data Markers."""
+
+
+class XL_DATA_LABEL_POSITION(BaseXmlEnum):
+ """Specifies where the data label is positioned.
+
+ Example::
+
+ from pptx.enum.chart import XL_LABEL_POSITION
+
+ data_labels = chart.plots[0].data_labels
+ data_labels.position = XL_LABEL_POSITION.OUTSIDE_END
+
+ MS API Name: `XlDataLabelPosition`
+
+ http://msdn.microsoft.com/en-us/library/office/ff745082.aspx
+ """
+
+ ABOVE = (0, "t", "The data label is positioned above the data point.")
+ """The data label is positioned above the data point."""
+
+ BELOW = (1, "b", "The data label is positioned below the data point.")
+ """The data label is positioned below the data point."""
+
+ BEST_FIT = (5, "bestFit", "Word sets the position of the data label.")
+ """Word sets the position of the data label."""
+
+ CENTER = (
+ -4108,
+ "ctr",
+ "The data label is centered on the data point or inside a bar or a pie slice.",
+ )
+ """The data label is centered on the data point or inside a bar or a pie slice."""
+
+ INSIDE_BASE = (
+ 4,
+ "inBase",
+ "The data label is positioned inside the data point at the bottom edge.",
+ )
+ """The data label is positioned inside the data point at the bottom edge."""
+
+ INSIDE_END = (3, "inEnd", "The data label is positioned inside the data point at the top edge.")
+ """The data label is positioned inside the data point at the top edge."""
+
+ LEFT = (-4131, "l", "The data label is positioned to the left of the data point.")
+ """The data label is positioned to the left of the data point."""
+
+ MIXED = (6, "", "Data labels are in multiple positions (read-only).")
+ """Data labels are in multiple positions (read-only)."""
+
+ OUTSIDE_END = (
+ 2,
+ "outEnd",
+ "The data label is positioned outside the data point at the top edge.",
+ )
+ """The data label is positioned outside the data point at the top edge."""
+
+ RIGHT = (-4152, "r", "The data label is positioned to the right of the data point.")
+ """The data label is positioned to the right of the data point."""
+
+
+XL_LABEL_POSITION = XL_DATA_LABEL_POSITION
+
+
+class XL_LEGEND_POSITION(BaseXmlEnum):
+ """Specifies the position of the legend on a chart.
+
+ Example::
+
+ from pptx.enum.chart import XL_LEGEND_POSITION
+
+ chart.has_legend = True
+ chart.legend.position = XL_LEGEND_POSITION.BOTTOM
+
+ MS API Name: `XlLegendPosition`
+
+ http://msdn.microsoft.com/en-us/library/office/ff745840.aspx
+ """
+
+ BOTTOM = (-4107, "b", "Below the chart.")
+ """Below the chart."""
+
+ CORNER = (2, "tr", "In the upper-right corner of the chart border.")
+ """In the upper-right corner of the chart border."""
+
+ CUSTOM = (-4161, "", "A custom position (read-only).")
+ """A custom position (read-only)."""
+
+ LEFT = (-4131, "l", "Left of the chart.")
+ """Left of the chart."""
+
+ RIGHT = (-4152, "r", "Right of the chart.")
+ """Right of the chart."""
+
+ TOP = (-4160, "t", "Above the chart.")
+ """Above the chart."""
+
+
+class XL_MARKER_STYLE(BaseXmlEnum):
+ """Specifies the marker style for a point or series in a line, scatter, or radar chart.
+
+ Example::
+
+ from pptx.enum.chart import XL_MARKER_STYLE
+
+ series.marker.style = XL_MARKER_STYLE.CIRCLE
+
+ MS API Name: `XlMarkerStyle`
+
+ http://msdn.microsoft.com/en-us/library/office/ff197219.aspx
+ """
+
+ AUTOMATIC = (-4105, "auto", "Automatic markers")
+ """Automatic markers"""
+
+ CIRCLE = (8, "circle", "Circular markers")
+ """Circular markers"""
+
+ DASH = (-4115, "dash", "Long bar markers")
+ """Long bar markers"""
+
+ DIAMOND = (2, "diamond", "Diamond-shaped markers")
+ """Diamond-shaped markers"""
+
+ DOT = (-4118, "dot", "Short bar markers")
+ """Short bar markers"""
+
+ NONE = (-4142, "none", "No markers")
+ """No markers"""
+
+ PICTURE = (-4147, "picture", "Picture markers")
+ """Picture markers"""
+
+ PLUS = (9, "plus", "Square markers with a plus sign")
+ """Square markers with a plus sign"""
+
+ SQUARE = (1, "square", "Square markers")
+ """Square markers"""
+
+ STAR = (5, "star", "Square markers with an asterisk")
+ """Square markers with an asterisk"""
+
+ TRIANGLE = (3, "triangle", "Triangular markers")
+ """Triangular markers"""
+
+ X = (-4168, "x", "Square markers with an X")
+ """Square markers with an X"""
+
+
+class XL_TICK_MARK(BaseXmlEnum):
+ """Specifies a type of axis tick for a chart.
+
+ Example::
+
+ from pptx.enum.chart import XL_TICK_MARK
+
+ chart.value_axis.minor_tick_mark = XL_TICK_MARK.INSIDE
+
+ MS API Name: `XlTickMark`
+
+ http://msdn.microsoft.com/en-us/library/office/ff193878.aspx
+ """
+
+ CROSS = (4, "cross", "Tick mark crosses the axis")
+ """Tick mark crosses the axis"""
+
+ INSIDE = (2, "in", "Tick mark appears inside the axis")
+ """Tick mark appears inside the axis"""
+
+ NONE = (-4142, "none", "No tick mark")
+ """No tick mark"""
+
+ OUTSIDE = (3, "out", "Tick mark appears outside the axis")
+ """Tick mark appears outside the axis"""
+
+
+class XL_TICK_LABEL_POSITION(BaseXmlEnum):
+ """Specifies the position of tick-mark labels on a chart axis.
+
+ Example::
+
+ from pptx.enum.chart import XL_TICK_LABEL_POSITION
+
+ category_axis = chart.category_axis
+ category_axis.tick_label_position = XL_TICK_LABEL_POSITION.LOW
+
+ MS API Name: `XlTickLabelPosition`
+
+ http://msdn.microsoft.com/en-us/library/office/ff822561.aspx
+ """
+
+ HIGH = (-4127, "high", "Top or right side of the chart.")
+ """Top or right side of the chart."""
+
+ LOW = (-4134, "low", "Bottom or left side of the chart.")
+ """Bottom or left side of the chart."""
+
+ NEXT_TO_AXIS = (4, "nextTo", "Next to axis (where axis is not at either side of the chart).")
+ """Next to axis (where axis is not at either side of the chart)."""
+
+ NONE = (-4142, "none", "No tick labels.")
+ """No tick labels."""
diff --git a/.venv/lib/python3.12/site-packages/pptx/enum/dml.py b/.venv/lib/python3.12/site-packages/pptx/enum/dml.py
new file mode 100644
index 00000000..40d5c5cd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/enum/dml.py
@@ -0,0 +1,405 @@
+"""Enumerations used by DrawingML objects."""
+
+from __future__ import annotations
+
+from pptx.enum.base import BaseEnum, BaseXmlEnum
+
+
+class MSO_COLOR_TYPE(BaseEnum):
+ """
+ Specifies the color specification scheme
+
+ Example::
+
+ from pptx.enum.dml import MSO_COLOR_TYPE
+
+ assert shape.fill.fore_color.type == MSO_COLOR_TYPE.SCHEME
+
+ MS API Name: "MsoColorType"
+
+ http://msdn.microsoft.com/en-us/library/office/ff864912(v=office.15).aspx
+ """
+
+ RGB = (1, "Color is specified by an |RGBColor| value.")
+ """Color is specified by an |RGBColor| value."""
+
+ SCHEME = (2, "Color is one of the preset theme colors")
+ """Color is one of the preset theme colors"""
+
+ HSL = (101, "Color is specified using Hue, Saturation, and Luminosity values")
+ """Color is specified using Hue, Saturation, and Luminosity values"""
+
+ PRESET = (102, "Color is specified using a named built-in color")
+ """Color is specified using a named built-in color"""
+
+ SCRGB = (103, "Color is an scRGB color, a wide color gamut RGB color space")
+ """Color is an scRGB color, a wide color gamut RGB color space"""
+
+ SYSTEM = (
+ 104,
+ "Color is one specified by the operating system, such as the window background color.",
+ )
+ """Color is one specified by the operating system, such as the window background color."""
+
+
+class MSO_FILL_TYPE(BaseEnum):
+ """
+ Specifies the type of bitmap used for the fill of a shape.
+
+ Alias: ``MSO_FILL``
+
+ Example::
+
+ from pptx.enum.dml import MSO_FILL
+
+ assert shape.fill.type == MSO_FILL.SOLID
+
+ MS API Name: `MsoFillType`
+
+ http://msdn.microsoft.com/EN-US/library/office/ff861408.aspx
+ """
+
+ BACKGROUND = (
+ 5,
+ "The shape is transparent, such that whatever is behind the shape shows through."
+ " Often this is the slide background, but if a visible shape is behind, that will"
+ " show through.",
+ )
+ """The shape is transparent, such that whatever is behind the shape shows through.
+
+ Often this is the slide background, but if a visible shape is behind, that will show through.
+ """
+
+ GRADIENT = (3, "Shape is filled with a gradient")
+ """Shape is filled with a gradient"""
+
+ GROUP = (101, "Shape is part of a group and should inherit the fill properties of the group.")
+ """Shape is part of a group and should inherit the fill properties of the group."""
+
+ PATTERNED = (2, "Shape is filled with a pattern")
+ """Shape is filled with a pattern"""
+
+ PICTURE = (6, "Shape is filled with a bitmapped image")
+ """Shape is filled with a bitmapped image"""
+
+ SOLID = (1, "Shape is filled with a solid color")
+ """Shape is filled with a solid color"""
+
+ TEXTURED = (4, "Shape is filled with a texture")
+ """Shape is filled with a texture"""
+
+
+MSO_FILL = MSO_FILL_TYPE
+
+
+class MSO_LINE_DASH_STYLE(BaseXmlEnum):
+ """Specifies the dash style for a line.
+
+ Alias: ``MSO_LINE``
+
+ Example::
+
+ from pptx.enum.dml import MSO_LINE
+
+ shape.line.dash_style = MSO_LINE.DASH_DOT_DOT
+
+ MS API name: `MsoLineDashStyle`
+
+ https://learn.microsoft.com/en-us/office/vba/api/Office.MsoLineDashStyle
+ """
+
+ DASH = (4, "dash", "Line consists of dashes only.")
+ """Line consists of dashes only."""
+
+ DASH_DOT = (5, "dashDot", "Line is a dash-dot pattern.")
+ """Line is a dash-dot pattern."""
+
+ DASH_DOT_DOT = (6, "lgDashDotDot", "Line is a dash-dot-dot pattern.")
+ """Line is a dash-dot-dot pattern."""
+
+ LONG_DASH = (7, "lgDash", "Line consists of long dashes.")
+ """Line consists of long dashes."""
+
+ LONG_DASH_DOT = (8, "lgDashDot", "Line is a long dash-dot pattern.")
+ """Line is a long dash-dot pattern."""
+
+ ROUND_DOT = (3, "sysDot", "Line is made up of round dots.")
+ """Line is made up of round dots."""
+
+ SOLID = (1, "solid", "Line is solid.")
+ """Line is solid."""
+
+ SQUARE_DOT = (2, "sysDash", "Line is made up of square dots.")
+ """Line is made up of square dots."""
+
+ DASH_STYLE_MIXED = (-2, "", "Not supported.")
+ """Return value only, indicating more than one dash style applies."""
+
+
+MSO_LINE = MSO_LINE_DASH_STYLE
+
+
+class MSO_PATTERN_TYPE(BaseXmlEnum):
+ """Specifies the fill pattern used in a shape.
+
+ Alias: ``MSO_PATTERN``
+
+ Example::
+
+ from pptx.enum.dml import MSO_PATTERN
+
+ fill = shape.fill
+ fill.patterned()
+ fill.pattern = MSO_PATTERN.WAVE
+
+ MS API Name: `MsoPatternType`
+
+ https://learn.microsoft.com/en-us/office/vba/api/Office.MsoPatternType
+ """
+
+ CROSS = (51, "cross", "Cross")
+ """Cross"""
+
+ DARK_DOWNWARD_DIAGONAL = (15, "dkDnDiag", "Dark Downward Diagonal")
+ """Dark Downward Diagonal"""
+
+ DARK_HORIZONTAL = (13, "dkHorz", "Dark Horizontal")
+ """Dark Horizontal"""
+
+ DARK_UPWARD_DIAGONAL = (16, "dkUpDiag", "Dark Upward Diagonal")
+ """Dark Upward Diagonal"""
+
+ DARK_VERTICAL = (14, "dkVert", "Dark Vertical")
+ """Dark Vertical"""
+
+ DASHED_DOWNWARD_DIAGONAL = (28, "dashDnDiag", "Dashed Downward Diagonal")
+ """Dashed Downward Diagonal"""
+
+ DASHED_HORIZONTAL = (32, "dashHorz", "Dashed Horizontal")
+ """Dashed Horizontal"""
+
+ DASHED_UPWARD_DIAGONAL = (27, "dashUpDiag", "Dashed Upward Diagonal")
+ """Dashed Upward Diagonal"""
+
+ DASHED_VERTICAL = (31, "dashVert", "Dashed Vertical")
+ """Dashed Vertical"""
+
+ DIAGONAL_BRICK = (40, "diagBrick", "Diagonal Brick")
+ """Diagonal Brick"""
+
+ DIAGONAL_CROSS = (54, "diagCross", "Diagonal Cross")
+ """Diagonal Cross"""
+
+ DIVOT = (46, "divot", "Pattern Divot")
+ """Pattern Divot"""
+
+ DOTTED_DIAMOND = (24, "dotDmnd", "Dotted Diamond")
+ """Dotted Diamond"""
+
+ DOTTED_GRID = (45, "dotGrid", "Dotted Grid")
+ """Dotted Grid"""
+
+ DOWNWARD_DIAGONAL = (52, "dnDiag", "Downward Diagonal")
+ """Downward Diagonal"""
+
+ HORIZONTAL = (49, "horz", "Horizontal")
+ """Horizontal"""
+
+ HORIZONTAL_BRICK = (35, "horzBrick", "Horizontal Brick")
+ """Horizontal Brick"""
+
+ LARGE_CHECKER_BOARD = (36, "lgCheck", "Large Checker Board")
+ """Large Checker Board"""
+
+ LARGE_CONFETTI = (33, "lgConfetti", "Large Confetti")
+ """Large Confetti"""
+
+ LARGE_GRID = (34, "lgGrid", "Large Grid")
+ """Large Grid"""
+
+ LIGHT_DOWNWARD_DIAGONAL = (21, "ltDnDiag", "Light Downward Diagonal")
+ """Light Downward Diagonal"""
+
+ LIGHT_HORIZONTAL = (19, "ltHorz", "Light Horizontal")
+ """Light Horizontal"""
+
+ LIGHT_UPWARD_DIAGONAL = (22, "ltUpDiag", "Light Upward Diagonal")
+ """Light Upward Diagonal"""
+
+ LIGHT_VERTICAL = (20, "ltVert", "Light Vertical")
+ """Light Vertical"""
+
+ NARROW_HORIZONTAL = (30, "narHorz", "Narrow Horizontal")
+ """Narrow Horizontal"""
+
+ NARROW_VERTICAL = (29, "narVert", "Narrow Vertical")
+ """Narrow Vertical"""
+
+ OUTLINED_DIAMOND = (41, "openDmnd", "Outlined Diamond")
+ """Outlined Diamond"""
+
+ PERCENT_10 = (2, "pct10", "10% of the foreground color.")
+ """10% of the foreground color."""
+
+ PERCENT_20 = (3, "pct20", "20% of the foreground color.")
+ """20% of the foreground color."""
+
+ PERCENT_25 = (4, "pct25", "25% of the foreground color.")
+ """25% of the foreground color."""
+
+ PERCENT_30 = (5, "pct30", "30% of the foreground color.")
+ """30% of the foreground color."""
+
+ ERCENT_40 = (6, "pct40", "40% of the foreground color.")
+ """40% of the foreground color."""
+
+ PERCENT_5 = (1, "pct5", "5% of the foreground color.")
+ """5% of the foreground color."""
+
+ PERCENT_50 = (7, "pct50", "50% of the foreground color.")
+ """50% of the foreground color."""
+
+ PERCENT_60 = (8, "pct60", "60% of the foreground color.")
+ """60% of the foreground color."""
+
+ PERCENT_70 = (9, "pct70", "70% of the foreground color.")
+ """70% of the foreground color."""
+
+ PERCENT_75 = (10, "pct75", "75% of the foreground color.")
+ """75% of the foreground color."""
+
+ PERCENT_80 = (11, "pct80", "80% of the foreground color.")
+ """80% of the foreground color."""
+
+ PERCENT_90 = (12, "pct90", "90% of the foreground color.")
+ """90% of the foreground color."""
+
+ PLAID = (42, "plaid", "Plaid")
+ """Plaid"""
+
+ SHINGLE = (47, "shingle", "Shingle")
+ """Shingle"""
+
+ SMALL_CHECKER_BOARD = (17, "smCheck", "Small Checker Board")
+ """Small Checker Board"""
+
+ SMALL_CONFETTI = (37, "smConfetti", "Small Confetti")
+ """Small Confetti"""
+
+ SMALL_GRID = (23, "smGrid", "Small Grid")
+ """Small Grid"""
+
+ SOLID_DIAMOND = (39, "solidDmnd", "Solid Diamond")
+ """Solid Diamond"""
+
+ SPHERE = (43, "sphere", "Sphere")
+ """Sphere"""
+
+ TRELLIS = (18, "trellis", "Trellis")
+ """Trellis"""
+
+ UPWARD_DIAGONAL = (53, "upDiag", "Upward Diagonal")
+ """Upward Diagonal"""
+
+ VERTICAL = (50, "vert", "Vertical")
+ """Vertical"""
+
+ WAVE = (48, "wave", "Wave")
+ """Wave"""
+
+ WEAVE = (44, "weave", "Weave")
+ """Weave"""
+
+ WIDE_DOWNWARD_DIAGONAL = (25, "wdDnDiag", "Wide Downward Diagonal")
+ """Wide Downward Diagonal"""
+
+ WIDE_UPWARD_DIAGONAL = (26, "wdUpDiag", "Wide Upward Diagonal")
+ """Wide Upward Diagonal"""
+
+ ZIG_ZAG = (38, "zigZag", "Zig Zag")
+ """Zig Zag"""
+
+ MIXED = (-2, "", "Mixed pattern (read-only).")
+ """Mixed pattern (read-only)."""
+
+
+MSO_PATTERN = MSO_PATTERN_TYPE
+
+
+class MSO_THEME_COLOR_INDEX(BaseXmlEnum):
+ """An Office theme color, one of those shown in the color gallery on the formatting ribbon.
+
+ Alias: ``MSO_THEME_COLOR``
+
+ Example::
+
+ from pptx.enum.dml import MSO_THEME_COLOR
+
+ shape.fill.solid()
+ shape.fill.fore_color.theme_color = MSO_THEME_COLOR.ACCENT_1
+
+ MS API Name: `MsoThemeColorIndex`
+
+ http://msdn.microsoft.com/en-us/library/office/ff860782(v=office.15).aspx
+ """
+
+ NOT_THEME_COLOR = (0, "", "Indicates the color is not a theme color.")
+ """Indicates the color is not a theme color."""
+
+ ACCENT_1 = (5, "accent1", "Specifies the Accent 1 theme color.")
+ """Specifies the Accent 1 theme color."""
+
+ ACCENT_2 = (6, "accent2", "Specifies the Accent 2 theme color.")
+ """Specifies the Accent 2 theme color."""
+
+ ACCENT_3 = (7, "accent3", "Specifies the Accent 3 theme color.")
+ """Specifies the Accent 3 theme color."""
+
+ ACCENT_4 = (8, "accent4", "Specifies the Accent 4 theme color.")
+ """Specifies the Accent 4 theme color."""
+
+ ACCENT_5 = (9, "accent5", "Specifies the Accent 5 theme color.")
+ """Specifies the Accent 5 theme color."""
+
+ ACCENT_6 = (10, "accent6", "Specifies the Accent 6 theme color.")
+ """Specifies the Accent 6 theme color."""
+
+ BACKGROUND_1 = (14, "bg1", "Specifies the Background 1 theme color.")
+ """Specifies the Background 1 theme color."""
+
+ BACKGROUND_2 = (16, "bg2", "Specifies the Background 2 theme color.")
+ """Specifies the Background 2 theme color."""
+
+ DARK_1 = (1, "dk1", "Specifies the Dark 1 theme color.")
+ """Specifies the Dark 1 theme color."""
+
+ DARK_2 = (3, "dk2", "Specifies the Dark 2 theme color.")
+ """Specifies the Dark 2 theme color."""
+
+ FOLLOWED_HYPERLINK = (12, "folHlink", "Specifies the theme color for a clicked hyperlink.")
+ """Specifies the theme color for a clicked hyperlink."""
+
+ HYPERLINK = (11, "hlink", "Specifies the theme color for a hyperlink.")
+ """Specifies the theme color for a hyperlink."""
+
+ LIGHT_1 = (2, "lt1", "Specifies the Light 1 theme color.")
+ """Specifies the Light 1 theme color."""
+
+ LIGHT_2 = (4, "lt2", "Specifies the Light 2 theme color.")
+ """Specifies the Light 2 theme color."""
+
+ TEXT_1 = (13, "tx1", "Specifies the Text 1 theme color.")
+ """Specifies the Text 1 theme color."""
+
+ TEXT_2 = (15, "tx2", "Specifies the Text 2 theme color.")
+ """Specifies the Text 2 theme color."""
+
+ MIXED = (
+ -2,
+ "",
+ "Indicates multiple theme colors are used, such as in a group shape (read-only).",
+ )
+ """Indicates multiple theme colors are used, such as in a group shape (read-only)."""
+
+
+MSO_THEME_COLOR = MSO_THEME_COLOR_INDEX
diff --git a/.venv/lib/python3.12/site-packages/pptx/enum/lang.py b/.venv/lib/python3.12/site-packages/pptx/enum/lang.py
new file mode 100644
index 00000000..a6bc1c8b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/enum/lang.py
@@ -0,0 +1,685 @@
+"""Enumerations used for specifying language."""
+
+from __future__ import annotations
+
+from pptx.enum.base import BaseXmlEnum
+
+
+class MSO_LANGUAGE_ID(BaseXmlEnum):
+ """
+ Specifies the language identifier.
+
+ Example::
+
+ from pptx.enum.lang import MSO_LANGUAGE_ID
+
+ font.language_id = MSO_LANGUAGE_ID.POLISH
+
+ MS API Name: `MsoLanguageId`
+
+ https://msdn.microsoft.com/en-us/library/office/ff862134.aspx
+ """
+
+ NONE = (0, "", "No language specified.")
+ """No language specified."""
+
+ AFRIKAANS = (1078, "af-ZA", "The Afrikaans language.")
+ """The Afrikaans language."""
+
+ ALBANIAN = (1052, "sq-AL", "The Albanian language.")
+ """The Albanian language."""
+
+ AMHARIC = (1118, "am-ET", "The Amharic language.")
+ """The Amharic language."""
+
+ ARABIC = (1025, "ar-SA", "The Arabic language.")
+ """The Arabic language."""
+
+ ARABIC_ALGERIA = (5121, "ar-DZ", "The Arabic Algeria language.")
+ """The Arabic Algeria language."""
+
+ ARABIC_BAHRAIN = (15361, "ar-BH", "The Arabic Bahrain language.")
+ """The Arabic Bahrain language."""
+
+ ARABIC_EGYPT = (3073, "ar-EG", "The Arabic Egypt language.")
+ """The Arabic Egypt language."""
+
+ ARABIC_IRAQ = (2049, "ar-IQ", "The Arabic Iraq language.")
+ """The Arabic Iraq language."""
+
+ ARABIC_JORDAN = (11265, "ar-JO", "The Arabic Jordan language.")
+ """The Arabic Jordan language."""
+
+ ARABIC_KUWAIT = (13313, "ar-KW", "The Arabic Kuwait language.")
+ """The Arabic Kuwait language."""
+
+ ARABIC_LEBANON = (12289, "ar-LB", "The Arabic Lebanon language.")
+ """The Arabic Lebanon language."""
+
+ ARABIC_LIBYA = (4097, "ar-LY", "The Arabic Libya language.")
+ """The Arabic Libya language."""
+
+ ARABIC_MOROCCO = (6145, "ar-MA", "The Arabic Morocco language.")
+ """The Arabic Morocco language."""
+
+ ARABIC_OMAN = (8193, "ar-OM", "The Arabic Oman language.")
+ """The Arabic Oman language."""
+
+ ARABIC_QATAR = (16385, "ar-QA", "The Arabic Qatar language.")
+ """The Arabic Qatar language."""
+
+ ARABIC_SYRIA = (10241, "ar-SY", "The Arabic Syria language.")
+ """The Arabic Syria language."""
+
+ ARABIC_TUNISIA = (7169, "ar-TN", "The Arabic Tunisia language.")
+ """The Arabic Tunisia language."""
+
+ ARABIC_UAE = (14337, "ar-AE", "The Arabic UAE language.")
+ """The Arabic UAE language."""
+
+ ARABIC_YEMEN = (9217, "ar-YE", "The Arabic Yemen language.")
+ """The Arabic Yemen language."""
+
+ ARMENIAN = (1067, "hy-AM", "The Armenian language.")
+ """The Armenian language."""
+
+ ASSAMESE = (1101, "as-IN", "The Assamese language.")
+ """The Assamese language."""
+
+ AZERI_CYRILLIC = (2092, "az-AZ", "The Azeri Cyrillic language.")
+ """The Azeri Cyrillic language."""
+
+ AZERI_LATIN = (1068, "az-Latn-AZ", "The Azeri Latin language.")
+ """The Azeri Latin language."""
+
+ BASQUE = (1069, "eu-ES", "The Basque language.")
+ """The Basque language."""
+
+ BELGIAN_DUTCH = (2067, "nl-BE", "The Belgian Dutch language.")
+ """The Belgian Dutch language."""
+
+ BELGIAN_FRENCH = (2060, "fr-BE", "The Belgian French language.")
+ """The Belgian French language."""
+
+ BENGALI = (1093, "bn-IN", "The Bengali language.")
+ """The Bengali language."""
+
+ BOSNIAN = (4122, "hr-BA", "The Bosnian language.")
+ """The Bosnian language."""
+
+ BOSNIAN_BOSNIA_HERZEGOVINA_CYRILLIC = (
+ 8218,
+ "bs-BA",
+ "The Bosnian Bosnia Herzegovina Cyrillic language.",
+ )
+ """The Bosnian Bosnia Herzegovina Cyrillic language."""
+
+ BOSNIAN_BOSNIA_HERZEGOVINA_LATIN = (
+ 5146,
+ "bs-Latn-BA",
+ "The Bosnian Bosnia Herzegovina Latin language.",
+ )
+ """The Bosnian Bosnia Herzegovina Latin language."""
+
+ BRAZILIAN_PORTUGUESE = (1046, "pt-BR", "The Brazilian Portuguese language.")
+ """The Brazilian Portuguese language."""
+
+ BULGARIAN = (1026, "bg-BG", "The Bulgarian language.")
+ """The Bulgarian language."""
+
+ BURMESE = (1109, "my-MM", "The Burmese language.")
+ """The Burmese language."""
+
+ BYELORUSSIAN = (1059, "be-BY", "The Byelorussian language.")
+ """The Byelorussian language."""
+
+ CATALAN = (1027, "ca-ES", "The Catalan language.")
+ """The Catalan language."""
+
+ CHEROKEE = (1116, "chr-US", "The Cherokee language.")
+ """The Cherokee language."""
+
+ CHINESE_HONG_KONG_SAR = (3076, "zh-HK", "The Chinese Hong Kong SAR language.")
+ """The Chinese Hong Kong SAR language."""
+
+ CHINESE_MACAO_SAR = (5124, "zh-MO", "The Chinese Macao SAR language.")
+ """The Chinese Macao SAR language."""
+
+ CHINESE_SINGAPORE = (4100, "zh-SG", "The Chinese Singapore language.")
+ """The Chinese Singapore language."""
+
+ CROATIAN = (1050, "hr-HR", "The Croatian language.")
+ """The Croatian language."""
+
+ CZECH = (1029, "cs-CZ", "The Czech language.")
+ """The Czech language."""
+
+ DANISH = (1030, "da-DK", "The Danish language.")
+ """The Danish language."""
+
+ DIVEHI = (1125, "div-MV", "The Divehi language.")
+ """The Divehi language."""
+
+ DUTCH = (1043, "nl-NL", "The Dutch language.")
+ """The Dutch language."""
+
+ EDO = (1126, "bin-NG", "The Edo language.")
+ """The Edo language."""
+
+ ENGLISH_AUS = (3081, "en-AU", "The English AUS language.")
+ """The English AUS language."""
+
+ ENGLISH_BELIZE = (10249, "en-BZ", "The English Belize language.")
+ """The English Belize language."""
+
+ ENGLISH_CANADIAN = (4105, "en-CA", "The English Canadian language.")
+ """The English Canadian language."""
+
+ ENGLISH_CARIBBEAN = (9225, "en-CB", "The English Caribbean language.")
+ """The English Caribbean language."""
+
+ ENGLISH_INDONESIA = (14345, "en-ID", "The English Indonesia language.")
+ """The English Indonesia language."""
+
+ ENGLISH_IRELAND = (6153, "en-IE", "The English Ireland language.")
+ """The English Ireland language."""
+
+ ENGLISH_JAMAICA = (8201, "en-JA", "The English Jamaica language.")
+ """The English Jamaica language."""
+
+ ENGLISH_NEW_ZEALAND = (5129, "en-NZ", "The English NewZealand language.")
+ """The English NewZealand language."""
+
+ ENGLISH_PHILIPPINES = (13321, "en-PH", "The English Philippines language.")
+ """The English Philippines language."""
+
+ ENGLISH_SOUTH_AFRICA = (7177, "en-ZA", "The English South Africa language.")
+ """The English South Africa language."""
+
+ ENGLISH_TRINIDAD_TOBAGO = (11273, "en-TT", "The English Trinidad Tobago language.")
+ """The English Trinidad Tobago language."""
+
+ ENGLISH_UK = (2057, "en-GB", "The English UK language.")
+ """The English UK language."""
+
+ ENGLISH_US = (1033, "en-US", "The English US language.")
+ """The English US language."""
+
+ ENGLISH_ZIMBABWE = (12297, "en-ZW", "The English Zimbabwe language.")
+ """The English Zimbabwe language."""
+
+ ESTONIAN = (1061, "et-EE", "The Estonian language.")
+ """The Estonian language."""
+
+ FAEROESE = (1080, "fo-FO", "The Faeroese language.")
+ """The Faeroese language."""
+
+ FARSI = (1065, "fa-IR", "The Farsi language.")
+ """The Farsi language."""
+
+ FILIPINO = (1124, "fil-PH", "The Filipino language.")
+ """The Filipino language."""
+
+ FINNISH = (1035, "fi-FI", "The Finnish language.")
+ """The Finnish language."""
+
+ FRANCH_CONGO_DRC = (9228, "fr-CD", "The French Congo DRC language.")
+ """The French Congo DRC language."""
+
+ FRENCH = (1036, "fr-FR", "The French language.")
+ """The French language."""
+
+ FRENCH_CAMEROON = (11276, "fr-CM", "The French Cameroon language.")
+ """The French Cameroon language."""
+
+ FRENCH_CANADIAN = (3084, "fr-CA", "The French Canadian language.")
+ """The French Canadian language."""
+
+ FRENCH_COTED_IVOIRE = (12300, "fr-CI", "The French Coted Ivoire language.")
+ """The French Coted Ivoire language."""
+
+ FRENCH_HAITI = (15372, "fr-HT", "The French Haiti language.")
+ """The French Haiti language."""
+
+ FRENCH_LUXEMBOURG = (5132, "fr-LU", "The French Luxembourg language.")
+ """The French Luxembourg language."""
+
+ FRENCH_MALI = (13324, "fr-ML", "The French Mali language.")
+ """The French Mali language."""
+
+ FRENCH_MONACO = (6156, "fr-MC", "The French Monaco language.")
+ """The French Monaco language."""
+
+ FRENCH_MOROCCO = (14348, "fr-MA", "The French Morocco language.")
+ """The French Morocco language."""
+
+ FRENCH_REUNION = (8204, "fr-RE", "The French Reunion language.")
+ """The French Reunion language."""
+
+ FRENCH_SENEGAL = (10252, "fr-SN", "The French Senegal language.")
+ """The French Senegal language."""
+
+ FRENCH_WEST_INDIES = (7180, "fr-WINDIES", "The French West Indies language.")
+ """The French West Indies language."""
+
+ FRISIAN_NETHERLANDS = (1122, "fy-NL", "The Frisian Netherlands language.")
+ """The Frisian Netherlands language."""
+
+ FULFULDE = (1127, "ff-NG", "The Fulfulde language.")
+ """The Fulfulde language."""
+
+ GAELIC_IRELAND = (2108, "ga-IE", "The Gaelic Ireland language.")
+ """The Gaelic Ireland language."""
+
+ GAELIC_SCOTLAND = (1084, "en-US", "The Gaelic Scotland language.")
+ """The Gaelic Scotland language."""
+
+ GALICIAN = (1110, "gl-ES", "The Galician language.")
+ """The Galician language."""
+
+ GEORGIAN = (1079, "ka-GE", "The Georgian language.")
+ """The Georgian language."""
+
+ GERMAN = (1031, "de-DE", "The German language.")
+ """The German language."""
+
+ GERMAN_AUSTRIA = (3079, "de-AT", "The German Austria language.")
+ """The German Austria language."""
+
+ GERMAN_LIECHTENSTEIN = (5127, "de-LI", "The German Liechtenstein language.")
+ """The German Liechtenstein language."""
+
+ GERMAN_LUXEMBOURG = (4103, "de-LU", "The German Luxembourg language.")
+ """The German Luxembourg language."""
+
+ GREEK = (1032, "el-GR", "The Greek language.")
+ """The Greek language."""
+
+ GUARANI = (1140, "gn-PY", "The Guarani language.")
+ """The Guarani language."""
+
+ GUJARATI = (1095, "gu-IN", "The Gujarati language.")
+ """The Gujarati language."""
+
+ HAUSA = (1128, "ha-NG", "The Hausa language.")
+ """The Hausa language."""
+
+ HAWAIIAN = (1141, "haw-US", "The Hawaiian language.")
+ """The Hawaiian language."""
+
+ HEBREW = (1037, "he-IL", "The Hebrew language.")
+ """The Hebrew language."""
+
+ HINDI = (1081, "hi-IN", "The Hindi language.")
+ """The Hindi language."""
+
+ HUNGARIAN = (1038, "hu-HU", "The Hungarian language.")
+ """The Hungarian language."""
+
+ IBIBIO = (1129, "ibb-NG", "The Ibibio language.")
+ """The Ibibio language."""
+
+ ICELANDIC = (1039, "is-IS", "The Icelandic language.")
+ """The Icelandic language."""
+
+ IGBO = (1136, "ig-NG", "The Igbo language.")
+ """The Igbo language."""
+
+ INDONESIAN = (1057, "id-ID", "The Indonesian language.")
+ """The Indonesian language."""
+
+ INUKTITUT = (1117, "iu-Cans-CA", "The Inuktitut language.")
+ """The Inuktitut language."""
+
+ ITALIAN = (1040, "it-IT", "The Italian language.")
+ """The Italian language."""
+
+ JAPANESE = (1041, "ja-JP", "The Japanese language.")
+ """The Japanese language."""
+
+ KANNADA = (1099, "kn-IN", "The Kannada language.")
+ """The Kannada language."""
+
+ KANURI = (1137, "kr-NG", "The Kanuri language.")
+ """The Kanuri language."""
+
+ KASHMIRI = (1120, "ks-Arab", "The Kashmiri language.")
+ """The Kashmiri language."""
+
+ KASHMIRI_DEVANAGARI = (2144, "ks-Deva", "The Kashmiri Devanagari language.")
+ """The Kashmiri Devanagari language."""
+
+ KAZAKH = (1087, "kk-KZ", "The Kazakh language.")
+ """The Kazakh language."""
+
+ KHMER = (1107, "kh-KH", "The Khmer language.")
+ """The Khmer language."""
+
+ KIRGHIZ = (1088, "ky-KG", "The Kirghiz language.")
+ """The Kirghiz language."""
+
+ KONKANI = (1111, "kok-IN", "The Konkani language.")
+ """The Konkani language."""
+
+ KOREAN = (1042, "ko-KR", "The Korean language.")
+ """The Korean language."""
+
+ KYRGYZ = (1088, "ky-KG", "The Kyrgyz language.")
+ """The Kyrgyz language."""
+
+ LAO = (1108, "lo-LA", "The Lao language.")
+ """The Lao language."""
+
+ LATIN = (1142, "la-Latn", "The Latin language.")
+ """The Latin language."""
+
+ LATVIAN = (1062, "lv-LV", "The Latvian language.")
+ """The Latvian language."""
+
+ LITHUANIAN = (1063, "lt-LT", "The Lithuanian language.")
+ """The Lithuanian language."""
+
+ MACEDONINAN_FYROM = (1071, "mk-MK", "The Macedonian FYROM language.")
+ """The Macedonian FYROM language."""
+
+ MALAY_BRUNEI_DARUSSALAM = (2110, "ms-BN", "The Malay Brunei Darussalam language.")
+ """The Malay Brunei Darussalam language."""
+
+ MALAYALAM = (1100, "ml-IN", "The Malayalam language.")
+ """The Malayalam language."""
+
+ MALAYSIAN = (1086, "ms-MY", "The Malaysian language.")
+ """The Malaysian language."""
+
+ MALTESE = (1082, "mt-MT", "The Maltese language.")
+ """The Maltese language."""
+
+ MANIPURI = (1112, "mni-IN", "The Manipuri language.")
+ """The Manipuri language."""
+
+ MAORI = (1153, "mi-NZ", "The Maori language.")
+ """The Maori language."""
+
+ MARATHI = (1102, "mr-IN", "The Marathi language.")
+ """The Marathi language."""
+
+ MEXICAN_SPANISH = (2058, "es-MX", "The Mexican Spanish language.")
+ """The Mexican Spanish language."""
+
+ MONGOLIAN = (1104, "mn-MN", "The Mongolian language.")
+ """The Mongolian language."""
+
+ NEPALI = (1121, "ne-NP", "The Nepali language.")
+ """The Nepali language."""
+
+ NO_PROOFING = (1024, "en-US", "No proofing.")
+ """No proofing."""
+
+ NORWEGIAN_BOKMOL = (1044, "nb-NO", "The Norwegian Bokmol language.")
+ """The Norwegian Bokmol language."""
+
+ NORWEGIAN_NYNORSK = (2068, "nn-NO", "The Norwegian Nynorsk language.")
+ """The Norwegian Nynorsk language."""
+
+ ORIYA = (1096, "or-IN", "The Oriya language.")
+ """The Oriya language."""
+
+ OROMO = (1138, "om-Ethi-ET", "The Oromo language.")
+ """The Oromo language."""
+
+ PASHTO = (1123, "ps-AF", "The Pashto language.")
+ """The Pashto language."""
+
+ POLISH = (1045, "pl-PL", "The Polish language.")
+ """The Polish language."""
+
+ PORTUGUESE = (2070, "pt-PT", "The Portuguese language.")
+ """The Portuguese language."""
+
+ PUNJABI = (1094, "pa-IN", "The Punjabi language.")
+ """The Punjabi language."""
+
+ QUECHUA_BOLIVIA = (1131, "quz-BO", "The Quechua Bolivia language.")
+ """The Quechua Bolivia language."""
+
+ QUECHUA_ECUADOR = (2155, "quz-EC", "The Quechua Ecuador language.")
+ """The Quechua Ecuador language."""
+
+ QUECHUA_PERU = (3179, "quz-PE", "The Quechua Peru language.")
+ """The Quechua Peru language."""
+
+ RHAETO_ROMANIC = (1047, "rm-CH", "The Rhaeto Romanic language.")
+ """The Rhaeto Romanic language."""
+
+ ROMANIAN = (1048, "ro-RO", "The Romanian language.")
+ """The Romanian language."""
+
+ ROMANIAN_MOLDOVA = (2072, "ro-MO", "The Romanian Moldova language.")
+ """The Romanian Moldova language."""
+
+ RUSSIAN = (1049, "ru-RU", "The Russian language.")
+ """The Russian language."""
+
+ RUSSIAN_MOLDOVA = (2073, "ru-MO", "The Russian Moldova language.")
+ """The Russian Moldova language."""
+
+ SAMI_LAPPISH = (1083, "se-NO", "The Sami Lappish language.")
+ """The Sami Lappish language."""
+
+ SANSKRIT = (1103, "sa-IN", "The Sanskrit language.")
+ """The Sanskrit language."""
+
+ SEPEDI = (1132, "ns-ZA", "The Sepedi language.")
+ """The Sepedi language."""
+
+ SERBIAN_BOSNIA_HERZEGOVINA_CYRILLIC = (
+ 7194,
+ "sr-BA",
+ "The Serbian Bosnia Herzegovina Cyrillic language.",
+ )
+ """The Serbian Bosnia Herzegovina Cyrillic language."""
+
+ SERBIAN_BOSNIA_HERZEGOVINA_LATIN = (
+ 6170,
+ "sr-Latn-BA",
+ "The Serbian Bosnia Herzegovina Latin language.",
+ )
+ """The Serbian Bosnia Herzegovina Latin language."""
+
+ SERBIAN_CYRILLIC = (3098, "sr-SP", "The Serbian Cyrillic language.")
+ """The Serbian Cyrillic language."""
+
+ SERBIAN_LATIN = (2074, "sr-Latn-CS", "The Serbian Latin language.")
+ """The Serbian Latin language."""
+
+ SESOTHO = (1072, "st-ZA", "The Sesotho language.")
+ """The Sesotho language."""
+
+ SIMPLIFIED_CHINESE = (2052, "zh-CN", "The Simplified Chinese language.")
+ """The Simplified Chinese language."""
+
+ SINDHI = (1113, "sd-Deva-IN", "The Sindhi language.")
+ """The Sindhi language."""
+
+ SINDHI_PAKISTAN = (2137, "sd-Arab-PK", "The Sindhi Pakistan language.")
+ """The Sindhi Pakistan language."""
+
+ SINHALESE = (1115, "si-LK", "The Sinhalese language.")
+ """The Sinhalese language."""
+
+ SLOVAK = (1051, "sk-SK", "The Slovak language.")
+ """The Slovak language."""
+
+ SLOVENIAN = (1060, "sl-SI", "The Slovenian language.")
+ """The Slovenian language."""
+
+ SOMALI = (1143, "so-SO", "The Somali language.")
+ """The Somali language."""
+
+ SORBIAN = (1070, "wen-DE", "The Sorbian language.")
+ """The Sorbian language."""
+
+ SPANISH = (1034, "es-ES_tradnl", "The Spanish language.")
+ """The Spanish language."""
+
+ SPANISH_ARGENTINA = (11274, "es-AR", "The Spanish Argentina language.")
+ """The Spanish Argentina language."""
+
+ SPANISH_BOLIVIA = (16394, "es-BO", "The Spanish Bolivia language.")
+ """The Spanish Bolivia language."""
+
+ SPANISH_CHILE = (13322, "es-CL", "The Spanish Chile language.")
+ """The Spanish Chile language."""
+
+ SPANISH_COLOMBIA = (9226, "es-CO", "The Spanish Colombia language.")
+ """The Spanish Colombia language."""
+
+ SPANISH_COSTA_RICA = (5130, "es-CR", "The Spanish Costa Rica language.")
+ """The Spanish Costa Rica language."""
+
+ SPANISH_DOMINICAN_REPUBLIC = (7178, "es-DO", "The Spanish Dominican Republic language.")
+ """The Spanish Dominican Republic language."""
+
+ SPANISH_ECUADOR = (12298, "es-EC", "The Spanish Ecuador language.")
+ """The Spanish Ecuador language."""
+
+ SPANISH_EL_SALVADOR = (17418, "es-SV", "The Spanish El Salvador language.")
+ """The Spanish El Salvador language."""
+
+ SPANISH_GUATEMALA = (4106, "es-GT", "The Spanish Guatemala language.")
+ """The Spanish Guatemala language."""
+
+ SPANISH_HONDURAS = (18442, "es-HN", "The Spanish Honduras language.")
+ """The Spanish Honduras language."""
+
+ SPANISH_MODERN_SORT = (3082, "es-ES", "The Spanish Modern Sort language.")
+ """The Spanish Modern Sort language."""
+
+ SPANISH_NICARAGUA = (19466, "es-NI", "The Spanish Nicaragua language.")
+ """The Spanish Nicaragua language."""
+
+ SPANISH_PANAMA = (6154, "es-PA", "The Spanish Panama language.")
+ """The Spanish Panama language."""
+
+ SPANISH_PARAGUAY = (15370, "es-PY", "The Spanish Paraguay language.")
+ """The Spanish Paraguay language."""
+
+ SPANISH_PERU = (10250, "es-PE", "The Spanish Peru language.")
+ """The Spanish Peru language."""
+
+ SPANISH_PUERTO_RICO = (20490, "es-PR", "The Spanish Puerto Rico language.")
+ """The Spanish Puerto Rico language."""
+
+ SPANISH_URUGUAY = (14346, "es-UR", "The Spanish Uruguay language.")
+ """The Spanish Uruguay language."""
+
+ SPANISH_VENEZUELA = (8202, "es-VE", "The Spanish Venezuela language.")
+ """The Spanish Venezuela language."""
+
+ SUTU = (1072, "st-ZA", "The Sutu language.")
+ """The Sutu language."""
+
+ SWAHILI = (1089, "sw-KE", "The Swahili language.")
+ """The Swahili language."""
+
+ SWEDISH = (1053, "sv-SE", "The Swedish language.")
+ """The Swedish language."""
+
+ SWEDISH_FINLAND = (2077, "sv-FI", "The Swedish Finland language.")
+ """The Swedish Finland language."""
+
+ SWISS_FRENCH = (4108, "fr-CH", "The Swiss French language.")
+ """The Swiss French language."""
+
+ SWISS_GERMAN = (2055, "de-CH", "The Swiss German language.")
+ """The Swiss German language."""
+
+ SWISS_ITALIAN = (2064, "it-CH", "The Swiss Italian language.")
+ """The Swiss Italian language."""
+
+ SYRIAC = (1114, "syr-SY", "The Syriac language.")
+ """The Syriac language."""
+
+ TAJIK = (1064, "tg-TJ", "The Tajik language.")
+ """The Tajik language."""
+
+ TAMAZIGHT = (1119, "tzm-Arab-MA", "The Tamazight language.")
+ """The Tamazight language."""
+
+ TAMAZIGHT_LATIN = (2143, "tmz-DZ", "The Tamazight Latin language.")
+ """The Tamazight Latin language."""
+
+ TAMIL = (1097, "ta-IN", "The Tamil language.")
+ """The Tamil language."""
+
+ TATAR = (1092, "tt-RU", "The Tatar language.")
+ """The Tatar language."""
+
+ TELUGU = (1098, "te-IN", "The Telugu language.")
+ """The Telugu language."""
+
+ THAI = (1054, "th-TH", "The Thai language.")
+ """The Thai language."""
+
+ TIBETAN = (1105, "bo-CN", "The Tibetan language.")
+ """The Tibetan language."""
+
+ TIGRIGNA_ERITREA = (2163, "ti-ER", "The Tigrigna Eritrea language.")
+ """The Tigrigna Eritrea language."""
+
+ TIGRIGNA_ETHIOPIC = (1139, "ti-ET", "The Tigrigna Ethiopic language.")
+ """The Tigrigna Ethiopic language."""
+
+ TRADITIONAL_CHINESE = (1028, "zh-TW", "The Traditional Chinese language.")
+ """The Traditional Chinese language."""
+
+ TSONGA = (1073, "ts-ZA", "The Tsonga language.")
+ """The Tsonga language."""
+
+ TSWANA = (1074, "tn-ZA", "The Tswana language.")
+ """The Tswana language."""
+
+ TURKISH = (1055, "tr-TR", "The Turkish language.")
+ """The Turkish language."""
+
+ TURKMEN = (1090, "tk-TM", "The Turkmen language.")
+ """The Turkmen language."""
+
+ UKRAINIAN = (1058, "uk-UA", "The Ukrainian language.")
+ """The Ukrainian language."""
+
+ URDU = (1056, "ur-PK", "The Urdu language.")
+ """The Urdu language."""
+
+ UZBEK_CYRILLIC = (2115, "uz-UZ", "The Uzbek Cyrillic language.")
+ """The Uzbek Cyrillic language."""
+
+ UZBEK_LATIN = (1091, "uz-Latn-UZ", "The Uzbek Latin language.")
+ """The Uzbek Latin language."""
+
+ VENDA = (1075, "ve-ZA", "The Venda language.")
+ """The Venda language."""
+
+ VIETNAMESE = (1066, "vi-VN", "The Vietnamese language.")
+ """The Vietnamese language."""
+
+ WELSH = (1106, "cy-GB", "The Welsh language.")
+ """The Welsh language."""
+
+ XHOSA = (1076, "xh-ZA", "The Xhosa language.")
+ """The Xhosa language."""
+
+ YI = (1144, "ii-CN", "The Yi language.")
+ """The Yi language."""
+
+ YIDDISH = (1085, "yi-Hebr", "The Yiddish language.")
+ """The Yiddish language."""
+
+ YORUBA = (1130, "yo-NG", "The Yoruba language.")
+ """The Yoruba language."""
+
+ ZULU = (1077, "zu-ZA", "The Zulu language.")
+ """The Zulu language."""
+
+ MIXED = (-2, "", "More than one language in specified range (read-only).")
+ """More than one language in specified range (read-only)."""
diff --git a/.venv/lib/python3.12/site-packages/pptx/enum/shapes.py b/.venv/lib/python3.12/site-packages/pptx/enum/shapes.py
new file mode 100644
index 00000000..86f521f4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/enum/shapes.py
@@ -0,0 +1,1029 @@
+"""Enumerations used by shapes and related objects."""
+
+from __future__ import annotations
+
+import enum
+
+from pptx.enum.base import BaseEnum, BaseXmlEnum
+
+
+class MSO_AUTO_SHAPE_TYPE(BaseXmlEnum):
+ """Specifies a type of AutoShape, e.g. DOWN_ARROW.
+
+ Alias: ``MSO_SHAPE``
+
+ Example::
+
+ from pptx.enum.shapes import MSO_SHAPE
+ from pptx.util import Inches
+
+ left = top = width = height = Inches(1.0)
+ slide.shapes.add_shape(
+ MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height
+ )
+
+ MS API Name: `MsoAutoShapeType`
+
+ https://learn.microsoft.com/en-us/office/vba/api/Office.MsoAutoShapeType
+ """
+
+ ACTION_BUTTON_BACK_OR_PREVIOUS = (
+ 129,
+ "actionButtonBackPrevious",
+ "Back or Previous button. Supports mouse-click and mouse-over actions",
+ )
+ """Back or Previous button. Supports mouse-click and mouse-over actions"""
+
+ ACTION_BUTTON_BEGINNING = (
+ 131,
+ "actionButtonBeginning",
+ "Beginning button. Supports mouse-click and mouse-over actions",
+ )
+ """Beginning button. Supports mouse-click and mouse-over actions"""
+
+ ACTION_BUTTON_CUSTOM = (
+ 125,
+ "actionButtonBlank",
+ "Button with no default picture or text. Supports mouse-click and mouse-over actions",
+ )
+ """Button with no default picture or text. Supports mouse-click and mouse-over actions"""
+
+ ACTION_BUTTON_DOCUMENT = (
+ 134,
+ "actionButtonDocument",
+ "Document button. Supports mouse-click and mouse-over actions",
+ )
+ """Document button. Supports mouse-click and mouse-over actions"""
+
+ ACTION_BUTTON_END = (
+ 132,
+ "actionButtonEnd",
+ "End button. Supports mouse-click and mouse-over actions",
+ )
+ """End button. Supports mouse-click and mouse-over actions"""
+
+ ACTION_BUTTON_FORWARD_OR_NEXT = (
+ 130,
+ "actionButtonForwardNext",
+ "Forward or Next button. Supports mouse-click and mouse-over actions",
+ )
+ """Forward or Next button. Supports mouse-click and mouse-over actions"""
+
+ ACTION_BUTTON_HELP = (
+ 127,
+ "actionButtonHelp",
+ "Help button. Supports mouse-click and mouse-over actions",
+ )
+ """Help button. Supports mouse-click and mouse-over actions"""
+
+ ACTION_BUTTON_HOME = (
+ 126,
+ "actionButtonHome",
+ "Home button. Supports mouse-click and mouse-over actions",
+ )
+ """Home button. Supports mouse-click and mouse-over actions"""
+
+ ACTION_BUTTON_INFORMATION = (
+ 128,
+ "actionButtonInformation",
+ "Information button. Supports mouse-click and mouse-over actions",
+ )
+ """Information button. Supports mouse-click and mouse-over actions"""
+
+ ACTION_BUTTON_MOVIE = (
+ 136,
+ "actionButtonMovie",
+ "Movie button. Supports mouse-click and mouse-over actions",
+ )
+ """Movie button. Supports mouse-click and mouse-over actions"""
+
+ ACTION_BUTTON_RETURN = (
+ 133,
+ "actionButtonReturn",
+ "Return button. Supports mouse-click and mouse-over actions",
+ )
+ """Return button. Supports mouse-click and mouse-over actions"""
+
+ ACTION_BUTTON_SOUND = (
+ 135,
+ "actionButtonSound",
+ "Sound button. Supports mouse-click and mouse-over actions",
+ )
+ """Sound button. Supports mouse-click and mouse-over actions"""
+
+ ARC = (25, "arc", "Arc")
+ """Arc"""
+
+ BALLOON = (137, "wedgeRoundRectCallout", "Rounded Rectangular Callout")
+ """Rounded Rectangular Callout"""
+
+ BENT_ARROW = (41, "bentArrow", "Block arrow that follows a curved 90-degree angle")
+ """Block arrow that follows a curved 90-degree angle"""
+
+ BENT_UP_ARROW = (
+ 44,
+ "bentUpArrow",
+ "Block arrow that follows a sharp 90-degree angle. Points up by default",
+ )
+ """Block arrow that follows a sharp 90-degree angle. Points up by default"""
+
+ BEVEL = (15, "bevel", "Bevel")
+ """Bevel"""
+
+ BLOCK_ARC = (20, "blockArc", "Block arc")
+ """Block arc"""
+
+ CAN = (13, "can", "Can")
+ """Can"""
+
+ CHART_PLUS = (182, "chartPlus", "Chart Plus")
+ """Chart Plus"""
+
+ CHART_STAR = (181, "chartStar", "Chart Star")
+ """Chart Star"""
+
+ CHART_X = (180, "chartX", "Chart X")
+ """Chart X"""
+
+ CHEVRON = (52, "chevron", "Chevron")
+ """Chevron"""
+
+ CHORD = (161, "chord", "Geometric chord shape")
+ """Geometric chord shape"""
+
+ CIRCULAR_ARROW = (60, "circularArrow", "Block arrow that follows a curved 180-degree angle")
+ """Block arrow that follows a curved 180-degree angle"""
+
+ CLOUD = (179, "cloud", "Cloud")
+ """Cloud"""
+
+ CLOUD_CALLOUT = (108, "cloudCallout", "Cloud callout")
+ """Cloud callout"""
+
+ CORNER = (162, "corner", "Corner")
+ """Corner"""
+
+ CORNER_TABS = (169, "cornerTabs", "Corner Tabs")
+ """Corner Tabs"""
+
+ CROSS = (11, "plus", "Cross")
+ """Cross"""
+
+ CUBE = (14, "cube", "Cube")
+ """Cube"""
+
+ CURVED_DOWN_ARROW = (48, "curvedDownArrow", "Block arrow that curves down")
+ """Block arrow that curves down"""
+
+ CURVED_DOWN_RIBBON = (100, "ellipseRibbon", "Ribbon banner that curves down")
+ """Ribbon banner that curves down"""
+
+ CURVED_LEFT_ARROW = (46, "curvedLeftArrow", "Block arrow that curves left")
+ """Block arrow that curves left"""
+
+ CURVED_RIGHT_ARROW = (45, "curvedRightArrow", "Block arrow that curves right")
+ """Block arrow that curves right"""
+
+ CURVED_UP_ARROW = (47, "curvedUpArrow", "Block arrow that curves up")
+ """Block arrow that curves up"""
+
+ CURVED_UP_RIBBON = (99, "ellipseRibbon2", "Ribbon banner that curves up")
+ """Ribbon banner that curves up"""
+
+ DECAGON = (144, "decagon", "Decagon")
+ """Decagon"""
+
+ DIAGONAL_STRIPE = (141, "diagStripe", "Diagonal Stripe")
+ """Diagonal Stripe"""
+
+ DIAMOND = (4, "diamond", "Diamond")
+ """Diamond"""
+
+ DODECAGON = (146, "dodecagon", "Dodecagon")
+ """Dodecagon"""
+
+ DONUT = (18, "donut", "Donut")
+ """Donut"""
+
+ DOUBLE_BRACE = (27, "bracePair", "Double brace")
+ """Double brace"""
+
+ DOUBLE_BRACKET = (26, "bracketPair", "Double bracket")
+ """Double bracket"""
+
+ DOUBLE_WAVE = (104, "doubleWave", "Double wave")
+ """Double wave"""
+
+ DOWN_ARROW = (36, "downArrow", "Block arrow that points down")
+ """Block arrow that points down"""
+
+ DOWN_ARROW_CALLOUT = (56, "downArrowCallout", "Callout with arrow that points down")
+ """Callout with arrow that points down"""
+
+ DOWN_RIBBON = (98, "ribbon", "Ribbon banner with center area below ribbon ends")
+ """Ribbon banner with center area below ribbon ends"""
+
+ EXPLOSION1 = (89, "irregularSeal1", "Explosion")
+ """Explosion"""
+
+ EXPLOSION2 = (90, "irregularSeal2", "Explosion")
+ """Explosion"""
+
+ FLOWCHART_ALTERNATE_PROCESS = (
+ 62,
+ "flowChartAlternateProcess",
+ "Alternate process flowchart symbol",
+ )
+ """Alternate process flowchart symbol"""
+
+ FLOWCHART_CARD = (75, "flowChartPunchedCard", "Card flowchart symbol")
+ """Card flowchart symbol"""
+
+ FLOWCHART_COLLATE = (79, "flowChartCollate", "Collate flowchart symbol")
+ """Collate flowchart symbol"""
+
+ FLOWCHART_CONNECTOR = (73, "flowChartConnector", "Connector flowchart symbol")
+ """Connector flowchart symbol"""
+
+ FLOWCHART_DATA = (64, "flowChartInputOutput", "Data flowchart symbol")
+ """Data flowchart symbol"""
+
+ FLOWCHART_DECISION = (63, "flowChartDecision", "Decision flowchart symbol")
+ """Decision flowchart symbol"""
+
+ FLOWCHART_DELAY = (84, "flowChartDelay", "Delay flowchart symbol")
+ """Delay flowchart symbol"""
+
+ FLOWCHART_DIRECT_ACCESS_STORAGE = (
+ 87,
+ "flowChartMagneticDrum",
+ "Direct access storage flowchart symbol",
+ )
+ """Direct access storage flowchart symbol"""
+
+ FLOWCHART_DISPLAY = (88, "flowChartDisplay", "Display flowchart symbol")
+ """Display flowchart symbol"""
+
+ FLOWCHART_DOCUMENT = (67, "flowChartDocument", "Document flowchart symbol")
+ """Document flowchart symbol"""
+
+ FLOWCHART_EXTRACT = (81, "flowChartExtract", "Extract flowchart symbol")
+ """Extract flowchart symbol"""
+
+ FLOWCHART_INTERNAL_STORAGE = (
+ 66,
+ "flowChartInternalStorage",
+ "Internal storage flowchart symbol",
+ )
+ """Internal storage flowchart symbol"""
+
+ FLOWCHART_MAGNETIC_DISK = (86, "flowChartMagneticDisk", "Magnetic disk flowchart symbol")
+ """Magnetic disk flowchart symbol"""
+
+ FLOWCHART_MANUAL_INPUT = (71, "flowChartManualInput", "Manual input flowchart symbol")
+ """Manual input flowchart symbol"""
+
+ FLOWCHART_MANUAL_OPERATION = (
+ 72,
+ "flowChartManualOperation",
+ "Manual operation flowchart symbol",
+ )
+ """Manual operation flowchart symbol"""
+
+ FLOWCHART_MERGE = (82, "flowChartMerge", "Merge flowchart symbol")
+ """Merge flowchart symbol"""
+
+ FLOWCHART_MULTIDOCUMENT = (68, "flowChartMultidocument", "Multi-document flowchart symbol")
+ """Multi-document flowchart symbol"""
+
+ FLOWCHART_OFFLINE_STORAGE = (139, "flowChartOfflineStorage", "Offline Storage")
+ """Offline Storage"""
+
+ FLOWCHART_OFFPAGE_CONNECTOR = (
+ 74,
+ "flowChartOffpageConnector",
+ "Off-page connector flowchart symbol",
+ )
+ """Off-page connector flowchart symbol"""
+
+ FLOWCHART_OR = (78, "flowChartOr", '"Or" flowchart symbol')
+ """\"Or\" flowchart symbol"""
+
+ FLOWCHART_PREDEFINED_PROCESS = (
+ 65,
+ "flowChartPredefinedProcess",
+ "Predefined process flowchart symbol",
+ )
+ """Predefined process flowchart symbol"""
+
+ FLOWCHART_PREPARATION = (70, "flowChartPreparation", "Preparation flowchart symbol")
+ """Preparation flowchart symbol"""
+
+ FLOWCHART_PROCESS = (61, "flowChartProcess", "Process flowchart symbol")
+ """Process flowchart symbol"""
+
+ FLOWCHART_PUNCHED_TAPE = (76, "flowChartPunchedTape", "Punched tape flowchart symbol")
+ """Punched tape flowchart symbol"""
+
+ FLOWCHART_SEQUENTIAL_ACCESS_STORAGE = (
+ 85,
+ "flowChartMagneticTape",
+ "Sequential access storage flowchart symbol",
+ )
+ """Sequential access storage flowchart symbol"""
+
+ FLOWCHART_SORT = (80, "flowChartSort", "Sort flowchart symbol")
+ """Sort flowchart symbol"""
+
+ FLOWCHART_STORED_DATA = (83, "flowChartOnlineStorage", "Stored data flowchart symbol")
+ """Stored data flowchart symbol"""
+
+ FLOWCHART_SUMMING_JUNCTION = (
+ 77,
+ "flowChartSummingJunction",
+ "Summing junction flowchart symbol",
+ )
+ """Summing junction flowchart symbol"""
+
+ FLOWCHART_TERMINATOR = (69, "flowChartTerminator", "Terminator flowchart symbol")
+ """Terminator flowchart symbol"""
+
+ FOLDED_CORNER = (16, "foldedCorner", "Folded corner")
+ """Folded corner"""
+
+ FRAME = (158, "frame", "Frame")
+ """Frame"""
+
+ FUNNEL = (174, "funnel", "Funnel")
+ """Funnel"""
+
+ GEAR_6 = (172, "gear6", "Gear 6")
+ """Gear 6"""
+
+ GEAR_9 = (173, "gear9", "Gear 9")
+ """Gear 9"""
+
+ HALF_FRAME = (159, "halfFrame", "Half Frame")
+ """Half Frame"""
+
+ HEART = (21, "heart", "Heart")
+ """Heart"""
+
+ HEPTAGON = (145, "heptagon", "Heptagon")
+ """Heptagon"""
+
+ HEXAGON = (10, "hexagon", "Hexagon")
+ """Hexagon"""
+
+ HORIZONTAL_SCROLL = (102, "horizontalScroll", "Horizontal scroll")
+ """Horizontal scroll"""
+
+ ISOSCELES_TRIANGLE = (7, "triangle", "Isosceles triangle")
+ """Isosceles triangle"""
+
+ LEFT_ARROW = (34, "leftArrow", "Block arrow that points left")
+ """Block arrow that points left"""
+
+ LEFT_ARROW_CALLOUT = (54, "leftArrowCallout", "Callout with arrow that points left")
+ """Callout with arrow that points left"""
+
+ LEFT_BRACE = (31, "leftBrace", "Left brace")
+ """Left brace"""
+
+ LEFT_BRACKET = (29, "leftBracket", "Left bracket")
+ """Left bracket"""
+
+ LEFT_CIRCULAR_ARROW = (176, "leftCircularArrow", "Left Circular Arrow")
+ """Left Circular Arrow"""
+
+ LEFT_RIGHT_ARROW = (
+ 37,
+ "leftRightArrow",
+ "Block arrow with arrowheads that point both left and right",
+ )
+ """Block arrow with arrowheads that point both left and right"""
+
+ LEFT_RIGHT_ARROW_CALLOUT = (
+ 57,
+ "leftRightArrowCallout",
+ "Callout with arrowheads that point both left and right",
+ )
+ """Callout with arrowheads that point both left and right"""
+
+ LEFT_RIGHT_CIRCULAR_ARROW = (177, "leftRightCircularArrow", "Left Right Circular Arrow")
+ """Left Right Circular Arrow"""
+
+ LEFT_RIGHT_RIBBON = (140, "leftRightRibbon", "Left Right Ribbon")
+ """Left Right Ribbon"""
+
+ LEFT_RIGHT_UP_ARROW = (
+ 40,
+ "leftRightUpArrow",
+ "Block arrow with arrowheads that point left, right, and up",
+ )
+ """Block arrow with arrowheads that point left, right, and up"""
+
+ LEFT_UP_ARROW = (43, "leftUpArrow", "Block arrow with arrowheads that point left and up")
+ """Block arrow with arrowheads that point left and up"""
+
+ LIGHTNING_BOLT = (22, "lightningBolt", "Lightning bolt")
+ """Lightning bolt"""
+
+ LINE_CALLOUT_1 = (109, "borderCallout1", "Callout with border and horizontal callout line")
+ """Callout with border and horizontal callout line"""
+
+ LINE_CALLOUT_1_ACCENT_BAR = (113, "accentCallout1", "Callout with vertical accent bar")
+ """Callout with vertical accent bar"""
+
+ LINE_CALLOUT_1_BORDER_AND_ACCENT_BAR = (
+ 121,
+ "accentBorderCallout1",
+ "Callout with border and vertical accent bar",
+ )
+ """Callout with border and vertical accent bar"""
+
+ LINE_CALLOUT_1_NO_BORDER = (117, "callout1", "Callout with horizontal line")
+ """Callout with horizontal line"""
+
+ LINE_CALLOUT_2 = (110, "borderCallout2", "Callout with diagonal straight line")
+ """Callout with diagonal straight line"""
+
+ LINE_CALLOUT_2_ACCENT_BAR = (
+ 114,
+ "accentCallout2",
+ "Callout with diagonal callout line and accent bar",
+ )
+ """Callout with diagonal callout line and accent bar"""
+
+ LINE_CALLOUT_2_BORDER_AND_ACCENT_BAR = (
+ 122,
+ "accentBorderCallout2",
+ "Callout with border, diagonal straight line, and accent bar",
+ )
+ """Callout with border, diagonal straight line, and accent bar"""
+
+ LINE_CALLOUT_2_NO_BORDER = (118, "callout2", "Callout with no border and diagonal callout line")
+ """Callout with no border and diagonal callout line"""
+
+ LINE_CALLOUT_3 = (111, "borderCallout3", "Callout with angled line")
+ """Callout with angled line"""
+
+ LINE_CALLOUT_3_ACCENT_BAR = (
+ 115,
+ "accentCallout3",
+ "Callout with angled callout line and accent bar",
+ )
+ """Callout with angled callout line and accent bar"""
+
+ LINE_CALLOUT_3_BORDER_AND_ACCENT_BAR = (
+ 123,
+ "accentBorderCallout3",
+ "Callout with border, angled callout line, and accent bar",
+ )
+ """Callout with border, angled callout line, and accent bar"""
+
+ LINE_CALLOUT_3_NO_BORDER = (119, "callout3", "Callout with no border and angled callout line")
+ """Callout with no border and angled callout line"""
+
+ LINE_CALLOUT_4 = (
+ 112,
+ "borderCallout3",
+ "Callout with callout line segments forming a U-shape.",
+ )
+ """Callout with callout line segments forming a U-shape."""
+
+ LINE_CALLOUT_4_ACCENT_BAR = (
+ 116,
+ "accentCallout3",
+ "Callout with accent bar and callout line segments forming a U-shape.",
+ )
+ """Callout with accent bar and callout line segments forming a U-shape."""
+
+ LINE_CALLOUT_4_BORDER_AND_ACCENT_BAR = (
+ 124,
+ "accentBorderCallout3",
+ "Callout with border, accent bar, and callout line segments forming a U-shape.",
+ )
+ """Callout with border, accent bar, and callout line segments forming a U-shape."""
+
+ LINE_CALLOUT_4_NO_BORDER = (
+ 120,
+ "callout3",
+ "Callout with no border and callout line segments forming a U-shape.",
+ )
+ """Callout with no border and callout line segments forming a U-shape."""
+
+ LINE_INVERSE = (183, "lineInv", "Straight Connector")
+ """Straight Connector"""
+
+ MATH_DIVIDE = (166, "mathDivide", "Division")
+ """Division"""
+
+ MATH_EQUAL = (167, "mathEqual", "Equal")
+ """Equal"""
+
+ MATH_MINUS = (164, "mathMinus", "Minus")
+ """Minus"""
+
+ MATH_MULTIPLY = (165, "mathMultiply", "Multiply")
+ """Multiply"""
+
+ MATH_NOT_EQUAL = (168, "mathNotEqual", "Not Equal")
+ """Not Equal"""
+
+ MATH_PLUS = (163, "mathPlus", "Plus")
+ """Plus"""
+
+ MOON = (24, "moon", "Moon")
+ """Moon"""
+
+ NON_ISOSCELES_TRAPEZOID = (143, "nonIsoscelesTrapezoid", "Non-isosceles Trapezoid")
+ """Non-isosceles Trapezoid"""
+
+ NOTCHED_RIGHT_ARROW = (50, "notchedRightArrow", "Notched block arrow that points right")
+ """Notched block arrow that points right"""
+
+ NO_SYMBOL = (19, "noSmoking", "'No' Symbol")
+ """'No' Symbol"""
+
+ OCTAGON = (6, "octagon", "Octagon")
+ """Octagon"""
+
+ OVAL = (9, "ellipse", "Oval")
+ """Oval"""
+
+ OVAL_CALLOUT = (107, "wedgeEllipseCallout", "Oval-shaped callout")
+ """Oval-shaped callout"""
+
+ PARALLELOGRAM = (2, "parallelogram", "Parallelogram")
+ """Parallelogram"""
+
+ PENTAGON = (51, "homePlate", "Pentagon")
+ """Pentagon"""
+
+ PIE = (142, "pie", "Pie")
+ """Pie"""
+
+ PIE_WEDGE = (175, "pieWedge", "Pie")
+ """Pie"""
+
+ PLAQUE = (28, "plaque", "Plaque")
+ """Plaque"""
+
+ PLAQUE_TABS = (171, "plaqueTabs", "Plaque Tabs")
+ """Plaque Tabs"""
+
+ QUAD_ARROW = (39, "quadArrow", "Block arrows that point up, down, left, and right")
+ """Block arrows that point up, down, left, and right"""
+
+ QUAD_ARROW_CALLOUT = (
+ 59,
+ "quadArrowCallout",
+ "Callout with arrows that point up, down, left, and right",
+ )
+ """Callout with arrows that point up, down, left, and right"""
+
+ RECTANGLE = (1, "rect", "Rectangle")
+ """Rectangle"""
+
+ RECTANGULAR_CALLOUT = (105, "wedgeRectCallout", "Rectangular callout")
+ """Rectangular callout"""
+
+ REGULAR_PENTAGON = (12, "pentagon", "Pentagon")
+ """Pentagon"""
+
+ RIGHT_ARROW = (33, "rightArrow", "Block arrow that points right")
+ """Block arrow that points right"""
+
+ RIGHT_ARROW_CALLOUT = (53, "rightArrowCallout", "Callout with arrow that points right")
+ """Callout with arrow that points right"""
+
+ RIGHT_BRACE = (32, "rightBrace", "Right brace")
+ """Right brace"""
+
+ RIGHT_BRACKET = (30, "rightBracket", "Right bracket")
+ """Right bracket"""
+
+ RIGHT_TRIANGLE = (8, "rtTriangle", "Right triangle")
+ """Right triangle"""
+
+ ROUNDED_RECTANGLE = (5, "roundRect", "Rounded rectangle")
+ """Rounded rectangle"""
+
+ ROUNDED_RECTANGULAR_CALLOUT = (106, "wedgeRoundRectCallout", "Rounded rectangle-shaped callout")
+ """Rounded rectangle-shaped callout"""
+
+ ROUND_1_RECTANGLE = (151, "round1Rect", "Round Single Corner Rectangle")
+ """Round Single Corner Rectangle"""
+
+ ROUND_2_DIAG_RECTANGLE = (153, "round2DiagRect", "Round Diagonal Corner Rectangle")
+ """Round Diagonal Corner Rectangle"""
+
+ ROUND_2_SAME_RECTANGLE = (152, "round2SameRect", "Round Same Side Corner Rectangle")
+ """Round Same Side Corner Rectangle"""
+
+ SMILEY_FACE = (17, "smileyFace", "Smiley face")
+ """Smiley face"""
+
+ SNIP_1_RECTANGLE = (155, "snip1Rect", "Snip Single Corner Rectangle")
+ """Snip Single Corner Rectangle"""
+
+ SNIP_2_DIAG_RECTANGLE = (157, "snip2DiagRect", "Snip Diagonal Corner Rectangle")
+ """Snip Diagonal Corner Rectangle"""
+
+ SNIP_2_SAME_RECTANGLE = (156, "snip2SameRect", "Snip Same Side Corner Rectangle")
+ """Snip Same Side Corner Rectangle"""
+
+ SNIP_ROUND_RECTANGLE = (154, "snipRoundRect", "Snip and Round Single Corner Rectangle")
+ """Snip and Round Single Corner Rectangle"""
+
+ SQUARE_TABS = (170, "squareTabs", "Square Tabs")
+ """Square Tabs"""
+
+ STAR_10_POINT = (149, "star10", "10-Point Star")
+ """10-Point Star"""
+
+ STAR_12_POINT = (150, "star12", "12-Point Star")
+ """12-Point Star"""
+
+ STAR_16_POINT = (94, "star16", "16-point star")
+ """16-point star"""
+
+ STAR_24_POINT = (95, "star24", "24-point star")
+ """24-point star"""
+
+ STAR_32_POINT = (96, "star32", "32-point star")
+ """32-point star"""
+
+ STAR_4_POINT = (91, "star4", "4-point star")
+ """4-point star"""
+
+ STAR_5_POINT = (92, "star5", "5-point star")
+ """5-point star"""
+
+ STAR_6_POINT = (147, "star6", "6-Point Star")
+ """6-Point Star"""
+
+ STAR_7_POINT = (148, "star7", "7-Point Star")
+ """7-Point Star"""
+
+ STAR_8_POINT = (93, "star8", "8-point star")
+ """8-point star"""
+
+ STRIPED_RIGHT_ARROW = (
+ 49,
+ "stripedRightArrow",
+ "Block arrow that points right with stripes at the tail",
+ )
+ """Block arrow that points right with stripes at the tail"""
+
+ SUN = (23, "sun", "Sun")
+ """Sun"""
+
+ SWOOSH_ARROW = (178, "swooshArrow", "Swoosh Arrow")
+ """Swoosh Arrow"""
+
+ TEAR = (160, "teardrop", "Teardrop")
+ """Teardrop"""
+
+ TRAPEZOID = (3, "trapezoid", "Trapezoid")
+ """Trapezoid"""
+
+ UP_ARROW = (35, "upArrow", "Block arrow that points up")
+ """Block arrow that points up"""
+
+ UP_ARROW_CALLOUT = (55, "upArrowCallout", "Callout with arrow that points up")
+ """Callout with arrow that points up"""
+
+ UP_DOWN_ARROW = (38, "upDownArrow", "Block arrow that points up and down")
+ """Block arrow that points up and down"""
+
+ UP_DOWN_ARROW_CALLOUT = (58, "upDownArrowCallout", "Callout with arrows that point up and down")
+ """Callout with arrows that point up and down"""
+
+ UP_RIBBON = (97, "ribbon2", "Ribbon banner with center area above ribbon ends")
+ """Ribbon banner with center area above ribbon ends"""
+
+ U_TURN_ARROW = (42, "uturnArrow", "Block arrow forming a U shape")
+ """Block arrow forming a U shape"""
+
+ VERTICAL_SCROLL = (101, "verticalScroll", "Vertical scroll")
+ """Vertical scroll"""
+
+ WAVE = (103, "wave", "Wave")
+ """Wave"""
+
+
+MSO_SHAPE = MSO_AUTO_SHAPE_TYPE
+
+
+class MSO_CONNECTOR_TYPE(BaseXmlEnum):
+ """
+ Specifies a type of connector.
+
+ Alias: ``MSO_CONNECTOR``
+
+ Example::
+
+ from pptx.enum.shapes import MSO_CONNECTOR
+ from pptx.util import Cm
+
+ shapes = prs.slides[0].shapes
+ connector = shapes.add_connector(
+ MSO_CONNECTOR.STRAIGHT, Cm(2), Cm(2), Cm(10), Cm(10)
+ )
+ assert connector.left.cm == 2
+
+ MS API Name: `MsoConnectorType`
+
+ http://msdn.microsoft.com/en-us/library/office/ff860918.aspx
+ """
+
+ CURVE = (3, "curvedConnector3", "Curved connector.")
+ """Curved connector."""
+
+ ELBOW = (2, "bentConnector3", "Elbow connector.")
+ """Elbow connector."""
+
+ STRAIGHT = (1, "line", "Straight line connector.")
+ """Straight line connector."""
+
+ MIXED = (-2, "", "Return value only; indicates a combination of other states.")
+ """Return value only; indicates a combination of other states."""
+
+
+MSO_CONNECTOR = MSO_CONNECTOR_TYPE
+
+
+class MSO_SHAPE_TYPE(BaseEnum):
+ """Specifies the type of a shape, more specifically than the five base types.
+
+ Alias: ``MSO``
+
+ Example::
+
+ from pptx.enum.shapes import MSO_SHAPE_TYPE
+
+ assert shape.type == MSO_SHAPE_TYPE.PICTURE
+
+ MS API Name: `MsoShapeType`
+
+ http://msdn.microsoft.com/en-us/library/office/ff860759(v=office.15).aspx
+ """
+
+ AUTO_SHAPE = (1, "AutoShape")
+ """AutoShape"""
+
+ CALLOUT = (2, "Callout shape")
+ """Callout shape"""
+
+ CANVAS = (20, "Drawing canvas")
+ """Drawing canvas"""
+
+ CHART = (3, "Chart, e.g. pie chart, bar chart")
+ """Chart, e.g. pie chart, bar chart"""
+
+ COMMENT = (4, "Comment")
+ """Comment"""
+
+ DIAGRAM = (21, "Diagram")
+ """Diagram"""
+
+ EMBEDDED_OLE_OBJECT = (7, "Embedded OLE object")
+ """Embedded OLE object"""
+
+ FORM_CONTROL = (8, "Form control")
+ """Form control"""
+
+ FREEFORM = (5, "Freeform")
+ """Freeform"""
+
+ GROUP = (6, "Group shape")
+ """Group shape"""
+
+ IGX_GRAPHIC = (24, "SmartArt graphic")
+ """SmartArt graphic"""
+
+ INK = (22, "Ink")
+ """Ink"""
+
+ INK_COMMENT = (23, "Ink Comment")
+ """Ink Comment"""
+
+ LINE = (9, "Line")
+ """Line"""
+
+ LINKED_OLE_OBJECT = (10, "Linked OLE object")
+ """Linked OLE object"""
+
+ LINKED_PICTURE = (11, "Linked picture")
+ """Linked picture"""
+
+ MEDIA = (16, "Media")
+ """Media"""
+
+ OLE_CONTROL_OBJECT = (12, "OLE control object")
+ """OLE control object"""
+
+ PICTURE = (13, "Picture")
+ """Picture"""
+
+ PLACEHOLDER = (14, "Placeholder")
+ """Placeholder"""
+
+ SCRIPT_ANCHOR = (18, "Script anchor")
+ """Script anchor"""
+
+ TABLE = (19, "Table")
+ """Table"""
+
+ TEXT_BOX = (17, "Text box")
+ """Text box"""
+
+ TEXT_EFFECT = (15, "Text effect")
+ """Text effect"""
+
+ WEB_VIDEO = (26, "Web video")
+ """Web video"""
+
+ MIXED = (-2, "Multiple shape types (read-only).")
+ """Multiple shape types (read-only)."""
+
+
+MSO = MSO_SHAPE_TYPE
+
+
+class PP_MEDIA_TYPE(BaseEnum):
+ """Indicates the OLE media type.
+
+ Example::
+
+ from pptx.enum.shapes import PP_MEDIA_TYPE
+
+ movie = slide.shapes[0]
+ assert movie.media_type == PP_MEDIA_TYPE.MOVIE
+
+ MS API Name: `PpMediaType`
+
+ https://msdn.microsoft.com/en-us/library/office/ff746008.aspx
+ """
+
+ MOVIE = (3, "Video media such as MP4.")
+ """Video media such as MP4."""
+
+ OTHER = (1, "Other media types")
+ """Other media types"""
+
+ SOUND = (1, "Audio media such as MP3.")
+ """Audio media such as MP3."""
+
+ MIXED = (
+ -2,
+ "Return value only; indicates multiple media types, typically for a collection of shapes."
+ " May not be applicable in python-pptx.",
+ )
+ """Return value only; indicates multiple media types.
+
+ Typically for a collection of shapes. May not be applicable in python-pptx.
+ """
+
+
+class PP_PLACEHOLDER_TYPE(BaseXmlEnum):
+ """Specifies one of the 18 distinct types of placeholder.
+
+ Alias: ``PP_PLACEHOLDER``
+
+ Example::
+
+ from pptx.enum.shapes import PP_PLACEHOLDER
+
+ placeholder = slide.placeholders[0]
+ assert placeholder.type == PP_PLACEHOLDER.TITLE
+
+ MS API name: `PpPlaceholderType`
+
+ http://msdn.microsoft.com/en-us/library/office/ff860759(v=office.15 ").aspx"
+ """
+
+ BITMAP = (9, "clipArt", "Clip art placeholder")
+ """Clip art placeholder"""
+
+ BODY = (2, "body", "Body")
+ """Body"""
+
+ CENTER_TITLE = (3, "ctrTitle", "Center Title")
+ """Center Title"""
+
+ CHART = (8, "chart", "Chart")
+ """Chart"""
+
+ DATE = (16, "dt", "Date")
+ """Date"""
+
+ FOOTER = (15, "ftr", "Footer")
+ """Footer"""
+
+ HEADER = (14, "hdr", "Header")
+ """Header"""
+
+ MEDIA_CLIP = (10, "media", "Media Clip")
+ """Media Clip"""
+
+ OBJECT = (7, "obj", "Object")
+ """Object"""
+
+ ORG_CHART = (11, "dgm", "SmartArt placeholder. Organization chart is a legacy name.")
+ """SmartArt placeholder. Organization chart is a legacy name."""
+
+ PICTURE = (18, "pic", "Picture")
+ """Picture"""
+
+ SLIDE_IMAGE = (101, "sldImg", "Slide Image")
+ """Slide Image"""
+
+ SLIDE_NUMBER = (13, "sldNum", "Slide Number")
+ """Slide Number"""
+
+ SUBTITLE = (4, "subTitle", "Subtitle")
+ """Subtitle"""
+
+ TABLE = (12, "tbl", "Table")
+ """Table"""
+
+ TITLE = (1, "title", "Title")
+ """Title"""
+
+ VERTICAL_BODY = (6, "", "Vertical Body (read-only).")
+ """Vertical Body (read-only)."""
+
+ VERTICAL_OBJECT = (17, "", "Vertical Object (read-only).")
+ """Vertical Object (read-only)."""
+
+ VERTICAL_TITLE = (5, "", "Vertical Title (read-only).")
+ """Vertical Title (read-only)."""
+
+ MIXED = (-2, "", "Return value only; multiple placeholders of differing types.")
+ """Return value only; multiple placeholders of differing types."""
+
+
+PP_PLACEHOLDER = PP_PLACEHOLDER_TYPE
+
+
+class PROG_ID(enum.Enum):
+ """One-off Enum-like object for progId values.
+
+ Indicates the type of an OLE object in terms of the program used to open it.
+
+ A member of this enumeration can be used in a `SlideShapes.add_ole_object()` call to
+ specify a Microsoft Office file-type (Excel, PowerPoint, or Word), which will
+ then not require several of the arguments required to embed other object types.
+
+ Example::
+
+ from pptx.enum.shapes import PROG_ID
+ from pptx.util import Inches
+
+ embedded_xlsx_shape = slide.shapes.add_ole_object(
+ "workbook.xlsx", PROG_ID.XLSX, left=Inches(1), top=Inches(1)
+ )
+ assert embedded_xlsx_shape.ole_format.prog_id == "Excel.Sheet.12"
+ """
+
+ _progId: str
+ _icon_filename: str
+ _width: int
+ _height: int
+
+ def __new__(cls, value: str, progId: str, icon_filename: str, width: int, height: int):
+ self = object.__new__(cls)
+ self._value_ = value
+ self._progId = progId
+ self._icon_filename = icon_filename
+ self._width = width
+ self._height = height
+ return self
+
+ @property
+ def height(self):
+ return self._height
+
+ @property
+ def icon_filename(self):
+ return self._icon_filename
+
+ @property
+ def progId(self):
+ return self._progId
+
+ @property
+ def width(self):
+ return self._width
+
+ DOCX = ("DOCX", "Word.Document.12", "docx-icon.emf", 965200, 609600)
+ """`progId` for an embedded Word 2007+ (.docx) document."""
+
+ PPTX = ("PPTX", "PowerPoint.Show.12", "pptx-icon.emf", 965200, 609600)
+ """`progId` for an embedded PowerPoint 2007+ (.pptx) document."""
+
+ XLSX = ("XLSX", "Excel.Sheet.12", "xlsx-icon.emf", 965200, 609600)
+ """`progId` for an embedded Excel 2007+ (.xlsx) document."""
diff --git a/.venv/lib/python3.12/site-packages/pptx/enum/text.py b/.venv/lib/python3.12/site-packages/pptx/enum/text.py
new file mode 100644
index 00000000..db266a3c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/enum/text.py
@@ -0,0 +1,230 @@
+"""Enumerations used by text and related objects."""
+
+from __future__ import annotations
+
+from pptx.enum.base import BaseEnum, BaseXmlEnum
+
+
+class MSO_AUTO_SIZE(BaseEnum):
+ """Determines the type of automatic sizing allowed.
+
+ The following names can be used to specify the automatic sizing behavior used to fit a shape's
+ text within the shape bounding box, for example::
+
+ from pptx.enum.text import MSO_AUTO_SIZE
+
+ shape.text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
+
+ The word-wrap setting of the text frame interacts with the auto-size setting to determine the
+ specific auto-sizing behavior.
+
+ Note that `TextFrame.auto_size` can also be set to |None|, which removes the auto size setting
+ altogether. This causes the setting to be inherited, either from the layout placeholder, in the
+ case of a placeholder shape, or from the theme.
+
+ MS API Name: `MsoAutoSize`
+
+ http://msdn.microsoft.com/en-us/library/office/ff865367(v=office.15).aspx
+ """
+
+ NONE = (
+ 0,
+ "No automatic sizing of the shape or text will be done.\n\nText can freely extend beyond"
+ " the horizontal and vertical edges of the shape bounding box.",
+ )
+ """No automatic sizing of the shape or text will be done.
+
+ Text can freely extend beyond the horizontal and vertical edges of the shape bounding box.
+ """
+
+ SHAPE_TO_FIT_TEXT = (
+ 1,
+ "The shape height and possibly width are adjusted to fit the text.\n\nNote this setting"
+ " interacts with the TextFrame.word_wrap property setting. If word wrap is turned on,"
+ " only the height of the shape will be adjusted; soft line breaks will be used to fit the"
+ " text horizontally.",
+ )
+ """The shape height and possibly width are adjusted to fit the text.
+
+ Note this setting interacts with the TextFrame.word_wrap property setting. If word wrap is
+ turned on, only the height of the shape will be adjusted; soft line breaks will be used to fit
+ the text horizontally.
+ """
+
+ TEXT_TO_FIT_SHAPE = (
+ 2,
+ "The font size is reduced as necessary to fit the text within the shape.",
+ )
+ """The font size is reduced as necessary to fit the text within the shape."""
+
+ MIXED = (-2, "Return value only; indicates a combination of automatic sizing schemes are used.")
+ """Return value only; indicates a combination of automatic sizing schemes are used."""
+
+
+class MSO_TEXT_UNDERLINE_TYPE(BaseXmlEnum):
+ """
+ Indicates the type of underline for text. Used with
+ :attr:`.Font.underline` to specify the style of text underlining.
+
+ Alias: ``MSO_UNDERLINE``
+
+ Example::
+
+ from pptx.enum.text import MSO_UNDERLINE
+
+ run.font.underline = MSO_UNDERLINE.DOUBLE_LINE
+
+ MS API Name: `MsoTextUnderlineType`
+
+ http://msdn.microsoft.com/en-us/library/aa432699.aspx
+ """
+
+ NONE = (0, "none", "Specifies no underline.")
+ """Specifies no underline."""
+
+ DASH_HEAVY_LINE = (8, "dashHeavy", "Specifies a dash underline.")
+ """Specifies a dash underline."""
+
+ DASH_LINE = (7, "dash", "Specifies a dash line underline.")
+ """Specifies a dash line underline."""
+
+ DASH_LONG_HEAVY_LINE = (10, "dashLongHeavy", "Specifies a long heavy line underline.")
+ """Specifies a long heavy line underline."""
+
+ DASH_LONG_LINE = (9, "dashLong", "Specifies a dashed long line underline.")
+ """Specifies a dashed long line underline."""
+
+ DOT_DASH_HEAVY_LINE = (12, "dotDashHeavy", "Specifies a dot dash heavy line underline.")
+ """Specifies a dot dash heavy line underline."""
+
+ DOT_DASH_LINE = (11, "dotDash", "Specifies a dot dash line underline.")
+ """Specifies a dot dash line underline."""
+
+ DOT_DOT_DASH_HEAVY_LINE = (
+ 14,
+ "dotDotDashHeavy",
+ "Specifies a dot dot dash heavy line underline.",
+ )
+ """Specifies a dot dot dash heavy line underline."""
+
+ DOT_DOT_DASH_LINE = (13, "dotDotDash", "Specifies a dot dot dash line underline.")
+ """Specifies a dot dot dash line underline."""
+
+ DOTTED_HEAVY_LINE = (6, "dottedHeavy", "Specifies a dotted heavy line underline.")
+ """Specifies a dotted heavy line underline."""
+
+ DOTTED_LINE = (5, "dotted", "Specifies a dotted line underline.")
+ """Specifies a dotted line underline."""
+
+ DOUBLE_LINE = (3, "dbl", "Specifies a double line underline.")
+ """Specifies a double line underline."""
+
+ HEAVY_LINE = (4, "heavy", "Specifies a heavy line underline.")
+ """Specifies a heavy line underline."""
+
+ SINGLE_LINE = (2, "sng", "Specifies a single line underline.")
+ """Specifies a single line underline."""
+
+ WAVY_DOUBLE_LINE = (17, "wavyDbl", "Specifies a wavy double line underline.")
+ """Specifies a wavy double line underline."""
+
+ WAVY_HEAVY_LINE = (16, "wavyHeavy", "Specifies a wavy heavy line underline.")
+ """Specifies a wavy heavy line underline."""
+
+ WAVY_LINE = (15, "wavy", "Specifies a wavy line underline.")
+ """Specifies a wavy line underline."""
+
+ WORDS = (1, "words", "Specifies underlining words.")
+ """Specifies underlining words."""
+
+ MIXED = (-2, "", "Specifies a mix of underline types (read-only).")
+ """Specifies a mix of underline types (read-only)."""
+
+
+MSO_UNDERLINE = MSO_TEXT_UNDERLINE_TYPE
+
+
+class MSO_VERTICAL_ANCHOR(BaseXmlEnum):
+ """Specifies the vertical alignment of text in a text frame.
+
+ Used with the `.vertical_anchor` property of the |TextFrame| object. Note that the
+ `vertical_anchor` property can also have the value None, indicating there is no directly
+ specified vertical anchor setting and its effective value is inherited from its placeholder if
+ it has one or from the theme. |None| may also be assigned to remove an explicitly specified
+ vertical anchor setting.
+
+ MS API Name: `MsoVerticalAnchor`
+
+ http://msdn.microsoft.com/en-us/library/office/ff865255.aspx
+ """
+
+ TOP = (1, "t", "Aligns text to top of text frame")
+ """Aligns text to top of text frame"""
+
+ MIDDLE = (3, "ctr", "Centers text vertically")
+ """Centers text vertically"""
+
+ BOTTOM = (4, "b", "Aligns text to bottom of text frame")
+ """Aligns text to bottom of text frame"""
+
+ MIXED = (-2, "", "Return value only; indicates a combination of the other states.")
+ """Return value only; indicates a combination of the other states."""
+
+
+MSO_ANCHOR = MSO_VERTICAL_ANCHOR
+
+
+class PP_PARAGRAPH_ALIGNMENT(BaseXmlEnum):
+ """Specifies the horizontal alignment for one or more paragraphs.
+
+ Alias: `PP_ALIGN`
+
+ Example::
+
+ from pptx.enum.text import PP_ALIGN
+
+ shape.paragraphs[0].alignment = PP_ALIGN.CENTER
+
+ MS API Name: `PpParagraphAlignment`
+
+ http://msdn.microsoft.com/en-us/library/office/ff745375(v=office.15).aspx
+ """
+
+ CENTER = (2, "ctr", "Center align")
+ """Center align"""
+
+ DISTRIBUTE = (
+ 5,
+ "dist",
+ "Evenly distributes e.g. Japanese characters from left to right within a line",
+ )
+ """Evenly distributes e.g. Japanese characters from left to right within a line"""
+
+ JUSTIFY = (
+ 4,
+ "just",
+ "Justified, i.e. each line both begins and ends at the margin.\n\nSpacing between words"
+ " is adjusted such that the line exactly fills the width of the paragraph.",
+ )
+ """Justified, i.e. each line both begins and ends at the margin.
+
+ Spacing between words is adjusted such that the line exactly fills the width of the paragraph.
+ """
+
+ JUSTIFY_LOW = (7, "justLow", "Justify using a small amount of space between words.")
+ """Justify using a small amount of space between words."""
+
+ LEFT = (1, "l", "Left aligned")
+ """Left aligned"""
+
+ RIGHT = (3, "r", "Right aligned")
+ """Right aligned"""
+
+ THAI_DISTRIBUTE = (6, "thaiDist", "Thai distributed")
+ """Thai distributed"""
+
+ MIXED = (-2, "", "Multiple alignments are present in a set of paragraphs (read-only).")
+ """Multiple alignments are present in a set of paragraphs (read-only)."""
+
+
+PP_ALIGN = PP_PARAGRAPH_ALIGNMENT
diff --git a/.venv/lib/python3.12/site-packages/pptx/exc.py b/.venv/lib/python3.12/site-packages/pptx/exc.py
new file mode 100644
index 00000000..0a1e03b8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/exc.py
@@ -0,0 +1,23 @@
+"""Exceptions used with python-pptx.
+
+The base exception class is PythonPptxError.
+"""
+
+from __future__ import annotations
+
+
+class PythonPptxError(Exception):
+ """Generic error class."""
+
+
+class PackageNotFoundError(PythonPptxError):
+ """
+ Raised when a package cannot be found at the specified path.
+ """
+
+
+class InvalidXmlError(PythonPptxError):
+ """
+ Raised when a value is encountered in the XML that is not valid according
+ to the schema.
+ """
diff --git a/.venv/lib/python3.12/site-packages/pptx/media.py b/.venv/lib/python3.12/site-packages/pptx/media.py
new file mode 100644
index 00000000..7aaf47ca
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/media.py
@@ -0,0 +1,197 @@
+"""Objects related to images, audio, and video."""
+
+from __future__ import annotations
+
+import base64
+import hashlib
+import os
+from typing import IO
+
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.util import lazyproperty
+
+
+class Video(object):
+ """Immutable value object representing a video such as MP4."""
+
+ def __init__(self, blob: bytes, mime_type: str | None, filename: str | None):
+ super(Video, self).__init__()
+ self._blob = blob
+ self._mime_type = mime_type
+ self._filename = filename
+
+ @classmethod
+ def from_blob(cls, blob: bytes, mime_type: str | None, filename: str | None = None):
+ """Return a new |Video| object loaded from image binary in *blob*."""
+ return cls(blob, mime_type, filename)
+
+ @classmethod
+ def from_path_or_file_like(cls, movie_file: str | IO[bytes], mime_type: str | None) -> Video:
+ """Return a new |Video| object containing video in *movie_file*.
+
+ *movie_file* can be either a path (string) or a file-like
+ (e.g. StringIO) object.
+ """
+ if isinstance(movie_file, str):
+ # treat movie_file as a path
+ with open(movie_file, "rb") as f:
+ blob = f.read()
+ filename = os.path.basename(movie_file)
+ else:
+ # assume movie_file is a file-like object
+ blob = movie_file.read()
+ filename = None
+
+ return cls.from_blob(blob, mime_type, filename)
+
+ @property
+ def blob(self):
+ """The bytestream of the media "file"."""
+ return self._blob
+
+ @property
+ def content_type(self):
+ """MIME-type of this media, e.g. `'video/mp4'`."""
+ return self._mime_type
+
+ @property
+ def ext(self):
+ """Return the file extension for this video, e.g. 'mp4'.
+
+ The extension is that from the actual filename if known. Otherwise
+ it is the lowercase canonical extension for the video's MIME type.
+ 'vid' is used if the MIME type is 'video/unknown'.
+ """
+ if self._filename:
+ return os.path.splitext(self._filename)[1].lstrip(".")
+ return {
+ CT.ASF: "asf",
+ CT.AVI: "avi",
+ CT.MOV: "mov",
+ CT.MP4: "mp4",
+ CT.MPG: "mpg",
+ CT.MS_VIDEO: "avi",
+ CT.SWF: "swf",
+ CT.WMV: "wmv",
+ CT.X_MS_VIDEO: "avi",
+ }.get(self._mime_type, "vid")
+
+ @property
+ def filename(self) -> str:
+ """Return a filename.ext string appropriate to this video.
+
+ The base filename from the original path is used if this image was
+ loaded from the filesystem. If no filename is available, such as when
+ the video object is created from an in-memory stream, the string
+ 'movie.{ext}' is used where 'ext' is suitable to the video format,
+ such as 'mp4'.
+ """
+ if self._filename is not None:
+ return self._filename
+ return "movie.%s" % self.ext
+
+ @lazyproperty
+ def sha1(self):
+ """The SHA1 hash digest for the binary "file" of this video.
+
+ Example: `'1be010ea47803b00e140b852765cdf84f491da47'`
+ """
+ return hashlib.sha1(self._blob).hexdigest()
+
+
+SPEAKER_IMAGE_BYTES = base64.b64decode(
+ "iVBORw0KGgoAAAANSUhEUgAAAHgAAAA3CAYAAADHao5rAAAACXBIWXMAAAsTAAALEwEAmpw"
+ "YAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUh"
+ "UIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74"
+ "Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz"
+ "/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEB"
+ "GAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVo"
+ "pFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8"
+ "lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wA"
+ "AKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qI"
+ "l7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X"
+ "48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5Em"
+ "ozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgD"
+ "gGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/x"
+ "gNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKL"
+ "yBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h"
+ "1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP"
+ "2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0I"
+ "gYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iE"
+ "PENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG"
+ "+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1"
+ "mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAc"
+ "YZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81"
+ "XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgs"
+ "V/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx"
+ "+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5"
+ "Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+h"
+ "x9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGj"
+ "UYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb"
+ "15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZ"
+ "nw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFD"
+ "pWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbx"
+ "t3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvf"
+ "rH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+"
+ "F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrS"
+ "FoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6R"
+ "JZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3i"
+ "C+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtG"
+ "I2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQq"
+ "ohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKO"
+ "ZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2"
+ "Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhT"
+ "bF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319k"
+ "XbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/"
+ "T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr"
+ "60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRpt"
+ "TmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752"
+ "PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca"
+ "7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf"
+ "9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L"
+ "96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV"
+ "70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAe"
+ "iUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAACJ5JREFUeNrsm19oW9cdx7/n"
+ "/tO9uleS9ceWZMdO68Su0jiEmNCGLVsDg+J16xhkYx0UM1ayvS19SQZ7WfK6PWR7Gax0S9n"
+ "24JGFlbGNPIRgEkKWZC1u6rizHMXxv1iWJdnWvbqS7p9z9uDokqzs1bWd84GDQEigez465/"
+ "x+v3MOYYyBs3shXDAXzOGCOVwwhwvmcMEcLpjDBXPBHC6YwwVzuOAt4dGjRy/MzMwMmqZpS"
+ "JLkZbPZYn9//8NkMlnmgncwt27d+tKlS5e+W6lUEolEYjQUCoExBsuy4Lru+319fXMjIyNX"
+ "jh49+m8ueAfRarXUCxcuvHvv3r3DR44ceavRaMCyLDDGIAgCFEVBOBwGIQSFQuH9PXv2PD5"
+ "16tRvu7u7H3PB2xzXdZXz58//vFKp/Gzfvn1YXFyEYRgwDAOSJEEQBDDG4LouPM+DpmnwPA"
+ "/5fP73o6Ojf3zttdfGueBtzAcffPCD8fHxi0NDQ1hZWUE6nYYoiiCEQBCEZ14JIfA8D77vA"
+ "wAmJib+cPLkyctvvvnm33ZLf0i7Se7s7Gz/tWvXvpbL5bC+vo5sNgtFUTb/yU/EthshBAAg"
+ "yzIAoNls4tChQ6OXL1+GKIreG2+88U8ueJsxPj5+QlXVt0OhEGRZhqIoEEXxGbFPC6aUBuJ"
+ "VVYVlWThw4MDo2NiYkMlkisPDwx/v9D4Rdotc0zSj9+7dO5RMJgEA4XAYkiRBkqRA9tNNlu"
+ "VArKIokCQJ8Xgc8Xgc+/fvf/u999778fr6egcXvE0ol8spx3HeNQwDoihClmXIsgxRFCFJ0"
+ "ucEE0KgqipCodAz0pPJJFKpFGKx2I8uXrz4Qy54m1CpVBK+70MUxUDW0yO1Lbz9XnuUPz26"
+ "25/JZDLo7OzExMTE4cnJySG+Bm8DarVa1Pd9EEIgy3IwPYuiCFEANFWEIGw2x6PQNA2qqqK"
+ "dRTDGwBgDpTSQXKvVRj/88MOZoaGhSS74C8a27XA75ZEk6YloBYoswHQNfPy4Fx4JoTdmYj"
+ "BRhSozsP+ZwCil8DwPlFIkk0kkEglMTEwM5PP5wcHBwTwX/AXhOI5i23bY930wxqAoymbVi"
+ "jA0/DD+mj8C048iqjMUHYbFRg0n+uaQ0FzQJ5IdxwGlNFi/AaCzsxPpdHr0+vXrN3aq4F2x"
+ "Bs/Pz/eFQqE/t0egIAjQdR2SQDFVzmKlHoEme1BEH3uyDJFsHFdnX4SLECRRhOM4EAQBhmF"
+ "A1/UgvUomk4jH4/j0008PeZ4nccFbTKPRCH/00UdHi8Viph1gNZtNUEqhKAp0XUfRNMAYQC"
+ "ng+0DNAhp1H4s1A3fmkwhrMmKxGGKxGFRVDdZvWZYRiUQQjUZRq9V+Mjc39wIXvIWsra0lx"
+ "sfHTzx48OAuY+yGKIpQVRX1eh2O4wQBFmMUjsvQ8oCWC5RWGWZmKVyX4e58DLarIBzWnomy"
+ "23mxoiiIRqPQdR2FQqGfr8FbhOu6SrFYzJim+Y9mswnHcSCKImKxGFZWVmBZFgRBgCQTpPU"
+ "6LNOCquhgjMERAUIAnzJUWgrmqzJeTclouRSUUrQj8XbQZRgGwuEwFhYW+vgI3iJarZbS2d"
+ "l5v1Qqwfd9uK4LAEgmk2g0GlhfX99cV0UFL2dNCLSGmtlEvUFg2oBpA5YNmHWCikmgKNLnc"
+ "uH2VN2udFWr1QQXvEV4nicxxlCv19FsNoOtv0QiAUVRsLq6ikqlAtel2JvycOxFCyulFZh1"
+ "CqtBNuXaBPV6EyHRhyAIaLVaQST99MZEWzQPsrZ2BKu2bYMQgmq1Cs/z4DgOwuEwMpkMSqU"
+ "SlpeXwQBoegTfe9XEgayDhaUHWNuoo1YHKus1hEgVuR4GLRxFPB6HrusQRRGU0uBwQLsAsl"
+ "O3VXdskNVsNtHR0YHl5WU0Gg24rgvGGLLZLCilWFpawuLiIggR0d1l4KffMPH6yz5a5iNUV"
+ "qcR8ot4+9UKcv0pCOLmlBwKhaBpGjRNC1Kl9p9H07QmD7K2CFVVm2tra0in07AsCysrK0il"
+ "UnBdF5FIBD09PVhYWMDDhw+RyWTQ07MH/X0qznyripNFhnKNYU8SeOVwL6Idm9+jdFNm+yB"
+ "AO9CybRumaeLw4cOPueAtIhKJ1Hzf/4phGDe6urowOTmJY8eOBbXk9jRdKBQQiUTQ0dGBeD"
+ "yOSCyB4z0aZEmErKhoNDfX7v83BXueh2q1ilKpBF7J2sofLQi0p6dn0bZtDAwMYGNjA1NTU"
+ "/B9H61WC4qioK+vDxsbG5iZmcHdu3dh2zZEUUTL8QFBAYgIQSAolUrBsR1KadCe5NpYXl6G"
+ "qqp/yuVy/+GCt5C9e/fOy7L8dVmWkcvlMD09jc8++yxYM3VdR29vL+bn53H//n3cunULtm0"
+ "HGwqu6wY169XVVTiOE4hmjKHRaGB2dhaFQgHHjx+/EQ6H7Z3YT+K5c+d2pGBCCOvu7n48NT"
+ "X1gFL6bcMwUCgUUCqVgkpUu/Q4NzeHZrOJVquFSCQCwzCCIzuSJMH3fViWBUII2ulXoVDAn"
+ "Tt3UC6X/3L27NlfaJrW4IK3GFmW3ZdeeilvWda/HMe5u2/fvhFFUWCa5m/279///vDw8K8y"
+ "mczfCSH56enpr5bL5UC0IAhPSpksCKparRbK5TJmZmZw+/ZtfPLJJ1fPnDnzy506PQO76Nh"
+ "stVpNFIvFDGNMSKfTxVQqVX66tHnp0qXvjI2NfZ8Q8s3e3l709/ejq6sL0WgUoVAIruuiVq"
+ "thaWkJ09PTWFpaunL69Olfj4yMXNnJ/fJcXT7L5/ODY2Njb928efPLoii+3t5IkGUZnufBN"
+ "E2sra1dHR4e/vidd9753cDAQH6nP/Nzebtwdna2//bt269MTk4eKpVKXZ7nSbquW7lcbvrE"
+ "iRPjBw8enNwtz/rcXx9ljAlPypJ0Nz4fvx+8y+GCuWAOF8zhgjlcMIcL5nDBHC6YC+ZwwRw"
+ "umLMN+O8AX65uqCMleo4AAAAASUVORK5CYII="
+)
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/__init__.py b/.venv/lib/python3.12/site-packages/pptx/opc/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/opc/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/constants.py b/.venv/lib/python3.12/site-packages/pptx/opc/constants.py
new file mode 100644
index 00000000..e1b08a93
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/opc/constants.py
@@ -0,0 +1,331 @@
+"""Constant values related to the Open Packaging Convention.
+
+In particular, this includes content (MIME) types and relationship types.
+"""
+
+from __future__ import annotations
+
+
+class CONTENT_TYPE:
+ """Content type URIs (like MIME-types) that specify a part's format."""
+
+ ASF = "video/x-ms-asf"
+ AVI = "video/avi"
+ BMP = "image/bmp"
+ DML_CHART = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
+ DML_CHARTSHAPES = "application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml"
+ DML_DIAGRAM_COLORS = "application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml"
+ DML_DIAGRAM_DATA = "application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml"
+ DML_DIAGRAM_DRAWING = "application/vnd.ms-office.drawingml.diagramDrawing+xml"
+ DML_DIAGRAM_LAYOUT = "application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml"
+ DML_DIAGRAM_STYLE = "application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml"
+ GIF = "image/gif"
+ INK = "application/inkml+xml"
+ JPEG = "image/jpeg"
+ MOV = "video/quicktime"
+ MP4 = "video/mp4"
+ MPG = "video/mpeg"
+ MS_PHOTO = "image/vnd.ms-photo"
+ MS_VIDEO = "video/msvideo"
+ OFC_CHART_COLORS = "application/vnd.ms-office.chartcolorstyle+xml"
+ OFC_CHART_EX = "application/vnd.ms-office.chartex+xml"
+ OFC_CHART_STYLE = "application/vnd.ms-office.chartstyle+xml"
+ OFC_CUSTOM_PROPERTIES = "application/vnd.openxmlformats-officedocument.custom-properties+xml"
+ OFC_CUSTOM_XML_PROPERTIES = (
+ "application/vnd.openxmlformats-officedocument.customXmlProperties+xml"
+ )
+ OFC_DRAWING = "application/vnd.openxmlformats-officedocument.drawing+xml"
+ OFC_EXTENDED_PROPERTIES = (
+ "application/vnd.openxmlformats-officedocument.extended-properties+xml"
+ )
+ OFC_OLE_OBJECT = "application/vnd.openxmlformats-officedocument.oleObject"
+ OFC_PACKAGE = "application/vnd.openxmlformats-officedocument.package"
+ OFC_THEME = "application/vnd.openxmlformats-officedocument.theme+xml"
+ OFC_THEME_OVERRIDE = "application/vnd.openxmlformats-officedocument.themeOverride+xml"
+ OFC_VML_DRAWING = "application/vnd.openxmlformats-officedocument.vmlDrawing"
+ OPC_CORE_PROPERTIES = "application/vnd.openxmlformats-package.core-properties+xml"
+ OPC_DIGITAL_SIGNATURE_CERTIFICATE = (
+ "application/vnd.openxmlformats-package.digital-signature-certificate"
+ )
+ OPC_DIGITAL_SIGNATURE_ORIGIN = "application/vnd.openxmlformats-package.digital-signature-origin"
+ OPC_DIGITAL_SIGNATURE_XMLSIGNATURE = (
+ "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml"
+ )
+ OPC_RELATIONSHIPS = "application/vnd.openxmlformats-package.relationships+xml"
+ PML_COMMENTS = "application/vnd.openxmlformats-officedocument.presentationml.comments+xml"
+ PML_COMMENT_AUTHORS = (
+ "application/vnd.openxmlformats-officedocument.presentationml.commentAuthors+xml"
+ )
+ PML_HANDOUT_MASTER = (
+ "application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml"
+ )
+ PML_NOTES_MASTER = (
+ "application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml"
+ )
+ PML_NOTES_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml"
+ PML_PRESENTATION = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
+ PML_PRESENTATION_MAIN = (
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"
+ )
+ PML_PRES_MACRO_MAIN = "application/vnd.ms-powerpoint.presentation.macroEnabled.main+xml"
+ PML_PRES_PROPS = "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml"
+ PML_PRINTER_SETTINGS = (
+ "application/vnd.openxmlformats-officedocument.presentationml.printerSettings"
+ )
+ PML_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml"
+ PML_SLIDESHOW_MAIN = (
+ "application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml"
+ )
+ PML_SLIDE_LAYOUT = (
+ "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"
+ )
+ PML_SLIDE_MASTER = (
+ "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"
+ )
+ PML_SLIDE_UPDATE_INFO = (
+ "application/vnd.openxmlformats-officedocument.presentationml.slideUpdateInfo+xml"
+ )
+ PML_TABLE_STYLES = (
+ "application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml"
+ )
+ PML_TAGS = "application/vnd.openxmlformats-officedocument.presentationml.tags+xml"
+ PML_TEMPLATE_MAIN = (
+ "application/vnd.openxmlformats-officedocument.presentationml.template.main+xml"
+ )
+ PML_VIEW_PROPS = "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml"
+ PNG = "image/png"
+ SML_CALC_CHAIN = "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml"
+ SML_CHARTSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"
+ SML_COMMENTS = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"
+ SML_CONNECTIONS = "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml"
+ SML_CUSTOM_PROPERTY = (
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty"
+ )
+ SML_DIALOGSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml"
+ SML_EXTERNAL_LINK = (
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml"
+ )
+ SML_PIVOT_CACHE_DEFINITION = (
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"
+ )
+ SML_PIVOT_CACHE_RECORDS = (
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"
+ )
+ SML_PIVOT_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
+ SML_PRINTER_SETTINGS = (
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings"
+ )
+ SML_QUERY_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml"
+ SML_REVISION_HEADERS = (
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+xml"
+ )
+ SML_REVISION_LOG = "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml"
+ SML_SHARED_STRINGS = (
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"
+ )
+ SML_SHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+ SML_SHEET_MAIN = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
+ SML_SHEET_METADATA = (
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml"
+ )
+ SML_STYLES = "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"
+ SML_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"
+ SML_TABLE_SINGLE_CELLS = (
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.tableSingleCells+xml"
+ )
+ SML_TEMPLATE_MAIN = (
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml"
+ )
+ SML_USER_NAMES = "application/vnd.openxmlformats-officedocument.spreadsheetml.userNames+xml"
+ SML_VOLATILE_DEPENDENCIES = (
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.volatileDependencies+xml"
+ )
+ SML_WORKSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
+ SWF = "application/x-shockwave-flash"
+ TIFF = "image/tiff"
+ VIDEO = "video/unknown"
+ WML_COMMENTS = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml"
+ WML_DOCUMENT = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+ WML_DOCUMENT_GLOSSARY = (
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml"
+ )
+ WML_DOCUMENT_MAIN = (
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"
+ )
+ WML_ENDNOTES = "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml"
+ WML_FONT_TABLE = "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml"
+ WML_FOOTER = "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"
+ WML_FOOTNOTES = "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml"
+ WML_HEADER = "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"
+ WML_NUMBERING = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"
+ WML_PRINTER_SETTINGS = (
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.printerSettings"
+ )
+ WML_SETTINGS = "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"
+ WML_STYLES = "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"
+ WML_WEB_SETTINGS = (
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml"
+ )
+ WMV = "video/x-ms-wmv"
+ XML = "application/xml"
+ X_EMF = "image/x-emf"
+ X_FONTDATA = "application/x-fontdata"
+ X_FONT_TTF = "application/x-font-ttf"
+ X_MS_VIDEO = "video/x-msvideo"
+ X_WMF = "image/x-wmf"
+
+
+class NAMESPACE:
+ """Constant values for OPC XML namespaces"""
+
+ DML_WORDPROCESSING_DRAWING = (
+ "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
+ )
+ OFC_RELATIONSHIPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
+ OPC_RELATIONSHIPS = "http://schemas.openxmlformats.org/package/2006/relationships"
+ OPC_CONTENT_TYPES = "http://schemas.openxmlformats.org/package/2006/content-types"
+ WML_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
+
+
+class RELATIONSHIP_TARGET_MODE:
+ """Open XML relationship target modes"""
+
+ EXTERNAL = "External"
+ INTERNAL = "Internal"
+
+
+class RELATIONSHIP_TYPE:
+ AUDIO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio"
+ A_F_CHUNK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk"
+ CALC_CHAIN = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain"
+ CERTIFICATE = (
+ "http://schemas.openxmlformats.org/package/2006/relationships/digital-signatu"
+ "re/certificate"
+ )
+ CHART = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"
+ CHARTSHEET = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet"
+ CHART_COLOR_STYLE = "http://schemas.microsoft.com/office/2011/relationships/chartColorStyle"
+ CHART_USER_SHAPES = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartUserShapes"
+ )
+ COMMENTS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
+ COMMENT_AUTHORS = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentAuthors"
+ )
+ CONNECTIONS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/connections"
+ CONTROL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/control"
+ CORE_PROPERTIES = (
+ "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties"
+ )
+ CUSTOM_PROPERTIES = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties"
+ )
+ CUSTOM_PROPERTY = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customProperty"
+ )
+ CUSTOM_XML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml"
+ CUSTOM_XML_PROPS = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXmlProps"
+ )
+ DIAGRAM_COLORS = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramColors"
+ )
+ DIAGRAM_DATA = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData"
+ DIAGRAM_LAYOUT = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout"
+ )
+ DIAGRAM_QUICK_STYLE = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle"
+ )
+ DIALOGSHEET = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet"
+ DRAWING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
+ ENDNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes"
+ EXTENDED_PROPERTIES = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"
+ )
+ EXTERNAL_LINK = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLink"
+ )
+ FONT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font"
+ FONT_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable"
+ FOOTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
+ FOOTNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes"
+ GLOSSARY_DOCUMENT = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/glossaryDocument"
+ )
+ HANDOUT_MASTER = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/handoutMaster"
+ )
+ HEADER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header"
+ HYPERLINK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
+ IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
+ MEDIA = "http://schemas.microsoft.com/office/2007/relationships/media"
+ NOTES_MASTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster"
+ NOTES_SLIDE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide"
+ NUMBERING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering"
+ OFFICE_DOCUMENT = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
+ )
+ OLE_OBJECT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject"
+ ORIGIN = "http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin"
+ PACKAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/package"
+ PIVOT_CACHE_DEFINITION = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCac"
+ "heDefinition"
+ )
+ PIVOT_CACHE_RECORDS = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/spreadsh"
+ "eetml/pivotCacheRecords"
+ )
+ PIVOT_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable"
+ PRES_PROPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps"
+ PRINTER_SETTINGS = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings"
+ )
+ QUERY_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/queryTable"
+ REVISION_HEADERS = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionHeaders"
+ )
+ REVISION_LOG = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionLog"
+ SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings"
+ SHARED_STRINGS = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
+ )
+ SHEET_METADATA = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata"
+ )
+ SIGNATURE = (
+ "http://schemas.openxmlformats.org/package/2006/relationships/digital-signatu"
+ "re/signature"
+ )
+ SLIDE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"
+ SLIDE_LAYOUT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout"
+ SLIDE_MASTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster"
+ SLIDE_UPDATE_INFO = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideUpdateInfo"
+ )
+ STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"
+ TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table"
+ TABLE_SINGLE_CELLS = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableSingleCells"
+ )
+ TABLE_STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles"
+ TAGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags"
+ THEME = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"
+ THEME_OVERRIDE = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/themeOverride"
+ )
+ THUMBNAIL = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail"
+ USERNAMES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/usernames"
+ VIDEO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/video"
+ VIEW_PROPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps"
+ VML_DRAWING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"
+ VOLATILE_DEPENDENCIES = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/volatile"
+ "Dependencies"
+ )
+ WEB_SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings"
+ WORKSHEET_SOURCE = (
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheetSource"
+ )
+ XML_MAPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps"
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/oxml.py b/.venv/lib/python3.12/site-packages/pptx/opc/oxml.py
new file mode 100644
index 00000000..5dd902a5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/opc/oxml.py
@@ -0,0 +1,188 @@
+"""OPC-local oxml module to handle OPC-local concerns like relationship parsing."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, cast
+
+from lxml import etree
+
+from pptx.opc.constants import NAMESPACE as NS
+from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM
+from pptx.oxml import parse_xml, register_element_cls
+from pptx.oxml.simpletypes import (
+ ST_ContentType,
+ ST_Extension,
+ ST_TargetMode,
+ XsdAnyUri,
+ XsdId,
+)
+from pptx.oxml.xmlchemy import (
+ BaseOxmlElement,
+ OptionalAttribute,
+ RequiredAttribute,
+ ZeroOrMore,
+)
+
+if TYPE_CHECKING:
+ from pptx.opc.packuri import PackURI
+
+nsmap = {
+ "ct": NS.OPC_CONTENT_TYPES,
+ "pr": NS.OPC_RELATIONSHIPS,
+ "r": NS.OFC_RELATIONSHIPS,
+}
+
+
+def oxml_to_encoded_bytes(
+ element: BaseOxmlElement,
+ encoding: str = "utf-8",
+ pretty_print: bool = False,
+ standalone: bool | None = None,
+) -> bytes:
+ return etree.tostring(
+ element, encoding=encoding, pretty_print=pretty_print, standalone=standalone
+ )
+
+
+def oxml_tostring(
+ elm: BaseOxmlElement,
+ encoding: str | None = None,
+ pretty_print: bool = False,
+ standalone: bool | None = None,
+):
+ return etree.tostring(elm, encoding=encoding, pretty_print=pretty_print, standalone=standalone)
+
+
+def serialize_part_xml(part_elm: BaseOxmlElement) -> bytes:
+ """Produce XML-file bytes for `part_elm`, suitable for writing directly to a `.xml` file.
+
+ Includes XML-declaration header.
+ """
+ return etree.tostring(part_elm, encoding="UTF-8", standalone=True)
+
+
+class CT_Default(BaseOxmlElement):
+ """`<Default>` element.
+
+ Specifies the default content type to be applied to a part with the specified extension.
+ """
+
+ extension: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
+ "Extension", ST_Extension
+ )
+ contentType: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
+ "ContentType", ST_ContentType
+ )
+
+
+class CT_Override(BaseOxmlElement):
+ """`<Override>` element.
+
+ Specifies the content type to be applied for a part with the specified partname.
+ """
+
+ partName: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
+ "PartName", XsdAnyUri
+ )
+ contentType: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
+ "ContentType", ST_ContentType
+ )
+
+
+class CT_Relationship(BaseOxmlElement):
+ """`<Relationship>` element.
+
+ Represents a single relationship from a source to a target part.
+ """
+
+ rId: str = RequiredAttribute("Id", XsdId) # pyright: ignore[reportAssignmentType]
+ reltype: str = RequiredAttribute("Type", XsdAnyUri) # pyright: ignore[reportAssignmentType]
+ target_ref: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
+ "Target", XsdAnyUri
+ )
+ targetMode: str = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "TargetMode", ST_TargetMode, default=RTM.INTERNAL
+ )
+
+ @classmethod
+ def new(
+ cls, rId: str, reltype: str, target_ref: str, target_mode: str = RTM.INTERNAL
+ ) -> CT_Relationship:
+ """Return a new `<Relationship>` element.
+
+ `target_ref` is either a partname or a URI.
+ """
+ relationship = cast(CT_Relationship, parse_xml(f'<Relationship xmlns="{nsmap["pr"]}"/>'))
+ relationship.rId = rId
+ relationship.reltype = reltype
+ relationship.target_ref = target_ref
+ relationship.targetMode = target_mode
+ return relationship
+
+
+class CT_Relationships(BaseOxmlElement):
+ """`<Relationships>` element, the root element in a .rels file."""
+
+ relationship_lst: list[CT_Relationship]
+ _insert_relationship: Callable[[CT_Relationship], CT_Relationship]
+
+ relationship = ZeroOrMore("pr:Relationship")
+
+ def add_rel(
+ self, rId: str, reltype: str, target: str, is_external: bool = False
+ ) -> CT_Relationship:
+ """Add a child `<Relationship>` element with attributes set as specified."""
+ target_mode = RTM.EXTERNAL if is_external else RTM.INTERNAL
+ relationship = CT_Relationship.new(rId, reltype, target, target_mode)
+ return self._insert_relationship(relationship)
+
+ @classmethod
+ def new(cls) -> CT_Relationships:
+ """Return a new `<Relationships>` element."""
+ return cast(CT_Relationships, parse_xml(f'<Relationships xmlns="{nsmap["pr"]}"/>'))
+
+ @property
+ def xml_file_bytes(self) -> bytes:
+ """Return XML bytes, with XML-declaration, for this `<Relationships>` element.
+
+ Suitable for saving in a .rels stream, not pretty printed and with an XML declaration at
+ the top.
+ """
+ return oxml_to_encoded_bytes(self, encoding="UTF-8", standalone=True)
+
+
+class CT_Types(BaseOxmlElement):
+ """`<Types>` element.
+
+ The container element for Default and Override elements in [Content_Types].xml.
+ """
+
+ default_lst: list[CT_Default]
+ override_lst: list[CT_Override]
+
+ _add_default: Callable[..., CT_Default]
+ _add_override: Callable[..., CT_Override]
+
+ default = ZeroOrMore("ct:Default")
+ override = ZeroOrMore("ct:Override")
+
+ def add_default(self, ext: str, content_type: str) -> CT_Default:
+ """Add a child `<Default>` element with attributes set to parameter values."""
+ return self._add_default(extension=ext, contentType=content_type)
+
+ def add_override(self, partname: PackURI, content_type: str) -> CT_Override:
+ """Add a child `<Override>` element with attributes set to parameter values."""
+ return self._add_override(partName=partname, contentType=content_type)
+
+ @classmethod
+ def new(cls) -> CT_Types:
+ """Return a new `<Types>` element."""
+ return cast(CT_Types, parse_xml(f'<Types xmlns="{nsmap["ct"]}"/>'))
+
+
+register_element_cls("ct:Default", CT_Default)
+register_element_cls("ct:Override", CT_Override)
+register_element_cls("ct:Types", CT_Types)
+
+register_element_cls("pr:Relationship", CT_Relationship)
+register_element_cls("pr:Relationships", CT_Relationships)
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/package.py b/.venv/lib/python3.12/site-packages/pptx/opc/package.py
new file mode 100644
index 00000000..713759c5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/opc/package.py
@@ -0,0 +1,762 @@
+"""Fundamental Open Packaging Convention (OPC) objects.
+
+The :mod:`pptx.packaging` module coheres around the concerns of reading and writing
+presentations to and from a .pptx file.
+"""
+
+from __future__ import annotations
+
+import collections
+from typing import IO, TYPE_CHECKING, DefaultDict, Iterator, Mapping, Set, cast
+
+from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.opc.oxml import CT_Relationships, serialize_part_xml
+from pptx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI
+from pptx.opc.serialized import PackageReader, PackageWriter
+from pptx.opc.shared import CaseInsensitiveDict
+from pptx.oxml import parse_xml
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+ from typing_extensions import Self
+
+ from pptx.opc.oxml import CT_Relationship, CT_Types
+ from pptx.oxml.xmlchemy import BaseOxmlElement
+ from pptx.package import Package
+ from pptx.parts.presentation import PresentationPart
+
+
+class _RelatableMixin:
+ """Provide relationship methods required by both the package and each part."""
+
+ def part_related_by(self, reltype: str) -> Part:
+ """Return (single) part having relationship to this package of `reltype`.
+
+ Raises |KeyError| if no such relationship is found and |ValueError| if more than one such
+ relationship is found.
+ """
+ return self._rels.part_with_reltype(reltype)
+
+ def relate_to(self, target: Part | str, reltype: str, is_external: bool = False) -> str:
+ """Return rId key of relationship of `reltype` to `target`.
+
+ If such a relationship already exists, its rId is returned. Otherwise the relationship is
+ added and its new rId returned.
+ """
+ if isinstance(target, str):
+ assert is_external
+ return self._rels.get_or_add_ext_rel(reltype, target)
+
+ return self._rels.get_or_add(reltype, target)
+
+ def related_part(self, rId: str) -> Part:
+ """Return related |Part| subtype identified by `rId`."""
+ return self._rels[rId].target_part
+
+ def target_ref(self, rId: str) -> str:
+ """Return URL contained in target ref of relationship identified by `rId`."""
+ return self._rels[rId].target_ref
+
+ @lazyproperty
+ def _rels(self) -> _Relationships:
+ """|_Relationships| object containing relationships from this part to others."""
+ raise NotImplementedError( # pragma: no cover
+ "`%s` must implement `.rels`" % type(self).__name__
+ )
+
+
+class OpcPackage(_RelatableMixin):
+ """Main API class for |python-opc|.
+
+ A new instance is constructed by calling the :meth:`open` classmethod with a path to a package
+ file or file-like object containing a package (.pptx file).
+ """
+
+ def __init__(self, pkg_file: str | IO[bytes]):
+ self._pkg_file = pkg_file
+
+ @classmethod
+ def open(cls, pkg_file: str | IO[bytes]) -> Self:
+ """Return an |OpcPackage| instance loaded with the contents of `pkg_file`."""
+ return cls(pkg_file)._load()
+
+ def drop_rel(self, rId: str) -> None:
+ """Remove relationship identified by `rId`."""
+ self._rels.pop(rId)
+
+ def iter_parts(self) -> Iterator[Part]:
+ """Generate exactly one reference to each part in the package."""
+ visited: Set[Part] = set()
+ for rel in self.iter_rels():
+ if rel.is_external:
+ continue
+ part = rel.target_part
+ if part in visited:
+ continue
+ yield part
+ visited.add(part)
+
+ def iter_rels(self) -> Iterator[_Relationship]:
+ """Generate exactly one reference to each relationship in package.
+
+ Performs a depth-first traversal of the rels graph.
+ """
+ visited: Set[Part] = set()
+
+ def walk_rels(rels: _Relationships) -> Iterator[_Relationship]:
+ for rel in rels.values():
+ yield rel
+ # --- external items can have no relationships ---
+ if rel.is_external:
+ continue
+ # -- all relationships other than those for the package belong to a part. Once
+ # -- that part has been processed, processing it again would lead to the same
+ # -- relationships appearing more than once.
+ part = rel.target_part
+ if part in visited:
+ continue
+ visited.add(part)
+ # --- recurse into relationships of each unvisited target-part ---
+ yield from walk_rels(part.rels)
+
+ yield from walk_rels(self._rels)
+
+ @property
+ def main_document_part(self) -> PresentationPart:
+ """Return |Part| subtype serving as the main document part for this package.
+
+ In this case it will be a |Presentation| part.
+ """
+ return cast("PresentationPart", self.part_related_by(RT.OFFICE_DOCUMENT))
+
+ def next_partname(self, tmpl: str) -> PackURI:
+ """Return |PackURI| next available partname matching `tmpl`.
+
+ `tmpl` is a printf (%)-style template string containing a single replacement item, a '%d'
+ to be used to insert the integer portion of the partname. Example:
+ '/ppt/slides/slide%d.xml'
+ """
+ # --- expected next partname is tmpl % n where n is one greater than the number
+ # --- of existing partnames that match tmpl. Speed up finding the next one
+ # --- (maybe) by searching from the end downward rather than from 1 upward.
+ prefix = tmpl[: (tmpl % 42).find("42")]
+ partnames = {p.partname for p in self.iter_parts() if p.partname.startswith(prefix)}
+ for n in range(len(partnames) + 1, 0, -1):
+ candidate_partname = tmpl % n
+ if candidate_partname not in partnames:
+ return PackURI(candidate_partname)
+ raise Exception("ProgrammingError: ran out of candidate_partnames") # pragma: no cover
+
+ def save(self, pkg_file: str | IO[bytes]) -> None:
+ """Save this package to `pkg_file`.
+
+ `file` can be either a path to a file (a string) or a file-like object.
+ """
+ PackageWriter.write(pkg_file, self._rels, tuple(self.iter_parts()))
+
+ def _load(self) -> Self:
+ """Return the package after loading all parts and relationships."""
+ pkg_xml_rels, parts = _PackageLoader.load(self._pkg_file, cast("Package", self))
+ self._rels.load_from_xml(PACKAGE_URI, pkg_xml_rels, parts)
+ return self
+
+ @lazyproperty
+ def _rels(self) -> _Relationships:
+ """|Relationships| object containing relationships of this package."""
+ return _Relationships(PACKAGE_URI.baseURI)
+
+
+class _PackageLoader:
+ """Function-object that loads a package from disk (or other store)."""
+
+ def __init__(self, pkg_file: str | IO[bytes], package: Package):
+ self._pkg_file = pkg_file
+ self._package = package
+
+ @classmethod
+ def load(
+ cls, pkg_file: str | IO[bytes], package: Package
+ ) -> tuple[CT_Relationships, dict[PackURI, Part]]:
+ """Return (pkg_xml_rels, parts) pair resulting from loading `pkg_file`.
+
+ The returned `parts` value is a {partname: part} mapping with each part in the package
+ included and constructed complete with its relationships to other parts in the package.
+
+ The returned `pkg_xml_rels` value is a `CT_Relationships` object containing the parsed
+ package relationships. It is the caller's responsibility (the package object) to load
+ those relationships into its |_Relationships| object.
+ """
+ return cls(pkg_file, package)._load()
+
+ def _load(self) -> tuple[CT_Relationships, dict[PackURI, Part]]:
+ """Return (pkg_xml_rels, parts) pair resulting from loading pkg_file."""
+ parts, xml_rels = self._parts, self._xml_rels
+
+ for partname, part in parts.items():
+ part.load_rels_from_xml(xml_rels[partname], parts)
+
+ return xml_rels[PACKAGE_URI], parts
+
+ @lazyproperty
+ def _content_types(self) -> _ContentTypeMap:
+ """|_ContentTypeMap| object providing content-types for items of this package.
+
+ Provides a content-type (MIME-type) for any given partname.
+ """
+ return _ContentTypeMap.from_xml(self._package_reader[CONTENT_TYPES_URI])
+
+ @lazyproperty
+ def _package_reader(self) -> PackageReader:
+ """|PackageReader| object providing access to package-items in pkg_file."""
+ return PackageReader(self._pkg_file)
+
+ @lazyproperty
+ def _parts(self) -> dict[PackURI, Part]:
+ """dict {partname: Part} populated with parts loading from package.
+
+ Among other duties, this collection is passed to each relationships collection so each
+ relationship can resolve a reference to its target part when required. This reference can
+ only be reliably carried out once the all parts have been loaded.
+ """
+ content_types = self._content_types
+ package = self._package
+ package_reader = self._package_reader
+
+ return {
+ partname: PartFactory(
+ partname,
+ content_types[partname],
+ package,
+ blob=package_reader[partname],
+ )
+ for partname in (p for p in self._xml_rels if p != "/")
+ # -- invalid partnames can arise in some packages; ignore those rather than raise an
+ # -- exception.
+ if partname in package_reader
+ }
+
+ @lazyproperty
+ def _xml_rels(self) -> dict[PackURI, CT_Relationships]:
+ """dict {partname: xml_rels} for package and all package parts.
+
+ This is used as the basis for other loading operations such as loading parts and
+ populating their relationships.
+ """
+ xml_rels: dict[PackURI, CT_Relationships] = {}
+ visited_partnames: Set[PackURI] = set()
+
+ def load_rels(source_partname: PackURI, rels: CT_Relationships):
+ """Populate `xml_rels` dict by traversing relationships depth-first."""
+ xml_rels[source_partname] = rels
+ visited_partnames.add(source_partname)
+ base_uri = source_partname.baseURI
+
+ # --- recursion stops when there are no unvisited partnames in rels ---
+ for rel in rels.relationship_lst:
+ if rel.targetMode == RTM.EXTERNAL:
+ continue
+ target_partname = PackURI.from_rel_ref(base_uri, rel.target_ref)
+ if target_partname in visited_partnames:
+ continue
+ load_rels(target_partname, self._xml_rels_for(target_partname))
+
+ load_rels(PACKAGE_URI, self._xml_rels_for(PACKAGE_URI))
+ return xml_rels
+
+ def _xml_rels_for(self, partname: PackURI) -> CT_Relationships:
+ """Return CT_Relationships object formed by parsing rels XML for `partname`.
+
+ A CT_Relationships object is returned in all cases. A part that has no relationships
+ receives an "empty" CT_Relationships object, i.e. containing no `CT_Relationship` objects.
+ """
+ rels_xml = self._package_reader.rels_xml_for(partname)
+ return (
+ CT_Relationships.new()
+ if rels_xml is None
+ else cast(CT_Relationships, parse_xml(rels_xml))
+ )
+
+
+class Part(_RelatableMixin):
+ """Base class for package parts.
+
+ Provides common properties and methods, but intended to be subclassed in client code to
+ implement specific part behaviors. Also serves as the default class for parts that are not yet
+ given specific behaviors.
+ """
+
+ def __init__(
+ self, partname: PackURI, content_type: str, package: Package, blob: bytes | None = None
+ ):
+ # --- XmlPart subtypes, don't store a blob (the original XML) ---
+ self._partname = partname
+ self._content_type = content_type
+ self._package = package
+ self._blob = blob
+
+ @classmethod
+ def load(cls, partname: PackURI, content_type: str, package: Package, blob: bytes) -> Self:
+ """Return `cls` instance loaded from arguments.
+
+ This one is a straight pass-through, but subtypes may do some pre-processing, see XmlPart
+ for an example.
+ """
+ return cls(partname, content_type, package, blob)
+
+ @property
+ def blob(self) -> bytes:
+ """Contents of this package part as a sequence of bytes.
+
+ Intended to be overridden by subclasses. Default behavior is to return the blob initial
+ loaded during `Package.open()` operation.
+ """
+ return self._blob or b""
+
+ @blob.setter
+ def blob(self, blob: bytes):
+ """Note that not all subclasses use the part blob as their blob source.
+
+ In particular, the |XmlPart| subclass uses its `self._element` to serialize a blob on
+ demand. This works fine for binary parts though.
+ """
+ self._blob = blob
+
+ @lazyproperty
+ def content_type(self) -> str:
+ """Content-type (MIME-type) of this part."""
+ return self._content_type
+
+ def load_rels_from_xml(self, xml_rels: CT_Relationships, parts: dict[PackURI, Part]) -> None:
+ """load _Relationships for this part from `xml_rels`.
+
+ Part references are resolved using the `parts` dict that maps each partname to the loaded
+ part with that partname. These relationships are loaded from a serialized package and so
+ already have assigned rIds. This method is only used during package loading.
+ """
+ self._rels.load_from_xml(self._partname.baseURI, xml_rels, parts)
+
+ @lazyproperty
+ def package(self) -> Package:
+ """Package this part belongs to."""
+ return self._package
+
+ @property
+ def partname(self) -> PackURI:
+ """|PackURI| partname for this part, e.g. "/ppt/slides/slide1.xml"."""
+ return self._partname
+
+ @partname.setter
+ def partname(self, partname: PackURI):
+ if not isinstance(partname, PackURI): # pyright: ignore[reportUnnecessaryIsInstance]
+ raise TypeError( # pragma: no cover
+ "partname must be instance of PackURI, got '%s'" % type(partname).__name__
+ )
+ self._partname = partname
+
+ @lazyproperty
+ def rels(self) -> _Relationships:
+ """Collection of relationships from this part to other parts."""
+ # --- this must be public to allow the part graph to be traversed ---
+ return self._rels
+
+ def _blob_from_file(self, file: str | IO[bytes]) -> bytes:
+ """Return bytes of `file`, which is either a str path or a file-like object."""
+ # --- a str `file` is assumed to be a path ---
+ if isinstance(file, str):
+ with open(file, "rb") as f:
+ return f.read()
+
+ # --- otherwise, assume `file` is a file-like object
+ # --- reposition file cursor if it has one
+ if callable(getattr(file, "seek")):
+ file.seek(0)
+ return file.read()
+
+ @lazyproperty
+ def _rels(self) -> _Relationships:
+ """Relationships from this part to others."""
+ return _Relationships(self._partname.baseURI)
+
+
+class XmlPart(Part):
+ """Base class for package parts containing an XML payload, which is most of them.
+
+ Provides additional methods to the |Part| base class that take care of parsing and
+ reserializing the XML payload and managing relationships to other parts.
+ """
+
+ def __init__(
+ self, partname: PackURI, content_type: str, package: Package, element: BaseOxmlElement
+ ):
+ super(XmlPart, self).__init__(partname, content_type, package)
+ self._element = element
+
+ @classmethod
+ def load(cls, partname: PackURI, content_type: str, package: Package, blob: bytes):
+ """Return instance of `cls` loaded with parsed XML from `blob`."""
+ return cls(
+ partname, content_type, package, element=cast("BaseOxmlElement", parse_xml(blob))
+ )
+
+ @property
+ def blob(self) -> bytes: # pyright: ignore[reportIncompatibleMethodOverride]
+ """bytes XML serialization of this part."""
+ return serialize_part_xml(self._element)
+
+ # -- XmlPart cannot set its blob, which is why pyright complains --
+
+ def drop_rel(self, rId: str) -> None:
+ """Remove relationship identified by `rId` if its reference count is under 2.
+
+ Relationships with a reference count of 0 are implicit relationships. Note that only XML
+ parts can drop relationships.
+ """
+ if self._rel_ref_count(rId) < 2:
+ self._rels.pop(rId)
+
+ @property
+ def part(self):
+ """This part.
+
+ This is part of the parent protocol, "children" of the document will not know the part
+ that contains them so must ask their parent object. That chain of delegation ends here for
+ child objects.
+ """
+ return self
+
+ def _rel_ref_count(self, rId: str) -> int:
+ """Return int count of references in this part's XML to `rId`."""
+ return len([r for r in cast("list[str]", self._element.xpath("//@r:id")) if r == rId])
+
+
+class PartFactory:
+ """Constructs a registered subtype of |Part|.
+
+ Client code can register a subclass of |Part| to be used for a package blob based on its
+ content type.
+ """
+
+ part_type_for: dict[str, type[Part]] = {}
+
+ def __new__(cls, partname: PackURI, content_type: str, package: Package, blob: bytes) -> Part:
+ PartClass = cls._part_cls_for(content_type)
+ return PartClass.load(partname, content_type, package, blob)
+
+ @classmethod
+ def _part_cls_for(cls, content_type: str) -> type[Part]:
+ """Return the custom part class registered for `content_type`.
+
+ Returns |Part| if no custom class is registered for `content_type`.
+ """
+ if content_type in cls.part_type_for:
+ return cls.part_type_for[content_type]
+ return Part
+
+
+class _ContentTypeMap:
+ """Value type providing dict semantics for looking up content type by partname."""
+
+ def __init__(self, overrides: dict[str, str], defaults: dict[str, str]):
+ self._overrides = overrides
+ self._defaults = defaults
+
+ def __getitem__(self, partname: PackURI) -> str:
+ """Return content-type (MIME-type) for part identified by *partname*."""
+ if not isinstance(partname, PackURI): # pyright: ignore[reportUnnecessaryIsInstance]
+ raise TypeError(
+ "_ContentTypeMap key must be <type 'PackURI'>, got %s" % type(partname).__name__
+ )
+
+ if partname in self._overrides:
+ return self._overrides[partname]
+
+ if partname.ext in self._defaults:
+ return self._defaults[partname.ext]
+
+ raise KeyError("no content-type for partname '%s' in [Content_Types].xml" % partname)
+
+ @classmethod
+ def from_xml(cls, content_types_xml: bytes) -> _ContentTypeMap:
+ """Return |_ContentTypeMap| instance populated from `content_types_xml`."""
+ types_elm = cast("CT_Types", parse_xml(content_types_xml))
+ # -- note all partnames in [Content_Types].xml are absolute --
+ overrides = CaseInsensitiveDict(
+ (o.partName.lower(), o.contentType) for o in types_elm.override_lst
+ )
+ defaults = CaseInsensitiveDict(
+ (d.extension.lower(), d.contentType) for d in types_elm.default_lst
+ )
+ return cls(overrides, defaults)
+
+
+class _Relationships(Mapping[str, "_Relationship"]):
+ """Collection of |_Relationship| instances having `dict` semantics.
+
+ Relationships are keyed by their rId, but may also be found in other ways, such as by their
+ relationship type. |Relationship| objects are keyed by their rId.
+
+ Iterating this collection has normal mapping semantics, generating the keys (rIds) of the
+ mapping. `rels.keys()`, `rels.values()`, and `rels.items() can be used as they would be for a
+ `dict`.
+ """
+
+ def __init__(self, base_uri: str):
+ self._base_uri = base_uri
+
+ def __contains__(self, rId: object) -> bool:
+ """Implement 'in' operation, like `"rId7" in relationships`."""
+ return rId in self._rels
+
+ def __getitem__(self, rId: str) -> _Relationship:
+ """Implement relationship lookup by rId using indexed access, like rels[rId]."""
+ try:
+ return self._rels[rId]
+ except KeyError:
+ raise KeyError("no relationship with key '%s'" % rId)
+
+ def __iter__(self) -> Iterator[str]:
+ """Implement iteration of rIds (iterating a mapping produces its keys)."""
+ return iter(self._rels)
+
+ def __len__(self) -> int:
+ """Return count of relationships in collection."""
+ return len(self._rels)
+
+ def get_or_add(self, reltype: str, target_part: Part) -> str:
+ """Return str rId of `reltype` to `target_part`.
+
+ The rId of an existing matching relationship is used if present. Otherwise, a new
+ relationship is added and that rId is returned.
+ """
+ existing_rId = self._get_matching(reltype, target_part)
+ return (
+ self._add_relationship(reltype, target_part) if existing_rId is None else existing_rId
+ )
+
+ def get_or_add_ext_rel(self, reltype: str, target_ref: str) -> str:
+ """Return str rId of external relationship of `reltype` to `target_ref`.
+
+ The rId of an existing matching relationship is used if present. Otherwise, a new
+ relationship is added and that rId is returned.
+ """
+ existing_rId = self._get_matching(reltype, target_ref, is_external=True)
+ return (
+ self._add_relationship(reltype, target_ref, is_external=True)
+ if existing_rId is None
+ else existing_rId
+ )
+
+ def load_from_xml(
+ self, base_uri: str, xml_rels: CT_Relationships, parts: dict[PackURI, Part]
+ ) -> None:
+ """Replace any relationships in this collection with those from `xml_rels`."""
+
+ def iter_valid_rels():
+ """Filter out broken relationships such as those pointing to NULL."""
+ for rel_elm in xml_rels.relationship_lst:
+ # --- Occasionally a PowerPoint plugin or other client will "remove"
+ # --- a relationship simply by "voiding" its Target value, like making
+ # --- it "/ppt/slides/NULL". Skip any relationships linking to a
+ # --- partname that is not present in the package.
+ if rel_elm.targetMode == RTM.INTERNAL:
+ partname = PackURI.from_rel_ref(base_uri, rel_elm.target_ref)
+ if partname not in parts:
+ continue
+ yield _Relationship.from_xml(base_uri, rel_elm, parts)
+
+ self._rels.clear()
+ self._rels.update((rel.rId, rel) for rel in iter_valid_rels())
+
+ def part_with_reltype(self, reltype: str) -> Part:
+ """Return target part of relationship with matching `reltype`.
+
+ Raises |KeyError| if not found and |ValueError| if more than one matching relationship is
+ found.
+ """
+ rels_of_reltype = self._rels_by_reltype[reltype]
+
+ if len(rels_of_reltype) == 0:
+ raise KeyError("no relationship of type '%s' in collection" % reltype)
+
+ if len(rels_of_reltype) > 1:
+ raise ValueError("multiple relationships of type '%s' in collection" % reltype)
+
+ return rels_of_reltype[0].target_part
+
+ def pop(self, rId: str) -> _Relationship:
+ """Return |_Relationship| identified by `rId` after removing it from collection.
+
+ The caller is responsible for ensuring it is no longer required.
+ """
+ return self._rels.pop(rId)
+
+ @property
+ def xml(self):
+ """bytes XML serialization of this relationship collection.
+
+ This value is suitable for storage as a .rels file in an OPC package. Includes a `<?xml..`
+ declaration header with encoding as UTF-8.
+ """
+ rels_elm = CT_Relationships.new()
+
+ # -- Sequence <Relationship> elements deterministically (in numerical order) to
+ # -- simplify testing and manual inspection.
+ def iter_rels_in_numerical_order():
+ sorted_num_rId_pairs = sorted(
+ (
+ int(rId[3:]) if rId.startswith("rId") and rId[3:].isdigit() else 0,
+ rId,
+ )
+ for rId in self.keys()
+ )
+ return (self[rId] for _, rId in sorted_num_rId_pairs)
+
+ for rel in iter_rels_in_numerical_order():
+ rels_elm.add_rel(rel.rId, rel.reltype, rel.target_ref, rel.is_external)
+
+ return rels_elm.xml_file_bytes
+
+ def _add_relationship(self, reltype: str, target: Part | str, is_external: bool = False) -> str:
+ """Return str rId of |_Relationship| newly added to spec."""
+ rId = self._next_rId
+ self._rels[rId] = _Relationship(
+ self._base_uri,
+ rId,
+ reltype,
+ target_mode=RTM.EXTERNAL if is_external else RTM.INTERNAL,
+ target=target,
+ )
+ return rId
+
+ def _get_matching(
+ self, reltype: str, target: Part | str, is_external: bool = False
+ ) -> str | None:
+ """Return optional str rId of rel of `reltype`, `target`, and `is_external`.
+
+ Returns `None` on no matching relationship
+ """
+ for rel in self._rels_by_reltype[reltype]:
+ if rel.is_external != is_external:
+ continue
+ rel_target = rel.target_ref if rel.is_external else rel.target_part
+ if rel_target == target:
+ return rel.rId
+
+ return None
+
+ @property
+ def _next_rId(self) -> str:
+ """Next str rId available in collection.
+
+ The next rId is the first unused key starting from "rId1" and making use of any gaps in
+ numbering, e.g. 'rId2' for rIds ['rId1', 'rId3'].
+ """
+ # --- The common case is where all sequential numbers starting at "rId1" are
+ # --- used and the next available rId is "rId%d" % (len(rels)+1). So we start
+ # --- there and count down to produce the best performance.
+ for n in range(len(self) + 1, 0, -1):
+ rId_candidate = "rId%d" % n # like 'rId19'
+ if rId_candidate not in self._rels:
+ return rId_candidate
+ raise Exception(
+ "ProgrammingError: Impossible to have more distinct rIds than relationships"
+ )
+
+ @lazyproperty
+ def _rels(self) -> dict[str, _Relationship]:
+ """dict {rId: _Relationship} containing relationships of this collection."""
+ return {}
+
+ @property
+ def _rels_by_reltype(self) -> dict[str, list[_Relationship]]:
+ """defaultdict {reltype: [rels]} for all relationships in collection."""
+ D: DefaultDict[str, list[_Relationship]] = collections.defaultdict(list)
+ for rel in self.values():
+ D[rel.reltype].append(rel)
+ return D
+
+
+class _Relationship:
+ """Value object describing link from a part or package to another part."""
+
+ def __init__(self, base_uri: str, rId: str, reltype: str, target_mode: str, target: Part | str):
+ self._base_uri = base_uri
+ self._rId = rId
+ self._reltype = reltype
+ self._target_mode = target_mode
+ self._target = target
+
+ @classmethod
+ def from_xml(
+ cls, base_uri: str, rel: CT_Relationship, parts: dict[PackURI, Part]
+ ) -> _Relationship:
+ """Return |_Relationship| object based on CT_Relationship element `rel`."""
+ target = (
+ rel.target_ref
+ if rel.targetMode == RTM.EXTERNAL
+ else parts[PackURI.from_rel_ref(base_uri, rel.target_ref)]
+ )
+ return cls(base_uri, rel.rId, rel.reltype, rel.targetMode, target)
+
+ @lazyproperty
+ def is_external(self) -> bool:
+ """True if target_mode is `RTM.EXTERNAL`.
+
+ An external relationship is a link to a resource outside the package, such as a
+ web-resource (URL).
+ """
+ return self._target_mode == RTM.EXTERNAL
+
+ @lazyproperty
+ def reltype(self) -> str:
+ """Member of RELATIONSHIP_TYPE describing relationship of target to source."""
+ return self._reltype
+
+ @lazyproperty
+ def rId(self) -> str:
+ """str relationship-id, like 'rId9'.
+
+ Corresponds to the `Id` attribute on the `CT_Relationship` element and uniquely identifies
+ this relationship within its peers for the source-part or package.
+ """
+ return self._rId
+
+ @lazyproperty
+ def target_part(self) -> Part:
+ """|Part| or subtype referred to by this relationship."""
+ if self.is_external:
+ raise ValueError(
+ "`.target_part` property on _Relationship is undefined when "
+ "target-mode is external"
+ )
+ assert isinstance(self._target, Part)
+ return self._target
+
+ @lazyproperty
+ def target_partname(self) -> PackURI:
+ """|PackURI| instance containing partname targeted by this relationship.
+
+ Raises `ValueError` on reference if target_mode is external. Use :attr:`target_mode` to
+ check before referencing.
+ """
+ if self.is_external:
+ raise ValueError(
+ "`.target_partname` property on _Relationship is undefined when "
+ "target-mode is external"
+ )
+ assert isinstance(self._target, Part)
+ return self._target.partname
+
+ @lazyproperty
+ def target_ref(self) -> str:
+ """str reference to relationship target.
+
+ For internal relationships this is the relative partname, suitable for serialization
+ purposes. For an external relationship it is typically a URL.
+ """
+ if self.is_external:
+ assert isinstance(self._target, str)
+ return self._target
+
+ return self.target_partname.relative_ref(self._base_uri)
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/packuri.py b/.venv/lib/python3.12/site-packages/pptx/opc/packuri.py
new file mode 100644
index 00000000..74ddd333
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/opc/packuri.py
@@ -0,0 +1,109 @@
+"""Provides the PackURI value type and known pack-URI strings such as PACKAGE_URI."""
+
+from __future__ import annotations
+
+import posixpath
+import re
+
+
+class PackURI(str):
+ """Proxy for a pack URI (partname).
+
+ Provides utility properties the baseURI and the filename slice. Behaves as |str| otherwise.
+ """
+
+ _filename_re = re.compile("([a-zA-Z]+)([0-9][0-9]*)?")
+
+ def __new__(cls, pack_uri_str: str):
+ if not pack_uri_str[0] == "/":
+ raise ValueError(f"PackURI must begin with slash, got {repr(pack_uri_str)}")
+ return str.__new__(cls, pack_uri_str)
+
+ @staticmethod
+ def from_rel_ref(baseURI: str, relative_ref: str) -> PackURI:
+ """Construct an absolute pack URI formed by translating `relative_ref` onto `baseURI`."""
+ joined_uri = posixpath.join(baseURI, relative_ref)
+ abs_uri = posixpath.abspath(joined_uri)
+ return PackURI(abs_uri)
+
+ @property
+ def baseURI(self) -> str:
+ """The base URI of this pack URI; the directory portion, roughly speaking.
+
+ E.g. `"/ppt/slides"` for `"/ppt/slides/slide1.xml"`.
+
+ For the package pseudo-partname "/", the baseURI is "/".
+ """
+ return posixpath.split(self)[0]
+
+ @property
+ def ext(self) -> str:
+ """The extension portion of this pack URI.
+
+ E.g. `"xml"` for `"/ppt/slides/slide1.xml"`. Note the leading period is not included.
+ """
+ # -- raw_ext is either empty string or starts with period, e.g. ".xml" --
+ raw_ext = posixpath.splitext(self)[1]
+ return raw_ext[1:] if raw_ext.startswith(".") else raw_ext
+
+ @property
+ def filename(self) -> str:
+ """The "filename" portion of this pack URI.
+
+ E.g. `"slide1.xml"` for `"/ppt/slides/slide1.xml"`.
+
+ For the package pseudo-partname "/", `filename` is ''.
+ """
+ return posixpath.split(self)[1]
+
+ @property
+ def idx(self) -> int | None:
+ """Optional int partname index.
+
+ Value is an integer for an "array" partname or None for singleton partname, e.g. `21` for
+ `"/ppt/slides/slide21.xml"` and |None| for `"/ppt/presentation.xml"`.
+ """
+ filename = self.filename
+ if not filename:
+ return None
+ name_part = posixpath.splitext(filename)[0] # filename w/ext removed
+ match = self._filename_re.match(name_part)
+ if match is None:
+ return None
+ if match.group(2):
+ return int(match.group(2))
+ return None
+
+ @property
+ def membername(self) -> str:
+ """The pack URI with the leading slash stripped off.
+
+ This is the form used as the Zip file membername for the package item. Returns "" for the
+ package pseudo-partname "/".
+ """
+ return self[1:]
+
+ def relative_ref(self, baseURI: str) -> str:
+ """Return string containing relative reference to package item from `baseURI`.
+
+ E.g. PackURI("/ppt/slideLayouts/slideLayout1.xml") would return
+ "../slideLayouts/slideLayout1.xml" for baseURI "/ppt/slides".
+ """
+ # workaround for posixpath bug in 2.6, doesn't generate correct
+ # relative path when `start` (second) parameter is root ("/")
+ return self[1:] if baseURI == "/" else posixpath.relpath(self, baseURI)
+
+ @property
+ def rels_uri(self) -> PackURI:
+ """The pack URI of the .rels part corresponding to the current pack URI.
+
+ Only produces sensible output if the pack URI is a partname or the package pseudo-partname
+ "/".
+ """
+ rels_filename = "%s.rels" % self.filename
+ rels_uri_str = posixpath.join(self.baseURI, "_rels", rels_filename)
+ return PackURI(rels_uri_str)
+
+
+PACKAGE_URI = PackURI("/")
+CONTENT_TYPES_URI = PackURI("/[Content_Types].xml")
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/serialized.py b/.venv/lib/python3.12/site-packages/pptx/opc/serialized.py
new file mode 100644
index 00000000..92366708
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/opc/serialized.py
@@ -0,0 +1,296 @@
+"""API for reading/writing serialized Open Packaging Convention (OPC) package."""
+
+from __future__ import annotations
+
+import os
+import posixpath
+import zipfile
+from typing import IO, TYPE_CHECKING, Any, Container, Sequence
+
+from pptx.exc import PackageNotFoundError
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.opc.oxml import CT_Types, serialize_part_xml
+from pptx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI
+from pptx.opc.shared import CaseInsensitiveDict
+from pptx.opc.spec import default_content_types
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.opc.package import Part, _Relationships # pyright: ignore[reportPrivateUsage]
+
+
+class PackageReader(Container[bytes]):
+ """Provides access to package-parts of an OPC package with dict semantics.
+
+ The package may be in zip-format (a .pptx file) or expanded into a directory structure,
+ perhaps by unzipping a .pptx file.
+ """
+
+ def __init__(self, pkg_file: str | IO[bytes]):
+ self._pkg_file = pkg_file
+
+ def __contains__(self, pack_uri: object) -> bool:
+ """Return True when part identified by `pack_uri` is present in package."""
+ return pack_uri in self._blob_reader
+
+ def __getitem__(self, pack_uri: PackURI) -> bytes:
+ """Return bytes for part corresponding to `pack_uri`."""
+ return self._blob_reader[pack_uri]
+
+ def rels_xml_for(self, partname: PackURI) -> bytes | None:
+ """Return optional rels item XML for `partname`.
+
+ Returns `None` if no rels item is present for `partname`. `partname` is a |PackURI|
+ instance.
+ """
+ blob_reader, uri = self._blob_reader, partname.rels_uri
+ return blob_reader[uri] if uri in blob_reader else None
+
+ @lazyproperty
+ def _blob_reader(self) -> _PhysPkgReader:
+ """|_PhysPkgReader| subtype providing read access to the package file."""
+ return _PhysPkgReader.factory(self._pkg_file)
+
+
+class PackageWriter:
+ """Writes a zip-format OPC package to `pkg_file`.
+
+ `pkg_file` can be either a path to a zip file (a string) or a file-like object. `pkg_rels` is
+ the |_Relationships| object containing relationships for the package. `parts` is a sequence of
+ |Part| subtype instance to be written to the package.
+
+ Its single API classmethod is :meth:`write`. This class is not intended to be instantiated.
+ """
+
+ def __init__(self, pkg_file: str | IO[bytes], pkg_rels: _Relationships, parts: Sequence[Part]):
+ self._pkg_file = pkg_file
+ self._pkg_rels = pkg_rels
+ self._parts = parts
+
+ @classmethod
+ def write(
+ cls, pkg_file: str | IO[bytes], pkg_rels: _Relationships, parts: Sequence[Part]
+ ) -> None:
+ """Write a physical package (.pptx file) to `pkg_file`.
+
+ The serialized package contains `pkg_rels` and `parts`, a content-types stream based on
+ the content type of each part, and a .rels file for each part that has relationships.
+ """
+ cls(pkg_file, pkg_rels, parts)._write()
+
+ def _write(self) -> None:
+ """Write physical package (.pptx file)."""
+ with _PhysPkgWriter.factory(self._pkg_file) as phys_writer:
+ self._write_content_types_stream(phys_writer)
+ self._write_pkg_rels(phys_writer)
+ self._write_parts(phys_writer)
+
+ def _write_content_types_stream(self, phys_writer: _PhysPkgWriter) -> None:
+ """Write `[Content_Types].xml` part to the physical package.
+
+ This part must contain an appropriate content type lookup target for each part in the
+ package.
+ """
+ phys_writer.write(
+ CONTENT_TYPES_URI,
+ serialize_part_xml(_ContentTypesItem.xml_for(self._parts)),
+ )
+
+ def _write_parts(self, phys_writer: _PhysPkgWriter) -> None:
+ """Write blob of each part in `parts` to the package.
+
+ A rels item for each part is also written when the part has relationships.
+ """
+ for part in self._parts:
+ phys_writer.write(part.partname, part.blob)
+ if part._rels: # pyright: ignore[reportPrivateUsage]
+ phys_writer.write(part.partname.rels_uri, part.rels.xml)
+
+ def _write_pkg_rels(self, phys_writer: _PhysPkgWriter) -> None:
+ """Write the XML rels item for `pkg_rels` ('/_rels/.rels') to the package."""
+ phys_writer.write(PACKAGE_URI.rels_uri, self._pkg_rels.xml)
+
+
+class _PhysPkgReader(Container[PackURI]):
+ """Base class for physical package reader objects."""
+
+ def __contains__(self, item: object) -> bool:
+ """Must be implemented by each subclass."""
+ raise NotImplementedError( # pragma: no cover
+ "`%s` must implement `.__contains__()`" % type(self).__name__
+ )
+
+ def __getitem__(self, pack_uri: PackURI) -> bytes:
+ """Blob for part corresponding to `pack_uri`."""
+ raise NotImplementedError( # pragma: no cover
+ f"`{type(self).__name__}` must implement `.__contains__()`"
+ )
+
+ @classmethod
+ def factory(cls, pkg_file: str | IO[bytes]) -> _PhysPkgReader:
+ """Return |_PhysPkgReader| subtype instance appropriage for `pkg_file`."""
+ # --- for pkg_file other than str, assume it's a stream and pass it to Zip
+ # --- reader to sort out
+ if not isinstance(pkg_file, str):
+ return _ZipPkgReader(pkg_file)
+
+ # --- otherwise we treat `pkg_file` as a path ---
+ if os.path.isdir(pkg_file):
+ return _DirPkgReader(pkg_file)
+
+ if zipfile.is_zipfile(pkg_file):
+ return _ZipPkgReader(pkg_file)
+
+ raise PackageNotFoundError("Package not found at '%s'" % pkg_file)
+
+
+class _DirPkgReader(_PhysPkgReader):
+ """Implements |PhysPkgReader| interface for OPC package extracted into directory.
+
+ `path` is the path to a directory containing an expanded package.
+ """
+
+ def __init__(self, path: str):
+ self._path = os.path.abspath(path)
+
+ def __contains__(self, pack_uri: object) -> bool:
+ """Return True when part identified by `pack_uri` is present in zip archive."""
+ if not isinstance(pack_uri, PackURI):
+ return False
+ return os.path.exists(posixpath.join(self._path, pack_uri.membername))
+
+ def __getitem__(self, pack_uri: PackURI) -> bytes:
+ """Return bytes of file corresponding to `pack_uri` in package directory."""
+ path = os.path.join(self._path, pack_uri.membername)
+ try:
+ with open(path, "rb") as f:
+ return f.read()
+ except IOError:
+ raise KeyError("no member '%s' in package" % pack_uri)
+
+
+class _ZipPkgReader(_PhysPkgReader):
+ """Implements |PhysPkgReader| interface for a zip-file OPC package."""
+
+ def __init__(self, pkg_file: str | IO[bytes]):
+ self._pkg_file = pkg_file
+
+ def __contains__(self, pack_uri: object) -> bool:
+ """Return True when part identified by `pack_uri` is present in zip archive."""
+ return pack_uri in self._blobs
+
+ def __getitem__(self, pack_uri: PackURI) -> bytes:
+ """Return bytes for part corresponding to `pack_uri`.
+
+ Raises |KeyError| if no matching member is present in zip archive.
+ """
+ if pack_uri not in self._blobs:
+ raise KeyError("no member '%s' in package" % pack_uri)
+ return self._blobs[pack_uri]
+
+ @lazyproperty
+ def _blobs(self) -> dict[PackURI, bytes]:
+ """dict mapping partname to package part binaries."""
+ with zipfile.ZipFile(self._pkg_file, "r") as z:
+ return {PackURI("/%s" % name): z.read(name) for name in z.namelist()}
+
+
+class _PhysPkgWriter:
+ """Base class for physical package writer objects."""
+
+ @classmethod
+ def factory(cls, pkg_file: str | IO[bytes]) -> _ZipPkgWriter:
+ """Return |_PhysPkgWriter| subtype instance appropriage for `pkg_file`.
+
+ Currently the only subtype is `_ZipPkgWriter`, but a `_DirPkgWriter` could be implemented
+ or even a `_StreamPkgWriter`.
+ """
+ return _ZipPkgWriter(pkg_file)
+
+ def write(self, pack_uri: PackURI, blob: bytes) -> None:
+ """Write `blob` to package with membername corresponding to `pack_uri`."""
+ raise NotImplementedError( # pragma: no cover
+ f"`{type(self).__name__}` must implement `.write()`"
+ )
+
+
+class _ZipPkgWriter(_PhysPkgWriter):
+ """Implements |PhysPkgWriter| interface for a zip-file (.pptx file) OPC package."""
+
+ def __init__(self, pkg_file: str | IO[bytes]):
+ self._pkg_file = pkg_file
+
+ def __enter__(self) -> _ZipPkgWriter:
+ """Enable use as a context-manager. Opening zip for writing happens here."""
+ return self
+
+ def __exit__(self, *exc: list[Any]) -> None:
+ """Close the zip archive on exit from context.
+
+ Closing flushes any pending physical writes and releasing any resources it's using.
+ """
+ self._zipf.close()
+
+ def write(self, pack_uri: PackURI, blob: bytes) -> None:
+ """Write `blob` to zip package with membername corresponding to `pack_uri`."""
+ self._zipf.writestr(pack_uri.membername, blob)
+
+ @lazyproperty
+ def _zipf(self) -> zipfile.ZipFile:
+ """`ZipFile` instance open for writing."""
+ return zipfile.ZipFile(
+ self._pkg_file, "w", compression=zipfile.ZIP_DEFLATED, strict_timestamps=False
+ )
+
+
+class _ContentTypesItem:
+ """Composes content-types "part" ([Content_Types].xml) for a collection of parts."""
+
+ def __init__(self, parts: Sequence[Part]):
+ self._parts = parts
+
+ @classmethod
+ def xml_for(cls, parts: Sequence[Part]) -> CT_Types:
+ """Return content-types XML mapping each part in `parts` to a content-type.
+
+ The resulting XML is suitable for storage as `[Content_Types].xml` in an OPC package.
+ """
+ return cls(parts)._xml
+
+ @lazyproperty
+ def _xml(self) -> CT_Types:
+ """lxml.etree._Element containing the content-types item.
+
+ This XML object is suitable for serialization to the `[Content_Types].xml` item for an OPC
+ package. Although the sequence of elements is not strictly significant, as an aid to
+ testing and readability Default elements are sorted by extension and Override elements are
+ sorted by partname.
+ """
+ defaults, overrides = self._defaults_and_overrides
+ _types_elm = CT_Types.new()
+
+ for ext, content_type in sorted(defaults.items()):
+ _types_elm.add_default(ext, content_type)
+ for partname, content_type in sorted(overrides.items()):
+ _types_elm.add_override(partname, content_type)
+
+ return _types_elm
+
+ @lazyproperty
+ def _defaults_and_overrides(self) -> tuple[dict[str, str], dict[PackURI, str]]:
+ """pair of dict (defaults, overrides) accounting for all parts.
+
+ `defaults` is {ext: content_type} and overrides is {partname: content_type}.
+ """
+ defaults = CaseInsensitiveDict(rels=CT.OPC_RELATIONSHIPS, xml=CT.XML)
+ overrides: dict[PackURI, str] = {}
+
+ for part in self._parts:
+ partname, content_type = part.partname, part.content_type
+ ext = partname.ext
+ if (ext.lower(), content_type) in default_content_types:
+ defaults[ext] = content_type
+ else:
+ overrides[partname] = content_type
+
+ return defaults, overrides
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/shared.py b/.venv/lib/python3.12/site-packages/pptx/opc/shared.py
new file mode 100644
index 00000000..cc7fce8c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/opc/shared.py
@@ -0,0 +1,20 @@
+"""Objects shared by modules in the pptx.opc sub-package."""
+
+from __future__ import annotations
+
+
+class CaseInsensitiveDict(dict):
+ """Mapping type like dict except it matches key without respect to case.
+
+ For example, D['A'] == D['a']. Note this is not general-purpose, just complete
+ enough to satisfy opc package needs. It assumes str keys for example.
+ """
+
+ def __contains__(self, key):
+ return super(CaseInsensitiveDict, self).__contains__(key.lower())
+
+ def __getitem__(self, key):
+ return super(CaseInsensitiveDict, self).__getitem__(key.lower())
+
+ def __setitem__(self, key, value):
+ return super(CaseInsensitiveDict, self).__setitem__(key.lower(), value)
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/spec.py b/.venv/lib/python3.12/site-packages/pptx/opc/spec.py
new file mode 100644
index 00000000..a83caf8b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/opc/spec.py
@@ -0,0 +1,44 @@
+"""Provides mappings that embody aspects of the Open XML spec ISO/IEC 29500."""
+
+from pptx.opc.constants import CONTENT_TYPE as CT
+
+default_content_types = (
+ ("bin", CT.PML_PRINTER_SETTINGS),
+ ("bin", CT.SML_PRINTER_SETTINGS),
+ ("bin", CT.WML_PRINTER_SETTINGS),
+ ("bmp", CT.BMP),
+ ("emf", CT.X_EMF),
+ ("fntdata", CT.X_FONTDATA),
+ ("gif", CT.GIF),
+ ("jpe", CT.JPEG),
+ ("jpeg", CT.JPEG),
+ ("jpg", CT.JPEG),
+ ("mov", CT.MOV),
+ ("mp4", CT.MP4),
+ ("mpg", CT.MPG),
+ ("png", CT.PNG),
+ ("rels", CT.OPC_RELATIONSHIPS),
+ ("tif", CT.TIFF),
+ ("tiff", CT.TIFF),
+ ("vid", CT.VIDEO),
+ ("wdp", CT.MS_PHOTO),
+ ("wmf", CT.X_WMF),
+ ("wmv", CT.WMV),
+ ("xlsx", CT.SML_SHEET),
+ ("xml", CT.XML),
+)
+
+
+image_content_types = {
+ "bmp": CT.BMP,
+ "emf": CT.X_EMF,
+ "gif": CT.GIF,
+ "jpe": CT.JPEG,
+ "jpeg": CT.JPEG,
+ "jpg": CT.JPEG,
+ "png": CT.PNG,
+ "tif": CT.TIFF,
+ "tiff": CT.TIFF,
+ "wdp": CT.MS_PHOTO,
+ "wmf": CT.X_WMF,
+}
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/__init__.py b/.venv/lib/python3.12/site-packages/pptx/oxml/__init__.py
new file mode 100644
index 00000000..21afaa92
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/__init__.py
@@ -0,0 +1,486 @@
+"""Initializes lxml parser, particularly the custom element classes.
+
+Also makes available a handful of functions that wrap its typical uses.
+"""
+
+from __future__ import annotations
+
+import os
+from typing import TYPE_CHECKING, Type
+
+from lxml import etree
+
+from pptx.oxml.ns import NamespacePrefixedTag
+
+if TYPE_CHECKING:
+ from pptx.oxml.xmlchemy import BaseOxmlElement
+
+
+# -- configure etree XML parser ----------------------------
+element_class_lookup = etree.ElementNamespaceClassLookup()
+oxml_parser = etree.XMLParser(remove_blank_text=True, resolve_entities=False)
+oxml_parser.set_element_class_lookup(element_class_lookup)
+
+
+def parse_from_template(template_file_name: str):
+ """Return an element loaded from the XML in the template file identified by `template_name`."""
+ thisdir = os.path.split(__file__)[0]
+ filename = os.path.join(thisdir, "..", "templates", "%s.xml" % template_file_name)
+ with open(filename, "rb") as f:
+ xml = f.read()
+ return parse_xml(xml)
+
+
+def parse_xml(xml: str | bytes):
+ """Return root lxml element obtained by parsing XML character string in `xml`."""
+ return etree.fromstring(xml, oxml_parser)
+
+
+def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]):
+ """Register `cls` to be constructed when oxml parser encounters element having `nsptag_name`.
+
+ `nsptag_name` is a string of the form `nspfx:tagroot`, e.g. `"w:document"`.
+ """
+ nsptag = NamespacePrefixedTag(nsptagname)
+ namespace = element_class_lookup.get_namespace(nsptag.nsuri)
+ namespace[nsptag.local_part] = cls
+
+
+from pptx.oxml.action import CT_Hyperlink # noqa: E402
+
+register_element_cls("a:hlinkClick", CT_Hyperlink)
+register_element_cls("a:hlinkHover", CT_Hyperlink)
+
+
+from pptx.oxml.chart.axis import ( # noqa: E402
+ CT_AxisUnit,
+ CT_CatAx,
+ CT_ChartLines,
+ CT_Crosses,
+ CT_DateAx,
+ CT_LblOffset,
+ CT_Orientation,
+ CT_Scaling,
+ CT_TickLblPos,
+ CT_TickMark,
+ CT_ValAx,
+)
+
+register_element_cls("c:catAx", CT_CatAx)
+register_element_cls("c:crosses", CT_Crosses)
+register_element_cls("c:dateAx", CT_DateAx)
+register_element_cls("c:lblOffset", CT_LblOffset)
+register_element_cls("c:majorGridlines", CT_ChartLines)
+register_element_cls("c:majorTickMark", CT_TickMark)
+register_element_cls("c:majorUnit", CT_AxisUnit)
+register_element_cls("c:minorTickMark", CT_TickMark)
+register_element_cls("c:minorUnit", CT_AxisUnit)
+register_element_cls("c:orientation", CT_Orientation)
+register_element_cls("c:scaling", CT_Scaling)
+register_element_cls("c:tickLblPos", CT_TickLblPos)
+register_element_cls("c:valAx", CT_ValAx)
+
+
+from pptx.oxml.chart.chart import ( # noqa: E402
+ CT_Chart,
+ CT_ChartSpace,
+ CT_ExternalData,
+ CT_PlotArea,
+ CT_Style,
+)
+
+register_element_cls("c:chart", CT_Chart)
+register_element_cls("c:chartSpace", CT_ChartSpace)
+register_element_cls("c:externalData", CT_ExternalData)
+register_element_cls("c:plotArea", CT_PlotArea)
+register_element_cls("c:style", CT_Style)
+
+
+from pptx.oxml.chart.datalabel import CT_DLbl, CT_DLblPos, CT_DLbls # noqa: E402
+
+register_element_cls("c:dLbl", CT_DLbl)
+register_element_cls("c:dLblPos", CT_DLblPos)
+register_element_cls("c:dLbls", CT_DLbls)
+
+
+from pptx.oxml.chart.legend import CT_Legend, CT_LegendPos # noqa: E402
+
+register_element_cls("c:legend", CT_Legend)
+register_element_cls("c:legendPos", CT_LegendPos)
+
+
+from pptx.oxml.chart.marker import CT_Marker, CT_MarkerSize, CT_MarkerStyle # noqa: E402
+
+register_element_cls("c:marker", CT_Marker)
+register_element_cls("c:size", CT_MarkerSize)
+register_element_cls("c:symbol", CT_MarkerStyle)
+
+
+from pptx.oxml.chart.plot import ( # noqa: E402
+ CT_Area3DChart,
+ CT_AreaChart,
+ CT_BarChart,
+ CT_BarDir,
+ CT_BubbleChart,
+ CT_BubbleScale,
+ CT_DoughnutChart,
+ CT_GapAmount,
+ CT_Grouping,
+ CT_LineChart,
+ CT_Overlap,
+ CT_PieChart,
+ CT_RadarChart,
+ CT_ScatterChart,
+)
+
+register_element_cls("c:area3DChart", CT_Area3DChart)
+register_element_cls("c:areaChart", CT_AreaChart)
+register_element_cls("c:barChart", CT_BarChart)
+register_element_cls("c:barDir", CT_BarDir)
+register_element_cls("c:bubbleChart", CT_BubbleChart)
+register_element_cls("c:bubbleScale", CT_BubbleScale)
+register_element_cls("c:doughnutChart", CT_DoughnutChart)
+register_element_cls("c:gapWidth", CT_GapAmount)
+register_element_cls("c:grouping", CT_Grouping)
+register_element_cls("c:lineChart", CT_LineChart)
+register_element_cls("c:overlap", CT_Overlap)
+register_element_cls("c:pieChart", CT_PieChart)
+register_element_cls("c:radarChart", CT_RadarChart)
+register_element_cls("c:scatterChart", CT_ScatterChart)
+
+
+from pptx.oxml.chart.series import ( # noqa: E402
+ CT_AxDataSource,
+ CT_DPt,
+ CT_Lvl,
+ CT_NumDataSource,
+ CT_SeriesComposite,
+ CT_StrVal_NumVal_Composite,
+)
+
+register_element_cls("c:bubbleSize", CT_NumDataSource)
+register_element_cls("c:cat", CT_AxDataSource)
+register_element_cls("c:dPt", CT_DPt)
+register_element_cls("c:lvl", CT_Lvl)
+register_element_cls("c:pt", CT_StrVal_NumVal_Composite)
+register_element_cls("c:ser", CT_SeriesComposite)
+register_element_cls("c:val", CT_NumDataSource)
+register_element_cls("c:xVal", CT_NumDataSource)
+register_element_cls("c:yVal", CT_NumDataSource)
+
+
+from pptx.oxml.chart.shared import ( # noqa: E402
+ CT_Boolean,
+ CT_Boolean_Explicit,
+ CT_Double,
+ CT_Layout,
+ CT_LayoutMode,
+ CT_ManualLayout,
+ CT_NumFmt,
+ CT_Title,
+ CT_Tx,
+ CT_UnsignedInt,
+)
+
+register_element_cls("c:autoTitleDeleted", CT_Boolean_Explicit)
+register_element_cls("c:autoUpdate", CT_Boolean)
+register_element_cls("c:bubble3D", CT_Boolean)
+register_element_cls("c:crossAx", CT_UnsignedInt)
+register_element_cls("c:crossesAt", CT_Double)
+register_element_cls("c:date1904", CT_Boolean)
+register_element_cls("c:delete", CT_Boolean)
+register_element_cls("c:idx", CT_UnsignedInt)
+register_element_cls("c:invertIfNegative", CT_Boolean_Explicit)
+register_element_cls("c:layout", CT_Layout)
+register_element_cls("c:manualLayout", CT_ManualLayout)
+register_element_cls("c:max", CT_Double)
+register_element_cls("c:min", CT_Double)
+register_element_cls("c:numFmt", CT_NumFmt)
+register_element_cls("c:order", CT_UnsignedInt)
+register_element_cls("c:overlay", CT_Boolean_Explicit)
+register_element_cls("c:ptCount", CT_UnsignedInt)
+register_element_cls("c:showCatName", CT_Boolean_Explicit)
+register_element_cls("c:showLegendKey", CT_Boolean_Explicit)
+register_element_cls("c:showPercent", CT_Boolean_Explicit)
+register_element_cls("c:showSerName", CT_Boolean_Explicit)
+register_element_cls("c:showVal", CT_Boolean_Explicit)
+register_element_cls("c:smooth", CT_Boolean)
+register_element_cls("c:title", CT_Title)
+register_element_cls("c:tx", CT_Tx)
+register_element_cls("c:varyColors", CT_Boolean)
+register_element_cls("c:x", CT_Double)
+register_element_cls("c:xMode", CT_LayoutMode)
+
+
+from pptx.oxml.coreprops import CT_CoreProperties # noqa: E402
+
+register_element_cls("cp:coreProperties", CT_CoreProperties)
+
+
+from pptx.oxml.dml.color import ( # noqa: E402
+ CT_Color,
+ CT_HslColor,
+ CT_Percentage,
+ CT_PresetColor,
+ CT_SchemeColor,
+ CT_ScRgbColor,
+ CT_SRgbColor,
+ CT_SystemColor,
+)
+
+register_element_cls("a:bgClr", CT_Color)
+register_element_cls("a:fgClr", CT_Color)
+register_element_cls("a:hslClr", CT_HslColor)
+register_element_cls("a:lumMod", CT_Percentage)
+register_element_cls("a:lumOff", CT_Percentage)
+register_element_cls("a:prstClr", CT_PresetColor)
+register_element_cls("a:schemeClr", CT_SchemeColor)
+register_element_cls("a:scrgbClr", CT_ScRgbColor)
+register_element_cls("a:srgbClr", CT_SRgbColor)
+register_element_cls("a:sysClr", CT_SystemColor)
+
+
+from pptx.oxml.dml.fill import ( # noqa: E402
+ CT_Blip,
+ CT_BlipFillProperties,
+ CT_GradientFillProperties,
+ CT_GradientStop,
+ CT_GradientStopList,
+ CT_GroupFillProperties,
+ CT_LinearShadeProperties,
+ CT_NoFillProperties,
+ CT_PatternFillProperties,
+ CT_RelativeRect,
+ CT_SolidColorFillProperties,
+)
+
+register_element_cls("a:blip", CT_Blip)
+register_element_cls("a:blipFill", CT_BlipFillProperties)
+register_element_cls("a:gradFill", CT_GradientFillProperties)
+register_element_cls("a:grpFill", CT_GroupFillProperties)
+register_element_cls("a:gs", CT_GradientStop)
+register_element_cls("a:gsLst", CT_GradientStopList)
+register_element_cls("a:lin", CT_LinearShadeProperties)
+register_element_cls("a:noFill", CT_NoFillProperties)
+register_element_cls("a:pattFill", CT_PatternFillProperties)
+register_element_cls("a:solidFill", CT_SolidColorFillProperties)
+register_element_cls("a:srcRect", CT_RelativeRect)
+
+
+from pptx.oxml.dml.line import CT_PresetLineDashProperties # noqa: E402
+
+register_element_cls("a:prstDash", CT_PresetLineDashProperties)
+
+
+from pptx.oxml.presentation import ( # noqa: E402
+ CT_Presentation,
+ CT_SlideId,
+ CT_SlideIdList,
+ CT_SlideMasterIdList,
+ CT_SlideMasterIdListEntry,
+ CT_SlideSize,
+)
+
+register_element_cls("p:presentation", CT_Presentation)
+register_element_cls("p:sldId", CT_SlideId)
+register_element_cls("p:sldIdLst", CT_SlideIdList)
+register_element_cls("p:sldMasterId", CT_SlideMasterIdListEntry)
+register_element_cls("p:sldMasterIdLst", CT_SlideMasterIdList)
+register_element_cls("p:sldSz", CT_SlideSize)
+
+
+from pptx.oxml.shapes.autoshape import ( # noqa: E402
+ CT_AdjPoint2D,
+ CT_CustomGeometry2D,
+ CT_GeomGuide,
+ CT_GeomGuideList,
+ CT_NonVisualDrawingShapeProps,
+ CT_Path2D,
+ CT_Path2DClose,
+ CT_Path2DLineTo,
+ CT_Path2DList,
+ CT_Path2DMoveTo,
+ CT_PresetGeometry2D,
+ CT_Shape,
+ CT_ShapeNonVisual,
+)
+
+register_element_cls("a:avLst", CT_GeomGuideList)
+register_element_cls("a:custGeom", CT_CustomGeometry2D)
+register_element_cls("a:gd", CT_GeomGuide)
+register_element_cls("a:close", CT_Path2DClose)
+register_element_cls("a:lnTo", CT_Path2DLineTo)
+register_element_cls("a:moveTo", CT_Path2DMoveTo)
+register_element_cls("a:path", CT_Path2D)
+register_element_cls("a:pathLst", CT_Path2DList)
+register_element_cls("a:prstGeom", CT_PresetGeometry2D)
+register_element_cls("a:pt", CT_AdjPoint2D)
+register_element_cls("p:cNvSpPr", CT_NonVisualDrawingShapeProps)
+register_element_cls("p:nvSpPr", CT_ShapeNonVisual)
+register_element_cls("p:sp", CT_Shape)
+
+
+from pptx.oxml.shapes.connector import ( # noqa: E402
+ CT_Connection,
+ CT_Connector,
+ CT_ConnectorNonVisual,
+ CT_NonVisualConnectorProperties,
+)
+
+register_element_cls("a:endCxn", CT_Connection)
+register_element_cls("a:stCxn", CT_Connection)
+register_element_cls("p:cNvCxnSpPr", CT_NonVisualConnectorProperties)
+register_element_cls("p:cxnSp", CT_Connector)
+register_element_cls("p:nvCxnSpPr", CT_ConnectorNonVisual)
+
+
+from pptx.oxml.shapes.graphfrm import ( # noqa: E402
+ CT_GraphicalObject,
+ CT_GraphicalObjectData,
+ CT_GraphicalObjectFrame,
+ CT_GraphicalObjectFrameNonVisual,
+ CT_OleObject,
+)
+
+register_element_cls("a:graphic", CT_GraphicalObject)
+register_element_cls("a:graphicData", CT_GraphicalObjectData)
+register_element_cls("p:graphicFrame", CT_GraphicalObjectFrame)
+register_element_cls("p:nvGraphicFramePr", CT_GraphicalObjectFrameNonVisual)
+register_element_cls("p:oleObj", CT_OleObject)
+
+
+from pptx.oxml.shapes.groupshape import ( # noqa: E402
+ CT_GroupShape,
+ CT_GroupShapeNonVisual,
+ CT_GroupShapeProperties,
+)
+
+register_element_cls("p:grpSp", CT_GroupShape)
+register_element_cls("p:grpSpPr", CT_GroupShapeProperties)
+register_element_cls("p:nvGrpSpPr", CT_GroupShapeNonVisual)
+register_element_cls("p:spTree", CT_GroupShape)
+
+
+from pptx.oxml.shapes.picture import CT_Picture, CT_PictureNonVisual # noqa: E402
+
+register_element_cls("p:blipFill", CT_BlipFillProperties)
+register_element_cls("p:nvPicPr", CT_PictureNonVisual)
+register_element_cls("p:pic", CT_Picture)
+
+
+from pptx.oxml.shapes.shared import ( # noqa: E402
+ CT_ApplicationNonVisualDrawingProps,
+ CT_LineProperties,
+ CT_NonVisualDrawingProps,
+ CT_Placeholder,
+ CT_Point2D,
+ CT_PositiveSize2D,
+ CT_ShapeProperties,
+ CT_Transform2D,
+)
+
+register_element_cls("a:chExt", CT_PositiveSize2D)
+register_element_cls("a:chOff", CT_Point2D)
+register_element_cls("a:ext", CT_PositiveSize2D)
+register_element_cls("a:ln", CT_LineProperties)
+register_element_cls("a:off", CT_Point2D)
+register_element_cls("a:xfrm", CT_Transform2D)
+register_element_cls("c:spPr", CT_ShapeProperties)
+register_element_cls("p:cNvPr", CT_NonVisualDrawingProps)
+register_element_cls("p:nvPr", CT_ApplicationNonVisualDrawingProps)
+register_element_cls("p:ph", CT_Placeholder)
+register_element_cls("p:spPr", CT_ShapeProperties)
+register_element_cls("p:xfrm", CT_Transform2D)
+
+
+from pptx.oxml.slide import ( # noqa: E402
+ CT_Background,
+ CT_BackgroundProperties,
+ CT_CommonSlideData,
+ CT_NotesMaster,
+ CT_NotesSlide,
+ CT_Slide,
+ CT_SlideLayout,
+ CT_SlideLayoutIdList,
+ CT_SlideLayoutIdListEntry,
+ CT_SlideMaster,
+ CT_SlideTiming,
+ CT_TimeNodeList,
+ CT_TLMediaNodeVideo,
+)
+
+register_element_cls("p:bg", CT_Background)
+register_element_cls("p:bgPr", CT_BackgroundProperties)
+register_element_cls("p:childTnLst", CT_TimeNodeList)
+register_element_cls("p:cSld", CT_CommonSlideData)
+register_element_cls("p:notes", CT_NotesSlide)
+register_element_cls("p:notesMaster", CT_NotesMaster)
+register_element_cls("p:sld", CT_Slide)
+register_element_cls("p:sldLayout", CT_SlideLayout)
+register_element_cls("p:sldLayoutId", CT_SlideLayoutIdListEntry)
+register_element_cls("p:sldLayoutIdLst", CT_SlideLayoutIdList)
+register_element_cls("p:sldMaster", CT_SlideMaster)
+register_element_cls("p:timing", CT_SlideTiming)
+register_element_cls("p:video", CT_TLMediaNodeVideo)
+
+
+from pptx.oxml.table import ( # noqa: E402
+ CT_Table,
+ CT_TableCell,
+ CT_TableCellProperties,
+ CT_TableCol,
+ CT_TableGrid,
+ CT_TableProperties,
+ CT_TableRow,
+)
+
+register_element_cls("a:gridCol", CT_TableCol)
+register_element_cls("a:tbl", CT_Table)
+register_element_cls("a:tblGrid", CT_TableGrid)
+register_element_cls("a:tblPr", CT_TableProperties)
+register_element_cls("a:tc", CT_TableCell)
+register_element_cls("a:tcPr", CT_TableCellProperties)
+register_element_cls("a:tr", CT_TableRow)
+
+
+from pptx.oxml.text import ( # noqa: E402
+ CT_RegularTextRun,
+ CT_TextBody,
+ CT_TextBodyProperties,
+ CT_TextCharacterProperties,
+ CT_TextField,
+ CT_TextFont,
+ CT_TextLineBreak,
+ CT_TextNormalAutofit,
+ CT_TextParagraph,
+ CT_TextParagraphProperties,
+ CT_TextSpacing,
+ CT_TextSpacingPercent,
+ CT_TextSpacingPoint,
+)
+
+register_element_cls("a:bodyPr", CT_TextBodyProperties)
+register_element_cls("a:br", CT_TextLineBreak)
+register_element_cls("a:defRPr", CT_TextCharacterProperties)
+register_element_cls("a:endParaRPr", CT_TextCharacterProperties)
+register_element_cls("a:fld", CT_TextField)
+register_element_cls("a:latin", CT_TextFont)
+register_element_cls("a:lnSpc", CT_TextSpacing)
+register_element_cls("a:normAutofit", CT_TextNormalAutofit)
+register_element_cls("a:r", CT_RegularTextRun)
+register_element_cls("a:p", CT_TextParagraph)
+register_element_cls("a:pPr", CT_TextParagraphProperties)
+register_element_cls("c:rich", CT_TextBody)
+register_element_cls("a:rPr", CT_TextCharacterProperties)
+register_element_cls("a:spcAft", CT_TextSpacing)
+register_element_cls("a:spcBef", CT_TextSpacing)
+register_element_cls("a:spcPct", CT_TextSpacingPercent)
+register_element_cls("a:spcPts", CT_TextSpacingPoint)
+register_element_cls("a:txBody", CT_TextBody)
+register_element_cls("c:txPr", CT_TextBody)
+register_element_cls("p:txBody", CT_TextBody)
+
+
+from pptx.oxml.theme import CT_OfficeStyleSheet # noqa: E402
+
+register_element_cls("a:theme", CT_OfficeStyleSheet)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/action.py b/.venv/lib/python3.12/site-packages/pptx/oxml/action.py
new file mode 100644
index 00000000..9b31a9e1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/action.py
@@ -0,0 +1,53 @@
+"""lxml custom element classes for text-related XML elements."""
+
+from __future__ import annotations
+
+from pptx.oxml.simpletypes import XsdString
+from pptx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute
+
+
+class CT_Hyperlink(BaseOxmlElement):
+ """Custom element class for <a:hlinkClick> elements."""
+
+ rId: str = OptionalAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType]
+ action: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "action", XsdString
+ )
+
+ @property
+ def action_fields(self) -> dict[str, str]:
+ """Query portion of the `ppaction://` URL as dict.
+
+ For example `{'id':'0', 'return':'true'}` in 'ppaction://customshow?id=0&return=true'.
+
+ Returns an empty dict if the URL contains no query string or if no action attribute is
+ present.
+ """
+ url = self.action
+
+ if url is None:
+ return {}
+
+ halves = url.split("?")
+ if len(halves) == 1:
+ return {}
+
+ key_value_pairs = halves[1].split("&")
+ return dict([pair.split("=") for pair in key_value_pairs])
+
+ @property
+ def action_verb(self) -> str | None:
+ """The host portion of the `ppaction://` URL contained in the action attribute.
+
+ For example 'customshow' in 'ppaction://customshow?id=0&return=true'. Returns |None| if no
+ action attribute is present.
+ """
+ url = self.action
+
+ if url is None:
+ return None
+
+ protocol_and_host = url.split("?")[0]
+ host = protocol_and_host[11:]
+
+ return host
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/__init__.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/axis.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/axis.py
new file mode 100644
index 00000000..7129810c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/axis.py
@@ -0,0 +1,297 @@
+"""Axis-related oxml objects."""
+
+from __future__ import annotations
+
+from pptx.enum.chart import XL_AXIS_CROSSES, XL_TICK_LABEL_POSITION, XL_TICK_MARK
+from pptx.oxml.chart.shared import CT_Title
+from pptx.oxml.simpletypes import ST_AxisUnit, ST_LblOffset, ST_Orientation
+from pptx.oxml.text import CT_TextBody
+from pptx.oxml.xmlchemy import (
+ BaseOxmlElement,
+ OneAndOnlyOne,
+ OptionalAttribute,
+ RequiredAttribute,
+ ZeroOrOne,
+)
+
+
+class BaseAxisElement(BaseOxmlElement):
+ """Base class for catAx, dateAx, valAx, and perhaps other axis elements."""
+
+ @property
+ def defRPr(self):
+ """
+ ``<a:defRPr>`` great-great-grandchild element, added with its
+ ancestors if not present.
+ """
+ txPr = self.get_or_add_txPr()
+ defRPr = txPr.defRPr
+ return defRPr
+
+ @property
+ def orientation(self):
+ """Value of `val` attribute of `c:scaling/c:orientation` grandchild element.
+
+ Defaults to `ST_Orientation.MIN_MAX` if attribute or any ancestors are not
+ present.
+ """
+ orientation = self.scaling.orientation
+ if orientation is None:
+ return ST_Orientation.MIN_MAX
+ return orientation.val
+
+ @orientation.setter
+ def orientation(self, value):
+ """`value` is a member of `ST_Orientation`."""
+ self.scaling._remove_orientation()
+ if value == ST_Orientation.MAX_MIN:
+ self.scaling.get_or_add_orientation().val = value
+
+ def _new_title(self):
+ return CT_Title.new_title()
+
+ def _new_txPr(self):
+ return CT_TextBody.new_txPr()
+
+
+class CT_AxisUnit(BaseOxmlElement):
+ """Used for `c:majorUnit` and `c:minorUnit` elements, and others."""
+
+ val = RequiredAttribute("val", ST_AxisUnit)
+
+
+class CT_CatAx(BaseAxisElement):
+ """`c:catAx` element, defining a category axis."""
+
+ _tag_seq = (
+ "c:axId",
+ "c:scaling",
+ "c:delete",
+ "c:axPos",
+ "c:majorGridlines",
+ "c:minorGridlines",
+ "c:title",
+ "c:numFmt",
+ "c:majorTickMark",
+ "c:minorTickMark",
+ "c:tickLblPos",
+ "c:spPr",
+ "c:txPr",
+ "c:crossAx",
+ "c:crosses",
+ "c:crossesAt",
+ "c:auto",
+ "c:lblAlgn",
+ "c:lblOffset",
+ "c:tickLblSkip",
+ "c:tickMarkSkip",
+ "c:noMultiLvlLbl",
+ "c:extLst",
+ )
+ scaling = OneAndOnlyOne("c:scaling")
+ delete_ = ZeroOrOne("c:delete", successors=_tag_seq[3:])
+ majorGridlines = ZeroOrOne("c:majorGridlines", successors=_tag_seq[5:])
+ minorGridlines = ZeroOrOne("c:minorGridlines", successors=_tag_seq[6:])
+ title = ZeroOrOne("c:title", successors=_tag_seq[7:])
+ numFmt = ZeroOrOne("c:numFmt", successors=_tag_seq[8:])
+ majorTickMark = ZeroOrOne("c:majorTickMark", successors=_tag_seq[9:])
+ minorTickMark = ZeroOrOne("c:minorTickMark", successors=_tag_seq[10:])
+ tickLblPos = ZeroOrOne("c:tickLblPos", successors=_tag_seq[11:])
+ spPr = ZeroOrOne("c:spPr", successors=_tag_seq[12:])
+ txPr = ZeroOrOne("c:txPr", successors=_tag_seq[13:])
+ crosses = ZeroOrOne("c:crosses", successors=_tag_seq[15:])
+ crossesAt = ZeroOrOne("c:crossesAt", successors=_tag_seq[16:])
+ lblOffset = ZeroOrOne("c:lblOffset", successors=_tag_seq[19:])
+ del _tag_seq
+
+
+class CT_ChartLines(BaseOxmlElement):
+ """Used for `c:majorGridlines` and `c:minorGridlines`.
+
+ Specifies gridlines visual properties such as color and width.
+ """
+
+ spPr = ZeroOrOne("c:spPr", successors=())
+
+
+class CT_Crosses(BaseOxmlElement):
+ """`c:crosses` element, specifying where the other axis crosses this one."""
+
+ val = RequiredAttribute("val", XL_AXIS_CROSSES)
+
+
+class CT_DateAx(BaseAxisElement):
+ """`c:dateAx` element, defining a date (category) axis."""
+
+ _tag_seq = (
+ "c:axId",
+ "c:scaling",
+ "c:delete",
+ "c:axPos",
+ "c:majorGridlines",
+ "c:minorGridlines",
+ "c:title",
+ "c:numFmt",
+ "c:majorTickMark",
+ "c:minorTickMark",
+ "c:tickLblPos",
+ "c:spPr",
+ "c:txPr",
+ "c:crossAx",
+ "c:crosses",
+ "c:crossesAt",
+ "c:auto",
+ "c:lblOffset",
+ "c:baseTimeUnit",
+ "c:majorUnit",
+ "c:majorTimeUnit",
+ "c:minorUnit",
+ "c:minorTimeUnit",
+ "c:extLst",
+ )
+ scaling = OneAndOnlyOne("c:scaling")
+ delete_ = ZeroOrOne("c:delete", successors=_tag_seq[3:])
+ majorGridlines = ZeroOrOne("c:majorGridlines", successors=_tag_seq[5:])
+ minorGridlines = ZeroOrOne("c:minorGridlines", successors=_tag_seq[6:])
+ title = ZeroOrOne("c:title", successors=_tag_seq[7:])
+ numFmt = ZeroOrOne("c:numFmt", successors=_tag_seq[8:])
+ majorTickMark = ZeroOrOne("c:majorTickMark", successors=_tag_seq[9:])
+ minorTickMark = ZeroOrOne("c:minorTickMark", successors=_tag_seq[10:])
+ tickLblPos = ZeroOrOne("c:tickLblPos", successors=_tag_seq[11:])
+ spPr = ZeroOrOne("c:spPr", successors=_tag_seq[12:])
+ txPr = ZeroOrOne("c:txPr", successors=_tag_seq[13:])
+ crosses = ZeroOrOne("c:crosses", successors=_tag_seq[15:])
+ crossesAt = ZeroOrOne("c:crossesAt", successors=_tag_seq[16:])
+ lblOffset = ZeroOrOne("c:lblOffset", successors=_tag_seq[18:])
+ del _tag_seq
+
+
+class CT_LblOffset(BaseOxmlElement):
+ """`c:lblOffset` custom element class."""
+
+ val = OptionalAttribute("val", ST_LblOffset, default=100)
+
+
+class CT_Orientation(BaseOxmlElement):
+ """`c:xAx/c:scaling/c:orientation` element, defining category order.
+
+ Used to reverse the order categories appear in on a bar chart so they start at the
+ top rather than the bottom. Because we read top-to-bottom, the default way looks odd
+ to many and perhaps most folks. Also applicable to value and date axes.
+ """
+
+ val = OptionalAttribute("val", ST_Orientation, default=ST_Orientation.MIN_MAX)
+
+
+class CT_Scaling(BaseOxmlElement):
+ """`c:scaling` element.
+
+ Defines axis scale characteristics such as maximum value, log vs. linear, etc.
+ """
+
+ _tag_seq = ("c:logBase", "c:orientation", "c:max", "c:min", "c:extLst")
+ orientation = ZeroOrOne("c:orientation", successors=_tag_seq[2:])
+ max = ZeroOrOne("c:max", successors=_tag_seq[3:])
+ min = ZeroOrOne("c:min", successors=_tag_seq[4:])
+ del _tag_seq
+
+ @property
+ def maximum(self):
+ """
+ The float value of the ``<c:max>`` child element, or |None| if no max
+ element is present.
+ """
+ max = self.max
+ if max is None:
+ return None
+ return max.val
+
+ @maximum.setter
+ def maximum(self, value):
+ """
+ Set the value of the ``<c:max>`` child element to the float *value*,
+ or remove the max element if *value* is |None|.
+ """
+ self._remove_max()
+ if value is None:
+ return
+ self._add_max(val=value)
+
+ @property
+ def minimum(self):
+ """
+ The float value of the ``<c:min>`` child element, or |None| if no min
+ element is present.
+ """
+ min = self.min
+ if min is None:
+ return None
+ return min.val
+
+ @minimum.setter
+ def minimum(self, value):
+ """
+ Set the value of the ``<c:min>`` child element to the float *value*,
+ or remove the min element if *value* is |None|.
+ """
+ self._remove_min()
+ if value is None:
+ return
+ self._add_min(val=value)
+
+
+class CT_TickLblPos(BaseOxmlElement):
+ """`c:tickLblPos` element."""
+
+ val = OptionalAttribute("val", XL_TICK_LABEL_POSITION)
+
+
+class CT_TickMark(BaseOxmlElement):
+ """Used for `c:minorTickMark` and `c:majorTickMark`."""
+
+ val = OptionalAttribute("val", XL_TICK_MARK, default=XL_TICK_MARK.CROSS)
+
+
+class CT_ValAx(BaseAxisElement):
+ """`c:valAx` element, defining a value axis."""
+
+ _tag_seq = (
+ "c:axId",
+ "c:scaling",
+ "c:delete",
+ "c:axPos",
+ "c:majorGridlines",
+ "c:minorGridlines",
+ "c:title",
+ "c:numFmt",
+ "c:majorTickMark",
+ "c:minorTickMark",
+ "c:tickLblPos",
+ "c:spPr",
+ "c:txPr",
+ "c:crossAx",
+ "c:crosses",
+ "c:crossesAt",
+ "c:crossBetween",
+ "c:majorUnit",
+ "c:minorUnit",
+ "c:dispUnits",
+ "c:extLst",
+ )
+ scaling = OneAndOnlyOne("c:scaling")
+ delete_ = ZeroOrOne("c:delete", successors=_tag_seq[3:])
+ majorGridlines = ZeroOrOne("c:majorGridlines", successors=_tag_seq[5:])
+ minorGridlines = ZeroOrOne("c:minorGridlines", successors=_tag_seq[6:])
+ title = ZeroOrOne("c:title", successors=_tag_seq[7:])
+ numFmt = ZeroOrOne("c:numFmt", successors=_tag_seq[8:])
+ majorTickMark = ZeroOrOne("c:majorTickMark", successors=_tag_seq[9:])
+ minorTickMark = ZeroOrOne("c:minorTickMark", successors=_tag_seq[10:])
+ tickLblPos = ZeroOrOne("c:tickLblPos", successors=_tag_seq[11:])
+ spPr = ZeroOrOne("c:spPr", successors=_tag_seq[12:])
+ txPr = ZeroOrOne("c:txPr", successors=_tag_seq[13:])
+ crossAx = ZeroOrOne("c:crossAx", successors=_tag_seq[14:])
+ crosses = ZeroOrOne("c:crosses", successors=_tag_seq[15:])
+ crossesAt = ZeroOrOne("c:crossesAt", successors=_tag_seq[16:])
+ majorUnit = ZeroOrOne("c:majorUnit", successors=_tag_seq[18:])
+ minorUnit = ZeroOrOne("c:minorUnit", successors=_tag_seq[19:])
+ del _tag_seq
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/chart.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/chart.py
new file mode 100644
index 00000000..f4cd0dc7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/chart.py
@@ -0,0 +1,282 @@
+"""Custom element classes for top-level chart-related XML elements."""
+
+from __future__ import annotations
+
+from typing import cast
+
+from pptx.oxml import parse_xml
+from pptx.oxml.chart.shared import CT_Title
+from pptx.oxml.ns import nsdecls, qn
+from pptx.oxml.simpletypes import ST_Style, XsdString
+from pptx.oxml.text import CT_TextBody
+from pptx.oxml.xmlchemy import (
+ BaseOxmlElement,
+ OneAndOnlyOne,
+ RequiredAttribute,
+ ZeroOrMore,
+ ZeroOrOne,
+)
+
+
+class CT_Chart(BaseOxmlElement):
+ """`c:chart` custom element class."""
+
+ _tag_seq = (
+ "c:title",
+ "c:autoTitleDeleted",
+ "c:pivotFmts",
+ "c:view3D",
+ "c:floor",
+ "c:sideWall",
+ "c:backWall",
+ "c:plotArea",
+ "c:legend",
+ "c:plotVisOnly",
+ "c:dispBlanksAs",
+ "c:showDLblsOverMax",
+ "c:extLst",
+ )
+ title = ZeroOrOne("c:title", successors=_tag_seq[1:])
+ autoTitleDeleted = ZeroOrOne("c:autoTitleDeleted", successors=_tag_seq[2:])
+ plotArea = OneAndOnlyOne("c:plotArea")
+ legend = ZeroOrOne("c:legend", successors=_tag_seq[9:])
+ rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType]
+
+ @property
+ def has_legend(self):
+ """
+ True if this chart has a legend defined, False otherwise.
+ """
+ legend = self.legend
+ if legend is None:
+ return False
+ return True
+
+ @has_legend.setter
+ def has_legend(self, bool_value):
+ """
+ Add, remove, or leave alone the ``<c:legend>`` child element depending
+ on current state and *bool_value*. If *bool_value* is |True| and no
+ ``<c:legend>`` element is present, a new default element is added.
+ When |False|, any existing legend element is removed.
+ """
+ if bool(bool_value) is False:
+ self._remove_legend()
+ else:
+ if self.legend is None:
+ self._add_legend()
+
+ @staticmethod
+ def new_chart(rId: str) -> CT_Chart:
+ """Return a new `c:chart` element."""
+ return cast(CT_Chart, parse_xml(f'<c:chart {nsdecls("c")} {nsdecls("r")} r:id="{rId}"/>'))
+
+ def _new_title(self):
+ return CT_Title.new_title()
+
+
+class CT_ChartSpace(BaseOxmlElement):
+ """`c:chartSpace` root element of a chart part."""
+
+ _tag_seq = (
+ "c:date1904",
+ "c:lang",
+ "c:roundedCorners",
+ "c:style",
+ "c:clrMapOvr",
+ "c:pivotSource",
+ "c:protection",
+ "c:chart",
+ "c:spPr",
+ "c:txPr",
+ "c:externalData",
+ "c:printSettings",
+ "c:userShapes",
+ "c:extLst",
+ )
+ date1904 = ZeroOrOne("c:date1904", successors=_tag_seq[1:])
+ style = ZeroOrOne("c:style", successors=_tag_seq[4:])
+ chart = OneAndOnlyOne("c:chart")
+ txPr = ZeroOrOne("c:txPr", successors=_tag_seq[10:])
+ externalData = ZeroOrOne("c:externalData", successors=_tag_seq[11:])
+ del _tag_seq
+
+ @property
+ def catAx_lst(self):
+ return self.chart.plotArea.catAx_lst
+
+ @property
+ def date_1904(self):
+ """
+ Return |True| if the `c:date1904` child element resolves truthy,
+ |False| otherwise. This value indicates whether date number values
+ are based on the 1900 or 1904 epoch.
+ """
+ date1904 = self.date1904
+ if date1904 is None:
+ return False
+ return date1904.val
+
+ @property
+ def dateAx_lst(self):
+ return self.xpath("c:chart/c:plotArea/c:dateAx")
+
+ def get_or_add_title(self):
+ """Return the `c:title` grandchild, newly created if not present."""
+ return self.chart.get_or_add_title()
+
+ @property
+ def plotArea(self):
+ """
+ Return the required `c:chartSpace/c:chart/c:plotArea` grandchild
+ element.
+ """
+ return self.chart.plotArea
+
+ @property
+ def valAx_lst(self):
+ return self.chart.plotArea.valAx_lst
+
+ @property
+ def xlsx_part_rId(self):
+ """
+ The string in the required ``r:id`` attribute of the
+ `<c:externalData>` child, or |None| if no externalData element is
+ present.
+ """
+ externalData = self.externalData
+ if externalData is None:
+ return None
+ return externalData.rId
+
+ def _add_externalData(self):
+ """
+ Always add a ``<c:autoUpdate val="0"/>`` child so auto-updating
+ behavior is off by default.
+ """
+ externalData = self._new_externalData()
+ externalData._add_autoUpdate(val=False)
+ self._insert_externalData(externalData)
+ return externalData
+
+ def _new_txPr(self):
+ return CT_TextBody.new_txPr()
+
+
+class CT_ExternalData(BaseOxmlElement):
+ """
+ `<c:externalData>` element, defining link to embedded Excel package part
+ containing the chart data.
+ """
+
+ autoUpdate = ZeroOrOne("c:autoUpdate")
+ rId = RequiredAttribute("r:id", XsdString)
+
+
+class CT_PlotArea(BaseOxmlElement):
+ """
+ ``<c:plotArea>`` element.
+ """
+
+ catAx = ZeroOrMore("c:catAx")
+ valAx = ZeroOrMore("c:valAx")
+
+ def iter_sers(self):
+ """
+ Generate each of the `c:ser` elements in this chart, ordered first by
+ the document order of the containing xChart element, then by their
+ ordering within the xChart element (not necessarily document order).
+ """
+ for xChart in self.iter_xCharts():
+ for ser in xChart.iter_sers():
+ yield ser
+
+ def iter_xCharts(self):
+ """
+ Generate each xChart child element in document.
+ """
+ plot_tags = (
+ qn("c:area3DChart"),
+ qn("c:areaChart"),
+ qn("c:bar3DChart"),
+ qn("c:barChart"),
+ qn("c:bubbleChart"),
+ qn("c:doughnutChart"),
+ qn("c:line3DChart"),
+ qn("c:lineChart"),
+ qn("c:ofPieChart"),
+ qn("c:pie3DChart"),
+ qn("c:pieChart"),
+ qn("c:radarChart"),
+ qn("c:scatterChart"),
+ qn("c:stockChart"),
+ qn("c:surface3DChart"),
+ qn("c:surfaceChart"),
+ )
+
+ for child in self.iterchildren():
+ if child.tag not in plot_tags:
+ continue
+ yield child
+
+ @property
+ def last_ser(self):
+ """
+ Return the last `<c:ser>` element in the last xChart element, based
+ on series order (not necessarily the same element as document order).
+ """
+ last_xChart = self.xCharts[-1]
+ sers = last_xChart.sers
+ if not sers:
+ return None
+ return sers[-1]
+
+ @property
+ def next_idx(self):
+ """
+ Return the next available `c:ser/c:idx` value within the scope of
+ this chart, the maximum idx value found on existing series,
+ incremented by one.
+ """
+ idx_vals = [s.idx.val for s in self.sers]
+ if not idx_vals:
+ return 0
+ return max(idx_vals) + 1
+
+ @property
+ def next_order(self):
+ """
+ Return the next available `c:ser/c:order` value within the scope of
+ this chart, the maximum order value found on existing series,
+ incremented by one.
+ """
+ order_vals = [s.order.val for s in self.sers]
+ if not order_vals:
+ return 0
+ return max(order_vals) + 1
+
+ @property
+ def sers(self):
+ """
+ Return a sequence containing all the `c:ser` elements in this chart,
+ ordered first by the document order of the containing xChart element,
+ then by their ordering within the xChart element (not necessarily
+ document order).
+ """
+ return tuple(self.iter_sers())
+
+ @property
+ def xCharts(self):
+ """
+ Return a sequence containing all the `c:{x}Chart` elements in this
+ chart, in document order.
+ """
+ return tuple(self.iter_xCharts())
+
+
+class CT_Style(BaseOxmlElement):
+ """
+ ``<c:style>`` element; defines the chart style.
+ """
+
+ val = RequiredAttribute("val", ST_Style)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/datalabel.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/datalabel.py
new file mode 100644
index 00000000..b6aac2fd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/datalabel.py
@@ -0,0 +1,252 @@
+"""Chart data-label related oxml objects."""
+
+from __future__ import annotations
+
+from pptx.enum.chart import XL_DATA_LABEL_POSITION
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.text import CT_TextBody
+from pptx.oxml.xmlchemy import (
+ BaseOxmlElement,
+ OneAndOnlyOne,
+ RequiredAttribute,
+ ZeroOrMore,
+ ZeroOrOne,
+)
+
+
+class CT_DLbl(BaseOxmlElement):
+ """
+ ``<c:dLbl>`` element specifying the properties of the data label for an
+ individual data point.
+ """
+
+ _tag_seq = (
+ "c:idx",
+ "c:layout",
+ "c:tx",
+ "c:numFmt",
+ "c:spPr",
+ "c:txPr",
+ "c:dLblPos",
+ "c:showLegendKey",
+ "c:showVal",
+ "c:showCatName",
+ "c:showSerName",
+ "c:showPercent",
+ "c:showBubbleSize",
+ "c:separator",
+ "c:extLst",
+ )
+ idx = OneAndOnlyOne("c:idx")
+ tx = ZeroOrOne("c:tx", successors=_tag_seq[3:])
+ spPr = ZeroOrOne("c:spPr", successors=_tag_seq[5:])
+ txPr = ZeroOrOne("c:txPr", successors=_tag_seq[6:])
+ dLblPos = ZeroOrOne("c:dLblPos", successors=_tag_seq[7:])
+ del _tag_seq
+
+ def get_or_add_rich(self):
+ """
+ Return the `c:rich` descendant representing the text frame of the
+ data label, newly created if not present. Any existing `c:strRef`
+ element is removed along with its contents.
+ """
+ tx = self.get_or_add_tx()
+ tx._remove_strRef()
+ return tx.get_or_add_rich()
+
+ def get_or_add_tx_rich(self):
+ """
+ Return the `c:tx[c:rich]` subtree, newly created if not present.
+ """
+ tx = self.get_or_add_tx()
+ tx._remove_strRef()
+ tx.get_or_add_rich()
+ return tx
+
+ @property
+ def idx_val(self):
+ """
+ The integer value of the `val` attribute on the required `c:idx`
+ child.
+ """
+ return self.idx.val
+
+ @classmethod
+ def new_dLbl(cls):
+ """Return a newly created "loose" `c:dLbl` element.
+
+ The `c:dLbl` element contains the same (fairly extensive) default
+ subtree added by PowerPoint when an individual data label is
+ customized in the UI. Note that the idx value must be set by the
+ client. Failure to set the idx value will likely result in any
+ changes not being visible and may result in a repair error on open.
+ """
+ return parse_xml(
+ "<c:dLbl %s>\n"
+ ' <c:idx val="666"/>\n'
+ " <c:spPr/>\n"
+ " <c:txPr>\n"
+ " <a:bodyPr/>\n"
+ " <a:lstStyle/>\n"
+ " <a:p>\n"
+ " <a:pPr>\n"
+ " <a:defRPr/>\n"
+ " </a:pPr>\n"
+ " </a:p>\n"
+ " </c:txPr>\n"
+ ' <c:showLegendKey val="0"/>\n'
+ ' <c:showVal val="1"/>\n'
+ ' <c:showCatName val="0"/>\n'
+ ' <c:showSerName val="0"/>\n'
+ ' <c:showPercent val="0"/>\n'
+ ' <c:showBubbleSize val="0"/>\n'
+ "</c:dLbl>" % nsdecls("c", "a")
+ )
+
+ def remove_tx_rich(self):
+ """
+ Remove any `c:tx[c:rich]` child, or do nothing if not present.
+ """
+ matches = self.xpath("c:tx[c:rich]")
+ if not matches:
+ return
+ tx = matches[0]
+ self.remove(tx)
+
+ def _new_txPr(self):
+ return CT_TextBody.new_txPr()
+
+
+class CT_DLblPos(BaseOxmlElement):
+ """
+ ``<c:dLblPos>`` element specifying the positioning of a data label with
+ respect to its data point.
+ """
+
+ val = RequiredAttribute("val", XL_DATA_LABEL_POSITION)
+
+
+class CT_DLbls(BaseOxmlElement):
+ """`c:dLbls` element specifying properties for a set of data labels."""
+
+ _tag_seq = (
+ "c:dLbl",
+ "c:numFmt",
+ "c:spPr",
+ "c:txPr",
+ "c:dLblPos",
+ "c:showLegendKey",
+ "c:showVal",
+ "c:showCatName",
+ "c:showSerName",
+ "c:showPercent",
+ "c:showBubbleSize",
+ "c:separator",
+ "c:showLeaderLines",
+ "c:leaderLines",
+ "c:extLst",
+ )
+ dLbl = ZeroOrMore("c:dLbl", successors=_tag_seq[1:])
+ numFmt = ZeroOrOne("c:numFmt", successors=_tag_seq[2:])
+ txPr = ZeroOrOne("c:txPr", successors=_tag_seq[4:])
+ dLblPos = ZeroOrOne("c:dLblPos", successors=_tag_seq[5:])
+ showLegendKey = ZeroOrOne("c:showLegendKey", successors=_tag_seq[6:])
+ showVal = ZeroOrOne("c:showVal", successors=_tag_seq[7:])
+ showCatName = ZeroOrOne("c:showCatName", successors=_tag_seq[8:])
+ showSerName = ZeroOrOne("c:showSerName", successors=_tag_seq[9:])
+ showPercent = ZeroOrOne("c:showPercent", successors=_tag_seq[10:])
+ del _tag_seq
+
+ @property
+ def defRPr(self):
+ """
+ ``<a:defRPr>`` great-great-grandchild element, added with its
+ ancestors if not present.
+ """
+ txPr = self.get_or_add_txPr()
+ defRPr = txPr.defRPr
+ return defRPr
+
+ def get_dLbl_for_point(self, idx):
+ """
+ Return the `c:dLbl` child representing the label for the data point
+ at index *idx*.
+ """
+ matches = self.xpath('c:dLbl[c:idx[@val="%d"]]' % idx)
+ if matches:
+ return matches[0]
+ return None
+
+ def get_or_add_dLbl_for_point(self, idx):
+ """
+ Return the `c:dLbl` element representing the label of the point at
+ index *idx*.
+ """
+ matches = self.xpath('c:dLbl[c:idx[@val="%d"]]' % idx)
+ if matches:
+ return matches[0]
+ return self._insert_dLbl_in_sequence(idx)
+
+ @classmethod
+ def new_dLbls(cls):
+ """Return a newly created "loose" `c:dLbls` element."""
+ return parse_xml(
+ "<c:dLbls %s>\n"
+ ' <c:showLegendKey val="0"/>\n'
+ ' <c:showVal val="0"/>\n'
+ ' <c:showCatName val="0"/>\n'
+ ' <c:showSerName val="0"/>\n'
+ ' <c:showPercent val="0"/>\n'
+ ' <c:showBubbleSize val="0"/>\n'
+ ' <c:showLeaderLines val="1"/>\n'
+ "</c:dLbls>" % nsdecls("c")
+ )
+
+ def _insert_dLbl_in_sequence(self, idx):
+ """
+ Return a newly created `c:dLbl` element having `c:idx` child of *idx*
+ and inserted in numeric sequence among the `c:dLbl` children of this
+ element.
+ """
+ new_dLbl = self._new_dLbl()
+ new_dLbl.idx.val = idx
+
+ dLbl = None
+ for dLbl in self.dLbl_lst:
+ if dLbl.idx_val > idx:
+ dLbl.addprevious(new_dLbl)
+ return new_dLbl
+ if dLbl is not None:
+ dLbl.addnext(new_dLbl)
+ else:
+ self.insert(0, new_dLbl)
+ return new_dLbl
+
+ def _new_dLbl(self):
+ return CT_DLbl.new_dLbl()
+
+ def _new_showCatName(self):
+ """Return a new `c:showCatName` with value initialized.
+
+ This method is called by the metaclass-generated code whenever a new
+ `c:showCatName` element is required. In this case, it defaults to
+ `val=true`, which is not what we need so we override to make val
+ explicitly False.
+ """
+ return parse_xml('<c:showCatName %s val="0"/>' % nsdecls("c"))
+
+ def _new_showLegendKey(self):
+ return parse_xml('<c:showLegendKey %s val="0"/>' % nsdecls("c"))
+
+ def _new_showPercent(self):
+ return parse_xml('<c:showPercent %s val="0"/>' % nsdecls("c"))
+
+ def _new_showSerName(self):
+ return parse_xml('<c:showSerName %s val="0"/>' % nsdecls("c"))
+
+ def _new_showVal(self):
+ return parse_xml('<c:showVal %s val="0"/>' % nsdecls("c"))
+
+ def _new_txPr(self):
+ return CT_TextBody.new_txPr()
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/legend.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/legend.py
new file mode 100644
index 00000000..196ca15d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/legend.py
@@ -0,0 +1,72 @@
+"""lxml custom element classes for legend-related XML elements."""
+
+from __future__ import annotations
+
+from pptx.enum.chart import XL_LEGEND_POSITION
+from pptx.oxml.text import CT_TextBody
+from pptx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, ZeroOrOne
+
+
+class CT_Legend(BaseOxmlElement):
+ """
+ ``<c:legend>`` custom element class
+ """
+
+ _tag_seq = (
+ "c:legendPos",
+ "c:legendEntry",
+ "c:layout",
+ "c:overlay",
+ "c:spPr",
+ "c:txPr",
+ "c:extLst",
+ )
+ legendPos = ZeroOrOne("c:legendPos", successors=_tag_seq[1:])
+ layout = ZeroOrOne("c:layout", successors=_tag_seq[3:])
+ overlay = ZeroOrOne("c:overlay", successors=_tag_seq[4:])
+ txPr = ZeroOrOne("c:txPr", successors=_tag_seq[6:])
+ del _tag_seq
+
+ @property
+ def defRPr(self):
+ """
+ `./c:txPr/a:p/a:pPr/a:defRPr` great-great-grandchild element, added
+ with its ancestors if not present.
+ """
+ txPr = self.get_or_add_txPr()
+ defRPr = txPr.defRPr
+ return defRPr
+
+ @property
+ def horz_offset(self):
+ """
+ The float value in ./c:layout/c:manualLayout/c:x when
+ ./c:layout/c:manualLayout/c:xMode@val == "factor". 0.0 if that
+ XPath expression has no match.
+ """
+ layout = self.layout
+ if layout is None:
+ return 0.0
+ return layout.horz_offset
+
+ @horz_offset.setter
+ def horz_offset(self, offset):
+ """
+ Set the value of ./c:layout/c:manualLayout/c:x@val to *offset* and
+ ./c:layout/c:manualLayout/c:xMode@val to "factor". Remove
+ ./c:layout/c:manualLayout if *offset* == 0.
+ """
+ layout = self.get_or_add_layout()
+ layout.horz_offset = offset
+
+ def _new_txPr(self):
+ return CT_TextBody.new_txPr()
+
+
+class CT_LegendPos(BaseOxmlElement):
+ """
+ ``<c:legendPos>`` element specifying position of legend with respect to
+ chart as a member of ST_LegendPos.
+ """
+
+ val = OptionalAttribute("val", XL_LEGEND_POSITION, default=XL_LEGEND_POSITION.RIGHT)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/marker.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/marker.py
new file mode 100644
index 00000000..34afd13d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/marker.py
@@ -0,0 +1,61 @@
+"""Series-related oxml objects."""
+
+from __future__ import annotations
+
+from pptx.enum.chart import XL_MARKER_STYLE
+from pptx.oxml.simpletypes import ST_MarkerSize
+from pptx.oxml.xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrOne
+
+
+class CT_Marker(BaseOxmlElement):
+ """
+ `c:marker` custom element class, containing visual properties for a data
+ point marker on line-type charts.
+ """
+
+ _tag_seq = ("c:symbol", "c:size", "c:spPr", "c:extLst")
+ symbol = ZeroOrOne("c:symbol", successors=_tag_seq[1:])
+ size = ZeroOrOne("c:size", successors=_tag_seq[2:])
+ spPr = ZeroOrOne("c:spPr", successors=_tag_seq[3:])
+ del _tag_seq
+
+ @property
+ def size_val(self):
+ """
+ Return the value of `./c:size/@val`, specifying the size of this
+ marker in points. Returns |None| if no `c:size` element is present or
+ its val attribute is not present.
+ """
+ size = self.size
+ if size is None:
+ return None
+ return size.val
+
+ @property
+ def symbol_val(self):
+ """
+ Return the value of `./c:symbol/@val`, specifying the shape of this
+ marker. Returns |None| if no `c:symbol` element is present.
+ """
+ symbol = self.symbol
+ if symbol is None:
+ return None
+ return symbol.val
+
+
+class CT_MarkerSize(BaseOxmlElement):
+ """
+ `c:size` custom element class, specifying the size (in points) of a data
+ point marker for a line, XY, or radar chart.
+ """
+
+ val = RequiredAttribute("val", ST_MarkerSize)
+
+
+class CT_MarkerStyle(BaseOxmlElement):
+ """
+ `c:symbol` custom element class, specifying the shape of a data point
+ marker for a line, XY, or radar chart.
+ """
+
+ val = RequiredAttribute("val", XL_MARKER_STYLE)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/plot.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/plot.py
new file mode 100644
index 00000000..9c695a43
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/plot.py
@@ -0,0 +1,345 @@
+"""Plot-related oxml objects."""
+
+from __future__ import annotations
+
+from pptx.oxml.chart.datalabel import CT_DLbls
+from pptx.oxml.simpletypes import (
+ ST_BarDir,
+ ST_BubbleScale,
+ ST_GapAmount,
+ ST_Grouping,
+ ST_Overlap,
+)
+from pptx.oxml.xmlchemy import (
+ BaseOxmlElement,
+ OneAndOnlyOne,
+ OptionalAttribute,
+ ZeroOrMore,
+ ZeroOrOne,
+)
+
+
+class BaseChartElement(BaseOxmlElement):
+ """
+ Base class for barChart, lineChart, and other plot elements.
+ """
+
+ @property
+ def cat(self):
+ """
+ Return the `c:cat` element of the first series in this xChart, or
+ |None| if not present.
+ """
+ cats = self.xpath("./c:ser[1]/c:cat")
+ return cats[0] if cats else None
+
+ @property
+ def cat_pt_count(self):
+ """
+ Return the value of the `c:ptCount` descendent of this xChart
+ element. Its parent can be one of three element types. This value
+ represents the true number of (leaf) categories, although they might
+ not all have a corresponding `c:pt` sibling; a category with no label
+ does not get a `c:pt` element. Returns 0 if there is no `c:ptCount`
+ descendent.
+ """
+ cat_ptCounts = self.xpath("./c:ser//c:cat//c:ptCount")
+ if not cat_ptCounts:
+ return 0
+ return cat_ptCounts[0].val
+
+ @property
+ def cat_pts(self):
+ """
+ Return a sequence representing the `c:pt` elements under the `c:cat`
+ element of the first series in this xChart element. A category having
+ no value will have no corresponding `c:pt` element; |None| will
+ appear in that position in such cases. Items appear in `idx` order.
+ Only those in the first ``<c:lvl>`` element are included in the case
+ of multi-level categories.
+ """
+ cat_pts = self.xpath("./c:ser[1]/c:cat//c:lvl[1]/c:pt")
+ if not cat_pts:
+ cat_pts = self.xpath("./c:ser[1]/c:cat//c:pt")
+
+ cat_pt_dict = dict((pt.idx, pt) for pt in cat_pts)
+
+ return [cat_pt_dict.get(idx, None) for idx in range(self.cat_pt_count)]
+
+ @property
+ def grouping_val(self):
+ """
+ Return the value of the ``./c:grouping{val=?}`` attribute, taking
+ defaults into account when items are not present.
+ """
+ grouping = self.grouping
+ if grouping is None:
+ return ST_Grouping.STANDARD
+ val = grouping.val
+ if val is None:
+ return ST_Grouping.STANDARD
+ return val
+
+ def iter_sers(self):
+ """
+ Generate each ``<c:ser>`` child element in this xChart in
+ c:order/@val sequence (not document or c:idx order).
+ """
+
+ def ser_order(ser):
+ return ser.order.val
+
+ return (ser for ser in sorted(self.xpath("./c:ser"), key=ser_order))
+
+ @property
+ def sers(self):
+ """
+ Sequence of ``<c:ser>`` child elements in this xChart in c:order/@val
+ sequence (not document or c:idx order).
+ """
+ return tuple(self.iter_sers())
+
+ def _new_dLbls(self):
+ return CT_DLbls.new_dLbls()
+
+
+class CT_Area3DChart(BaseChartElement):
+ """
+ ``<c:area3DChart>`` element.
+ """
+
+ grouping = ZeroOrOne(
+ "c:grouping",
+ successors=(
+ "c:varyColors",
+ "c:ser",
+ "c:dLbls",
+ "c:dropLines",
+ "c:gapDepth",
+ "c:axId",
+ ),
+ )
+
+
+class CT_AreaChart(BaseChartElement):
+ """
+ ``<c:areaChart>`` element.
+ """
+
+ _tag_seq = (
+ "c:grouping",
+ "c:varyColors",
+ "c:ser",
+ "c:dLbls",
+ "c:dropLines",
+ "c:axId",
+ "c:extLst",
+ )
+ grouping = ZeroOrOne("c:grouping", successors=_tag_seq[1:])
+ varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[2:])
+ ser = ZeroOrMore("c:ser", successors=_tag_seq[3:])
+ dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[4:])
+ del _tag_seq
+
+
+class CT_BarChart(BaseChartElement):
+ """
+ ``<c:barChart>`` element.
+ """
+
+ _tag_seq = (
+ "c:barDir",
+ "c:grouping",
+ "c:varyColors",
+ "c:ser",
+ "c:dLbls",
+ "c:gapWidth",
+ "c:overlap",
+ "c:serLines",
+ "c:axId",
+ "c:extLst",
+ )
+ barDir = OneAndOnlyOne("c:barDir")
+ grouping = ZeroOrOne("c:grouping", successors=_tag_seq[2:])
+ varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[3:])
+ ser = ZeroOrMore("c:ser", successors=_tag_seq[4:])
+ dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[5:])
+ gapWidth = ZeroOrOne("c:gapWidth", successors=_tag_seq[6:])
+ overlap = ZeroOrOne("c:overlap", successors=_tag_seq[7:])
+ del _tag_seq
+
+ @property
+ def grouping_val(self):
+ """
+ Return the value of the ``./c:grouping{val=?}`` attribute, taking
+ defaults into account when items are not present.
+ """
+ grouping = self.grouping
+ if grouping is None:
+ return ST_Grouping.CLUSTERED
+ val = grouping.val
+ if val is None:
+ return ST_Grouping.CLUSTERED
+ return val
+
+
+class CT_BarDir(BaseOxmlElement):
+ """
+ ``<c:barDir>`` child of a barChart element, specifying the orientation of
+ the bars, 'bar' if they are horizontal and 'col' if they are vertical.
+ """
+
+ val = OptionalAttribute("val", ST_BarDir, default=ST_BarDir.COL)
+
+
+class CT_BubbleChart(BaseChartElement):
+ """
+ ``<c:bubbleChart>`` custom element class
+ """
+
+ _tag_seq = (
+ "c:varyColors",
+ "c:ser",
+ "c:dLbls",
+ "c:axId",
+ "c:bubble3D",
+ "c:bubbleScale",
+ "c:showNegBubbles",
+ "c:sizeRepresents",
+ "c:axId",
+ "c:extLst",
+ )
+ ser = ZeroOrMore("c:ser", successors=_tag_seq[2:])
+ dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[3:])
+ bubble3D = ZeroOrOne("c:bubble3D", successors=_tag_seq[5:])
+ bubbleScale = ZeroOrOne("c:bubbleScale", successors=_tag_seq[6:])
+ del _tag_seq
+
+
+class CT_BubbleScale(BaseChartElement):
+ """
+ ``<c:bubbleScale>`` custom element class
+ """
+
+ val = OptionalAttribute("val", ST_BubbleScale, default=100)
+
+
+class CT_DoughnutChart(BaseChartElement):
+ """
+ ``<c:doughnutChart>`` element.
+ """
+
+ _tag_seq = (
+ "c:varyColors",
+ "c:ser",
+ "c:dLbls",
+ "c:firstSliceAng",
+ "c:holeSize",
+ "c:extLst",
+ )
+ varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[1:])
+ ser = ZeroOrMore("c:ser", successors=_tag_seq[2:])
+ dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[3:])
+ del _tag_seq
+
+
+class CT_GapAmount(BaseOxmlElement):
+ """
+ ``<c:gapWidth>`` child of ``<c:barChart>`` element, also used for other
+ purposes like error bars.
+ """
+
+ val = OptionalAttribute("val", ST_GapAmount, default=150)
+
+
+class CT_Grouping(BaseOxmlElement):
+ """
+ ``<c:grouping>`` child of an xChart element, specifying a value like
+ 'clustered' or 'stacked'. Also used for variants with the same tag name
+ like CT_BarGrouping.
+ """
+
+ val = OptionalAttribute("val", ST_Grouping)
+
+
+class CT_LineChart(BaseChartElement):
+ """
+ ``<c:lineChart>`` custom element class
+ """
+
+ _tag_seq = (
+ "c:grouping",
+ "c:varyColors",
+ "c:ser",
+ "c:dLbls",
+ "c:dropLines",
+ "c:hiLowLines",
+ "c:upDownBars",
+ "c:marker",
+ "c:smooth",
+ "c:axId",
+ "c:extLst",
+ )
+ grouping = ZeroOrOne("c:grouping", successors=(_tag_seq[1:]))
+ varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[2:])
+ ser = ZeroOrMore("c:ser", successors=_tag_seq[3:])
+ dLbls = ZeroOrOne("c:dLbls", successors=(_tag_seq[4:]))
+ del _tag_seq
+
+
+class CT_Overlap(BaseOxmlElement):
+ """
+ ``<c:overlap>`` element specifying bar overlap as an integer percentage
+ of bar width, in range -100 to 100.
+ """
+
+ val = OptionalAttribute("val", ST_Overlap, default=0)
+
+
+class CT_PieChart(BaseChartElement):
+ """
+ ``<c:pieChart>`` custom element class
+ """
+
+ _tag_seq = ("c:varyColors", "c:ser", "c:dLbls", "c:firstSliceAng", "c:extLst")
+ varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[1:])
+ ser = ZeroOrMore("c:ser", successors=_tag_seq[2:])
+ dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[3:])
+ del _tag_seq
+
+
+class CT_RadarChart(BaseChartElement):
+ """
+ ``<c:radarChart>`` custom element class
+ """
+
+ _tag_seq = (
+ "c:radarStyle",
+ "c:varyColors",
+ "c:ser",
+ "c:dLbls",
+ "c:axId",
+ "c:extLst",
+ )
+ varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[2:])
+ ser = ZeroOrMore("c:ser", successors=_tag_seq[3:])
+ dLbls = ZeroOrOne("c:dLbls", successors=(_tag_seq[4:]))
+ del _tag_seq
+
+
+class CT_ScatterChart(BaseChartElement):
+ """
+ ``<c:scatterChart>`` custom element class
+ """
+
+ _tag_seq = (
+ "c:scatterStyle",
+ "c:varyColors",
+ "c:ser",
+ "c:dLbls",
+ "c:axId",
+ "c:extLst",
+ )
+ varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[2:])
+ ser = ZeroOrMore("c:ser", successors=_tag_seq[3:])
+ del _tag_seq
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/series.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/series.py
new file mode 100644
index 00000000..9264d552
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/series.py
@@ -0,0 +1,254 @@
+"""Series-related oxml objects."""
+
+from __future__ import annotations
+
+from pptx.oxml.chart.datalabel import CT_DLbls
+from pptx.oxml.simpletypes import XsdUnsignedInt
+from pptx.oxml.xmlchemy import (
+ BaseOxmlElement,
+ OneAndOnlyOne,
+ OxmlElement,
+ RequiredAttribute,
+ ZeroOrMore,
+ ZeroOrOne,
+)
+
+
+class CT_AxDataSource(BaseOxmlElement):
+ """
+ ``<c:cat>`` custom element class used in category charts to specify
+ category labels and hierarchy.
+ """
+
+ multiLvlStrRef = ZeroOrOne("c:multiLvlStrRef", successors=())
+
+ @property
+ def lvls(self):
+ """
+ Return a list containing the `c:lvl` descendent elements in document
+ order. These will only be present when the required single child
+ is a `c:multiLvlStrRef` element. Returns an empty list when no
+ `c:lvl` descendent elements are present.
+ """
+ return self.xpath(".//c:lvl")
+
+
+class CT_DPt(BaseOxmlElement):
+ """
+ ``<c:dPt>`` custom element class, containing visual properties for a data
+ point.
+ """
+
+ _tag_seq = (
+ "c:idx",
+ "c:invertIfNegative",
+ "c:marker",
+ "c:bubble3D",
+ "c:explosion",
+ "c:spPr",
+ "c:pictureOptions",
+ "c:extLst",
+ )
+ idx = OneAndOnlyOne("c:idx")
+ marker = ZeroOrOne("c:marker", successors=_tag_seq[3:])
+ spPr = ZeroOrOne("c:spPr", successors=_tag_seq[6:])
+ del _tag_seq
+
+ @classmethod
+ def new_dPt(cls):
+ """
+ Return a newly created "loose" `c:dPt` element containing its default
+ subtree.
+ """
+ dPt = OxmlElement("c:dPt")
+ dPt.append(OxmlElement("c:idx"))
+ return dPt
+
+
+class CT_Lvl(BaseOxmlElement):
+ """
+ ``<c:lvl>`` custom element class used in multi-level categories to
+ specify a level of hierarchy.
+ """
+
+ pt = ZeroOrMore("c:pt", successors=())
+
+
+class CT_NumDataSource(BaseOxmlElement):
+ """
+ ``<c:yVal>`` custom element class used in XY and bubble charts, and
+ perhaps others.
+ """
+
+ numRef = OneAndOnlyOne("c:numRef")
+
+ @property
+ def ptCount_val(self):
+ """
+ Return the value of `./c:numRef/c:numCache/c:ptCount/@val`,
+ specifying how many `c:pt` elements are in this numeric data cache.
+ Returns 0 if no `c:ptCount` element is present, as this is the least
+ disruptive way to degrade when no cached point data is available.
+ This situation is not expected, but is valid according to the schema.
+ """
+ results = self.xpath(".//c:ptCount/@val")
+ return int(results[0]) if results else 0
+
+ def pt_v(self, idx):
+ """
+ Return the Y value for data point *idx* in this cache, or None if no
+ value is present for that data point.
+ """
+ results = self.xpath(".//c:pt[@idx=%d]" % idx)
+ return results[0].value if results else None
+
+
+class CT_SeriesComposite(BaseOxmlElement):
+ """
+ ``<c:ser>`` custom element class. Note there are several different series
+ element types in the schema, such as ``CT_LineSer`` and ``CT_BarSer``,
+ but they all share the same tag name. This class acts as a composite and
+ depends on the caller not to do anything invalid for a series belonging
+ to a particular plot type.
+ """
+
+ _tag_seq = (
+ "c:idx",
+ "c:order",
+ "c:tx",
+ "c:spPr",
+ "c:invertIfNegative",
+ "c:pictureOptions",
+ "c:marker",
+ "c:explosion",
+ "c:dPt",
+ "c:dLbls",
+ "c:trendline",
+ "c:errBars",
+ "c:cat",
+ "c:val",
+ "c:xVal",
+ "c:yVal",
+ "c:shape",
+ "c:smooth",
+ "c:bubbleSize",
+ "c:bubble3D",
+ "c:extLst",
+ )
+ idx = OneAndOnlyOne("c:idx")
+ order = OneAndOnlyOne("c:order")
+ tx = ZeroOrOne("c:tx", successors=_tag_seq[3:])
+ spPr = ZeroOrOne("c:spPr", successors=_tag_seq[4:])
+ invertIfNegative = ZeroOrOne("c:invertIfNegative", successors=_tag_seq[5:])
+ marker = ZeroOrOne("c:marker", successors=_tag_seq[7:])
+ dPt = ZeroOrMore("c:dPt", successors=_tag_seq[9:])
+ dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[10:])
+ cat = ZeroOrOne("c:cat", successors=_tag_seq[13:])
+ val = ZeroOrOne("c:val", successors=_tag_seq[14:])
+ xVal = ZeroOrOne("c:xVal", successors=_tag_seq[15:])
+ yVal = ZeroOrOne("c:yVal", successors=_tag_seq[16:])
+ smooth = ZeroOrOne("c:smooth", successors=_tag_seq[18:])
+ bubbleSize = ZeroOrOne("c:bubbleSize", successors=_tag_seq[19:])
+ del _tag_seq
+
+ @property
+ def bubbleSize_ptCount_val(self):
+ """
+ Return the number of bubble size values as reflected in the `val`
+ attribute of `./c:bubbleSize//c:ptCount`, or 0 if not present.
+ """
+ vals = self.xpath("./c:bubbleSize//c:ptCount/@val")
+ if not vals:
+ return 0
+ return int(vals[0])
+
+ @property
+ def cat_ptCount_val(self):
+ """
+ Return the number of categories as reflected in the `val` attribute
+ of `./c:cat//c:ptCount`, or 0 if not present.
+ """
+ vals = self.xpath("./c:cat//c:ptCount/@val")
+ if not vals:
+ return 0
+ return int(vals[0])
+
+ def get_dLbl(self, idx):
+ """
+ Return the `c:dLbl` element representing the label for the data point
+ at offset *idx* in this series, or |None| if not present.
+ """
+ dLbls = self.dLbls
+ if dLbls is None:
+ return None
+ return dLbls.get_dLbl_for_point(idx)
+
+ def get_or_add_dLbl(self, idx):
+ """
+ Return the `c:dLbl` element representing the label of the point at
+ offset *idx* in this series, newly created if not yet present.
+ """
+ dLbls = self.get_or_add_dLbls()
+ return dLbls.get_or_add_dLbl_for_point(idx)
+
+ def get_or_add_dPt_for_point(self, idx):
+ """
+ Return the `c:dPt` child representing the visual properties of the
+ data point at index *idx*.
+ """
+ matches = self.xpath('c:dPt[c:idx[@val="%d"]]' % idx)
+ if matches:
+ return matches[0]
+ dPt = self._add_dPt()
+ dPt.idx.val = idx
+ return dPt
+
+ @property
+ def xVal_ptCount_val(self):
+ """
+ Return the number of X values as reflected in the `val` attribute of
+ `./c:xVal//c:ptCount`, or 0 if not present.
+ """
+ vals = self.xpath("./c:xVal//c:ptCount/@val")
+ if not vals:
+ return 0
+ return int(vals[0])
+
+ @property
+ def yVal_ptCount_val(self):
+ """
+ Return the number of Y values as reflected in the `val` attribute of
+ `./c:yVal//c:ptCount`, or 0 if not present.
+ """
+ vals = self.xpath("./c:yVal//c:ptCount/@val")
+ if not vals:
+ return 0
+ return int(vals[0])
+
+ def _new_dLbls(self):
+ """Override metaclass method that creates `c:dLbls` element."""
+ return CT_DLbls.new_dLbls()
+
+ def _new_dPt(self):
+ """
+ Overrides the metaclass generated method to get `c:dPt` with minimal
+ subtree.
+ """
+ return CT_DPt.new_dPt()
+
+
+class CT_StrVal_NumVal_Composite(BaseOxmlElement):
+ """
+ ``<c:pt>`` element, can be either CT_StrVal or CT_NumVal complex type.
+ Using this class for both, differentiating as needed.
+ """
+
+ v = OneAndOnlyOne("c:v")
+ idx = RequiredAttribute("idx", XsdUnsignedInt)
+
+ @property
+ def value(self):
+ """
+ The float value of the text in the required ``<c:v>`` child.
+ """
+ return float(self.v.text)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/chart/shared.py b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/shared.py
new file mode 100644
index 00000000..5515aa4b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/chart/shared.py
@@ -0,0 +1,219 @@
+"""Shared oxml objects for charts."""
+
+from __future__ import annotations
+
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.simpletypes import (
+ ST_LayoutMode,
+ XsdBoolean,
+ XsdDouble,
+ XsdString,
+ XsdUnsignedInt,
+)
+from pptx.oxml.xmlchemy import (
+ BaseOxmlElement,
+ OptionalAttribute,
+ RequiredAttribute,
+ ZeroOrOne,
+)
+
+
+class CT_Boolean(BaseOxmlElement):
+ """
+ Common complex type used for elements having a True/False value.
+ """
+
+ val = OptionalAttribute("val", XsdBoolean, default=True)
+
+
+class CT_Boolean_Explicit(BaseOxmlElement):
+ """Always spells out the `val` attribute, e.g. `val=1`.
+
+ At least one boolean element is improperly interpreted by one or more
+ versions of PowerPoint. The `c:overlay` element is interpreted as |False|
+ when no `val` attribute is present, contrary to the behavior described in
+ the schema. A remedy for this is to interpret a missing `val` attribute
+ as |True| (consistent with the spec), but always write the attribute
+ whenever there is occasion for changing the element.
+ """
+
+ _val = OptionalAttribute("val", XsdBoolean, default=True)
+
+ @property
+ def val(self):
+ return self._val
+
+ @val.setter
+ def val(self, value):
+ val_str = "1" if bool(value) is True else "0"
+ self.set("val", val_str)
+
+
+class CT_Double(BaseOxmlElement):
+ """
+ Used for floating point values.
+ """
+
+ val = RequiredAttribute("val", XsdDouble)
+
+
+class CT_Layout(BaseOxmlElement):
+ """
+ ``<c:layout>`` custom element class
+ """
+
+ manualLayout = ZeroOrOne("c:manualLayout", successors=("c:extLst",))
+
+ @property
+ def horz_offset(self):
+ """
+ The float value in ./c:manualLayout/c:x when
+ c:layout/c:manualLayout/c:xMode@val == "factor". 0.0 if that XPath
+ expression finds no match.
+ """
+ manualLayout = self.manualLayout
+ if manualLayout is None:
+ return 0.0
+ return manualLayout.horz_offset
+
+ @horz_offset.setter
+ def horz_offset(self, offset):
+ """
+ Set the value of ./c:manualLayout/c:x@val to *offset* and
+ ./c:manualLayout/c:xMode@val to "factor". Remove ./c:manualLayout if
+ *offset* == 0.
+ """
+ if offset == 0.0:
+ self._remove_manualLayout()
+ return
+ manualLayout = self.get_or_add_manualLayout()
+ manualLayout.horz_offset = offset
+
+
+class CT_LayoutMode(BaseOxmlElement):
+ """
+ Used for ``<c:xMode>``, ``<c:yMode>``, ``<c:wMode>``, and ``<c:hMode>``
+ child elements of CT_ManualLayout.
+ """
+
+ val = OptionalAttribute("val", ST_LayoutMode, default=ST_LayoutMode.FACTOR)
+
+
+class CT_ManualLayout(BaseOxmlElement):
+ """
+ ``<c:manualLayout>`` custom element class
+ """
+
+ _tag_seq = (
+ "c:layoutTarget",
+ "c:xMode",
+ "c:yMode",
+ "c:wMode",
+ "c:hMode",
+ "c:x",
+ "c:y",
+ "c:w",
+ "c:h",
+ "c:extLst",
+ )
+ xMode = ZeroOrOne("c:xMode", successors=_tag_seq[2:])
+ x = ZeroOrOne("c:x", successors=_tag_seq[6:])
+ del _tag_seq
+
+ @property
+ def horz_offset(self):
+ """
+ The float value in ./c:x@val when ./c:xMode@val == "factor". 0.0 when
+ ./c:x is not present or ./c:xMode@val != "factor".
+ """
+ x, xMode = self.x, self.xMode
+ if x is None or xMode is None or xMode.val != ST_LayoutMode.FACTOR:
+ return 0.0
+ return x.val
+
+ @horz_offset.setter
+ def horz_offset(self, offset):
+ """
+ Set the value of ./c:x@val to *offset* and ./c:xMode@val to "factor".
+ """
+ self.get_or_add_xMode().val = ST_LayoutMode.FACTOR
+ self.get_or_add_x().val = offset
+
+
+class CT_NumFmt(BaseOxmlElement):
+ """
+ ``<c:numFmt>`` element specifying the formatting for number labels on a
+ tick mark or data point.
+ """
+
+ formatCode = RequiredAttribute("formatCode", XsdString)
+ sourceLinked = OptionalAttribute("sourceLinked", XsdBoolean)
+
+
+class CT_Title(BaseOxmlElement):
+ """`c:title` custom element class."""
+
+ _tag_seq = ("c:tx", "c:layout", "c:overlay", "c:spPr", "c:txPr", "c:extLst")
+ tx = ZeroOrOne("c:tx", successors=_tag_seq[1:])
+ spPr = ZeroOrOne("c:spPr", successors=_tag_seq[4:])
+ del _tag_seq
+
+ def get_or_add_tx_rich(self):
+ """Return `c:tx/c:rich`, newly created if not present.
+
+ Return the `c:rich` grandchild at `c:tx/c:rich`. Both the `c:tx` and
+ `c:rich` elements are created if not already present. Any
+ `c:tx/c:strRef` element is removed. (Such an element would contain
+ a cell reference for the axis title text in the chart's Excel
+ worksheet.)
+ """
+ tx = self.get_or_add_tx()
+ tx._remove_strRef()
+ return tx.get_or_add_rich()
+
+ @property
+ def tx_rich(self):
+ """Return `c:tx/c:rich` or |None| if not present."""
+ richs = self.xpath("c:tx/c:rich")
+ if not richs:
+ return None
+ return richs[0]
+
+ @staticmethod
+ def new_title():
+ """Return "loose" `c:title` element containing default children."""
+ return parse_xml(
+ "<c:title %s>" " <c:layout/>" ' <c:overlay val="0"/>' "</c:title>" % nsdecls("c")
+ )
+
+
+class CT_Tx(BaseOxmlElement):
+ """
+ ``<c:tx>`` element containing the text for a label on a data point or
+ other chart item.
+ """
+
+ strRef = ZeroOrOne("c:strRef")
+ rich = ZeroOrOne("c:rich")
+
+ def _new_rich(self):
+ return parse_xml(
+ "<c:rich %s>"
+ " <a:bodyPr/>"
+ " <a:lstStyle/>"
+ " <a:p>"
+ " <a:pPr>"
+ " <a:defRPr/>"
+ " </a:pPr>"
+ " </a:p>"
+ "</c:rich>" % nsdecls("c", "a")
+ )
+
+
+class CT_UnsignedInt(BaseOxmlElement):
+ """
+ ``<c:idx>`` element and others.
+ """
+
+ val = RequiredAttribute("val", XsdUnsignedInt)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/coreprops.py b/.venv/lib/python3.12/site-packages/pptx/oxml/coreprops.py
new file mode 100644
index 00000000..de6b26b2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/coreprops.py
@@ -0,0 +1,288 @@
+"""lxml custom element classes for core properties-related XML elements."""
+
+from __future__ import annotations
+
+import datetime as dt
+import re
+from typing import Callable, cast
+
+from lxml.etree import _Element # pyright: ignore[reportPrivateUsage]
+
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls, qn
+from pptx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne
+
+
+class CT_CoreProperties(BaseOxmlElement):
+ """`cp:coreProperties` element.
+
+ The root element of the Core Properties part stored as `/docProps/core.xml`. Implements many
+ of the Dublin Core document metadata elements. String elements resolve to an empty string ('')
+ if the element is not present in the XML. String elements are limited in length to 255 unicode
+ characters.
+ """
+
+ get_or_add_revision: Callable[[], _Element]
+
+ category = ZeroOrOne("cp:category", successors=())
+ contentStatus = ZeroOrOne("cp:contentStatus", successors=())
+ created = ZeroOrOne("dcterms:created", successors=())
+ creator = ZeroOrOne("dc:creator", successors=())
+ description = ZeroOrOne("dc:description", successors=())
+ identifier = ZeroOrOne("dc:identifier", successors=())
+ keywords = ZeroOrOne("cp:keywords", successors=())
+ language = ZeroOrOne("dc:language", successors=())
+ lastModifiedBy = ZeroOrOne("cp:lastModifiedBy", successors=())
+ lastPrinted = ZeroOrOne("cp:lastPrinted", successors=())
+ modified = ZeroOrOne("dcterms:modified", successors=())
+ revision: _Element | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "cp:revision", successors=()
+ )
+ subject = ZeroOrOne("dc:subject", successors=())
+ title = ZeroOrOne("dc:title", successors=())
+ version = ZeroOrOne("cp:version", successors=())
+
+ _coreProperties_tmpl = "<cp:coreProperties %s/>\n" % nsdecls("cp", "dc", "dcterms")
+
+ @staticmethod
+ def new_coreProperties() -> CT_CoreProperties:
+ """Return a new `cp:coreProperties` element"""
+ return cast(CT_CoreProperties, parse_xml(CT_CoreProperties._coreProperties_tmpl))
+
+ @property
+ def author_text(self) -> str:
+ return self._text_of_element("creator")
+
+ @author_text.setter
+ def author_text(self, value: str):
+ self._set_element_text("creator", value)
+
+ @property
+ def category_text(self) -> str:
+ return self._text_of_element("category")
+
+ @category_text.setter
+ def category_text(self, value: str):
+ self._set_element_text("category", value)
+
+ @property
+ def comments_text(self) -> str:
+ return self._text_of_element("description")
+
+ @comments_text.setter
+ def comments_text(self, value: str):
+ self._set_element_text("description", value)
+
+ @property
+ def contentStatus_text(self) -> str:
+ return self._text_of_element("contentStatus")
+
+ @contentStatus_text.setter
+ def contentStatus_text(self, value: str):
+ self._set_element_text("contentStatus", value)
+
+ @property
+ def created_datetime(self):
+ return self._datetime_of_element("created")
+
+ @created_datetime.setter
+ def created_datetime(self, value: dt.datetime):
+ self._set_element_datetime("created", value)
+
+ @property
+ def identifier_text(self) -> str:
+ return self._text_of_element("identifier")
+
+ @identifier_text.setter
+ def identifier_text(self, value: str):
+ self._set_element_text("identifier", value)
+
+ @property
+ def keywords_text(self) -> str:
+ return self._text_of_element("keywords")
+
+ @keywords_text.setter
+ def keywords_text(self, value: str):
+ self._set_element_text("keywords", value)
+
+ @property
+ def language_text(self) -> str:
+ return self._text_of_element("language")
+
+ @language_text.setter
+ def language_text(self, value: str):
+ self._set_element_text("language", value)
+
+ @property
+ def lastModifiedBy_text(self) -> str:
+ return self._text_of_element("lastModifiedBy")
+
+ @lastModifiedBy_text.setter
+ def lastModifiedBy_text(self, value: str):
+ self._set_element_text("lastModifiedBy", value)
+
+ @property
+ def lastPrinted_datetime(self):
+ return self._datetime_of_element("lastPrinted")
+
+ @lastPrinted_datetime.setter
+ def lastPrinted_datetime(self, value: dt.datetime):
+ self._set_element_datetime("lastPrinted", value)
+
+ @property
+ def modified_datetime(self):
+ return self._datetime_of_element("modified")
+
+ @modified_datetime.setter
+ def modified_datetime(self, value: dt.datetime):
+ self._set_element_datetime("modified", value)
+
+ @property
+ def revision_number(self) -> int:
+ """Integer value of revision property."""
+ revision = self.revision
+ if revision is None:
+ return 0
+ revision_str = revision.text
+ if revision_str is None:
+ return 0
+ try:
+ revision = int(revision_str)
+ except ValueError:
+ # -- non-integer revision strings also resolve to 0 --
+ return 0
+ # -- as do negative integers --
+ if revision < 0:
+ return 0
+ return revision
+
+ @revision_number.setter
+ def revision_number(self, value: int):
+ """Set revision property to string value of integer `value`."""
+ if not isinstance(value, int) or value < 1: # pyright: ignore[reportUnnecessaryIsInstance]
+ tmpl = "revision property requires positive int, got '%s'"
+ raise ValueError(tmpl % value)
+ revision = self.get_or_add_revision()
+ revision.text = str(value)
+
+ @property
+ def subject_text(self) -> str:
+ return self._text_of_element("subject")
+
+ @subject_text.setter
+ def subject_text(self, value: str):
+ self._set_element_text("subject", value)
+
+ @property
+ def title_text(self) -> str:
+ return self._text_of_element("title")
+
+ @title_text.setter
+ def title_text(self, value: str):
+ self._set_element_text("title", value)
+
+ @property
+ def version_text(self) -> str:
+ return self._text_of_element("version")
+
+ @version_text.setter
+ def version_text(self, value: str):
+ self._set_element_text("version", value)
+
+ def _datetime_of_element(self, property_name: str) -> dt.datetime | None:
+ element = cast("_Element | None", getattr(self, property_name))
+ if element is None:
+ return None
+ datetime_str = element.text
+ if datetime_str is None:
+ return None
+ try:
+ return self._parse_W3CDTF_to_datetime(datetime_str)
+ except ValueError:
+ # invalid datetime strings are ignored
+ return None
+
+ def _get_or_add(self, prop_name: str):
+ """Return element returned by 'get_or_add_' method for `prop_name`."""
+ get_or_add_method_name = "get_or_add_%s" % prop_name
+ get_or_add_method = getattr(self, get_or_add_method_name)
+ element = get_or_add_method()
+ return element
+
+ @classmethod
+ def _offset_dt(cls, datetime: dt.datetime, offset_str: str):
+ """Return |datetime| instance offset from `datetime` by offset specified in `offset_str`.
+
+ `offset_str` is a string like `'-07:00'`.
+ """
+ match = cls._offset_pattern.match(offset_str)
+ if match is None:
+ raise ValueError(f"{repr(offset_str)} is not a valid offset string")
+ sign, hours_str, minutes_str = match.groups()
+ sign_factor = -1 if sign == "+" else 1
+ hours = int(hours_str) * sign_factor
+ minutes = int(minutes_str) * sign_factor
+ td = dt.timedelta(hours=hours, minutes=minutes)
+ return datetime + td
+
+ _offset_pattern = re.compile(r"([+-])(\d\d):(\d\d)")
+
+ @classmethod
+ def _parse_W3CDTF_to_datetime(cls, w3cdtf_str: str) -> dt.datetime:
+ # valid W3CDTF date cases:
+ # yyyy e.g. '2003'
+ # yyyy-mm e.g. '2003-12'
+ # yyyy-mm-dd e.g. '2003-12-31'
+ # UTC timezone e.g. '2003-12-31T10:14:55Z'
+ # numeric timezone e.g. '2003-12-31T10:14:55-08:00'
+ templates = ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%d", "%Y-%m", "%Y")
+ # strptime isn't smart enough to parse literal timezone offsets like
+ # '-07:30', so we have to do it ourselves
+ parseable_part = w3cdtf_str[:19]
+ offset_str = w3cdtf_str[19:]
+ timestamp = None
+ for tmpl in templates:
+ try:
+ timestamp = dt.datetime.strptime(parseable_part, tmpl)
+ except ValueError:
+ continue
+ if timestamp is None:
+ tmpl = "could not parse W3CDTF datetime string '%s'"
+ raise ValueError(tmpl % w3cdtf_str)
+ if len(offset_str) == 6:
+ return cls._offset_dt(timestamp, offset_str)
+ return timestamp
+
+ def _set_element_datetime(self, prop_name: str, value: dt.datetime) -> None:
+ """Set date/time value of child element having `prop_name` to `value`."""
+ if not isinstance(value, dt.datetime): # pyright: ignore[reportUnnecessaryIsInstance]
+ tmpl = "property requires <type 'datetime.datetime'> object, got %s"
+ raise ValueError(tmpl % type(value))
+ element = self._get_or_add(prop_name)
+ dt_str = value.strftime("%Y-%m-%dT%H:%M:%SZ")
+ element.text = dt_str
+ if prop_name in ("created", "modified"):
+ # These two require an explicit 'xsi:type="dcterms:W3CDTF"'
+ # attribute. The first and last line are a hack required to add
+ # the xsi namespace to the root element rather than each child
+ # element in which it is referenced
+ self.set(qn("xsi:foo"), "bar")
+ element.set(qn("xsi:type"), "dcterms:W3CDTF")
+ del self.attrib[qn("xsi:foo")]
+
+ def _set_element_text(self, prop_name: str, value: str) -> None:
+ """Set string value of `name` property to `value`."""
+ value = str(value)
+ if len(value) > 255:
+ tmpl = "exceeded 255 char limit for property, got:\n\n'%s'"
+ raise ValueError(tmpl % value)
+ element = self._get_or_add(prop_name)
+ element.text = value
+
+ def _text_of_element(self, property_name: str) -> str:
+ element = getattr(self, property_name)
+ if element is None:
+ return ""
+ if element.text is None:
+ return ""
+ return element.text
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/dml/__init__.py b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/dml/color.py b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/color.py
new file mode 100644
index 00000000..dfce90aa
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/color.py
@@ -0,0 +1,111 @@
+"""lxml custom element classes for DrawingML-related XML elements."""
+
+from __future__ import annotations
+
+from pptx.enum.dml import MSO_THEME_COLOR
+from pptx.oxml.simpletypes import ST_HexColorRGB, ST_Percentage
+from pptx.oxml.xmlchemy import (
+ BaseOxmlElement,
+ Choice,
+ RequiredAttribute,
+ ZeroOrOne,
+ ZeroOrOneChoice,
+)
+
+
+class _BaseColorElement(BaseOxmlElement):
+ """
+ Base class for <a:srgbClr> and <a:schemeClr> elements.
+ """
+
+ lumMod = ZeroOrOne("a:lumMod")
+ lumOff = ZeroOrOne("a:lumOff")
+
+ def add_lumMod(self, value):
+ """
+ Return a newly added <a:lumMod> child element.
+ """
+ lumMod = self._add_lumMod()
+ lumMod.val = value
+ return lumMod
+
+ def add_lumOff(self, value):
+ """
+ Return a newly added <a:lumOff> child element.
+ """
+ lumOff = self._add_lumOff()
+ lumOff.val = value
+ return lumOff
+
+ def clear_lum(self):
+ """
+ Return self after removing any <a:lumMod> and <a:lumOff> child
+ elements.
+ """
+ self._remove_lumMod()
+ self._remove_lumOff()
+ return self
+
+
+class CT_Color(BaseOxmlElement):
+ """Custom element class for `a:fgClr`, `a:bgClr` and perhaps others."""
+
+ eg_colorChoice = ZeroOrOneChoice(
+ (
+ Choice("a:scrgbClr"),
+ Choice("a:srgbClr"),
+ Choice("a:hslClr"),
+ Choice("a:sysClr"),
+ Choice("a:schemeClr"),
+ Choice("a:prstClr"),
+ ),
+ successors=(),
+ )
+
+
+class CT_HslColor(_BaseColorElement):
+ """
+ Custom element class for <a:hslClr> element.
+ """
+
+
+class CT_Percentage(BaseOxmlElement):
+ """
+ Custom element class for <a:lumMod> and <a:lumOff> elements.
+ """
+
+ val = RequiredAttribute("val", ST_Percentage)
+
+
+class CT_PresetColor(_BaseColorElement):
+ """
+ Custom element class for <a:prstClr> element.
+ """
+
+
+class CT_SchemeColor(_BaseColorElement):
+ """
+ Custom element class for <a:schemeClr> element.
+ """
+
+ val = RequiredAttribute("val", MSO_THEME_COLOR)
+
+
+class CT_ScRgbColor(_BaseColorElement):
+ """
+ Custom element class for <a:scrgbClr> element.
+ """
+
+
+class CT_SRgbColor(_BaseColorElement):
+ """
+ Custom element class for <a:srgbClr> element.
+ """
+
+ val = RequiredAttribute("val", ST_HexColorRGB)
+
+
+class CT_SystemColor(_BaseColorElement):
+ """
+ Custom element class for <a:sysClr> element.
+ """
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/dml/fill.py b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/fill.py
new file mode 100644
index 00000000..2ff2255d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/fill.py
@@ -0,0 +1,197 @@
+"""lxml custom element classes for DrawingML-related XML elements."""
+
+from __future__ import annotations
+
+from pptx.enum.dml import MSO_PATTERN_TYPE
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.simpletypes import (
+ ST_Percentage,
+ ST_PositiveFixedAngle,
+ ST_PositiveFixedPercentage,
+ ST_RelationshipId,
+)
+from pptx.oxml.xmlchemy import (
+ BaseOxmlElement,
+ Choice,
+ OneOrMore,
+ OptionalAttribute,
+ RequiredAttribute,
+ ZeroOrOne,
+ ZeroOrOneChoice,
+)
+
+
+class CT_Blip(BaseOxmlElement):
+ """
+ <a:blip> element
+ """
+
+ rEmbed = OptionalAttribute("r:embed", ST_RelationshipId)
+
+
+class CT_BlipFillProperties(BaseOxmlElement):
+ """
+ Custom element class for <a:blipFill> element.
+ """
+
+ _tag_seq = ("a:blip", "a:srcRect", "a:tile", "a:stretch")
+ blip = ZeroOrOne("a:blip", successors=_tag_seq[1:])
+ srcRect = ZeroOrOne("a:srcRect", successors=_tag_seq[2:])
+ del _tag_seq
+
+ def crop(self, cropping):
+ """
+ Set `a:srcRect` child to crop according to *cropping* values.
+ """
+ srcRect = self._add_srcRect()
+ srcRect.l, srcRect.t, srcRect.r, srcRect.b = cropping
+
+
+class CT_GradientFillProperties(BaseOxmlElement):
+ """`a:gradFill` custom element class."""
+
+ _tag_seq = ("a:gsLst", "a:lin", "a:path", "a:tileRect")
+ gsLst = ZeroOrOne("a:gsLst", successors=_tag_seq[1:])
+ lin = ZeroOrOne("a:lin", successors=_tag_seq[2:])
+ path = ZeroOrOne("a:path", successors=_tag_seq[3:])
+ del _tag_seq
+
+ @classmethod
+ def new_gradFill(cls):
+ """Return newly-created "loose" default gradient subtree."""
+ return parse_xml(
+ '<a:gradFill %s rotWithShape="1">\n'
+ " <a:gsLst>\n"
+ ' <a:gs pos="0">\n'
+ ' <a:schemeClr val="accent1">\n'
+ ' <a:tint val="100000"/>\n'
+ ' <a:shade val="100000"/>\n'
+ ' <a:satMod val="130000"/>\n'
+ " </a:schemeClr>\n"
+ " </a:gs>\n"
+ ' <a:gs pos="100000">\n'
+ ' <a:schemeClr val="accent1">\n'
+ ' <a:tint val="50000"/>\n'
+ ' <a:shade val="100000"/>\n'
+ ' <a:satMod val="350000"/>\n'
+ " </a:schemeClr>\n"
+ " </a:gs>\n"
+ " </a:gsLst>\n"
+ ' <a:lin scaled="0"/>\n'
+ "</a:gradFill>\n" % nsdecls("a")
+ )
+
+ def _new_gsLst(self):
+ """Override default to add minimum subtree."""
+ return CT_GradientStopList.new_gsLst()
+
+
+class CT_GradientStop(BaseOxmlElement):
+ """`a:gs` custom element class."""
+
+ eg_colorChoice = ZeroOrOneChoice(
+ (
+ Choice("a:scrgbClr"),
+ Choice("a:srgbClr"),
+ Choice("a:hslClr"),
+ Choice("a:sysClr"),
+ Choice("a:schemeClr"),
+ Choice("a:prstClr"),
+ ),
+ successors=(),
+ )
+ pos = RequiredAttribute("pos", ST_PositiveFixedPercentage)
+
+
+class CT_GradientStopList(BaseOxmlElement):
+ """`a:gsLst` custom element class."""
+
+ gs = OneOrMore("a:gs")
+
+ @classmethod
+ def new_gsLst(cls):
+ """Return newly-created "loose" default stop-list subtree.
+
+ An `a:gsLst` element must have at least two `a:gs` children. These
+ are the default from the PowerPoint built-in "White" template.
+ """
+ return parse_xml(
+ "<a:gsLst %s>\n"
+ ' <a:gs pos="0">\n'
+ ' <a:schemeClr val="accent1">\n'
+ ' <a:tint val="100000"/>\n'
+ ' <a:shade val="100000"/>\n'
+ ' <a:satMod val="130000"/>\n'
+ " </a:schemeClr>\n"
+ " </a:gs>\n"
+ ' <a:gs pos="100000">\n'
+ ' <a:schemeClr val="accent1">\n'
+ ' <a:tint val="50000"/>\n'
+ ' <a:shade val="100000"/>\n'
+ ' <a:satMod val="350000"/>\n'
+ " </a:schemeClr>\n"
+ " </a:gs>\n"
+ "</a:gsLst>\n" % nsdecls("a")
+ )
+
+
+class CT_GroupFillProperties(BaseOxmlElement):
+ """`a:grpFill` custom element class"""
+
+
+class CT_LinearShadeProperties(BaseOxmlElement):
+ """`a:lin` custom element class"""
+
+ ang = OptionalAttribute("ang", ST_PositiveFixedAngle)
+
+
+class CT_NoFillProperties(BaseOxmlElement):
+ """`a:noFill` custom element class"""
+
+
+class CT_PatternFillProperties(BaseOxmlElement):
+ """`a:pattFill` custom element class"""
+
+ _tag_seq = ("a:fgClr", "a:bgClr")
+ fgClr = ZeroOrOne("a:fgClr", successors=_tag_seq[1:])
+ bgClr = ZeroOrOne("a:bgClr", successors=_tag_seq[2:])
+ del _tag_seq
+ prst = OptionalAttribute("prst", MSO_PATTERN_TYPE)
+
+ def _new_bgClr(self):
+ """Override default to add minimum subtree."""
+ xml = ("<a:bgClr %s>\n" ' <a:srgbClr val="FFFFFF"/>\n' "</a:bgClr>\n") % nsdecls("a")
+ bgClr = parse_xml(xml)
+ return bgClr
+
+ def _new_fgClr(self):
+ """Override default to add minimum subtree."""
+ xml = ("<a:fgClr %s>\n" ' <a:srgbClr val="000000"/>\n' "</a:fgClr>\n") % nsdecls("a")
+ fgClr = parse_xml(xml)
+ return fgClr
+
+
+class CT_RelativeRect(BaseOxmlElement):
+ """`a:srcRect` element and perhaps others."""
+
+ l = OptionalAttribute("l", ST_Percentage, default=0.0) # noqa
+ t = OptionalAttribute("t", ST_Percentage, default=0.0)
+ r = OptionalAttribute("r", ST_Percentage, default=0.0)
+ b = OptionalAttribute("b", ST_Percentage, default=0.0)
+
+
+class CT_SolidColorFillProperties(BaseOxmlElement):
+ """`a:solidFill` custom element class."""
+
+ eg_colorChoice = ZeroOrOneChoice(
+ (
+ Choice("a:scrgbClr"),
+ Choice("a:srgbClr"),
+ Choice("a:hslClr"),
+ Choice("a:sysClr"),
+ Choice("a:schemeClr"),
+ Choice("a:prstClr"),
+ ),
+ successors=(),
+ )
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/dml/line.py b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/line.py
new file mode 100644
index 00000000..720ca8e0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/dml/line.py
@@ -0,0 +1,12 @@
+"""lxml custom element classes for DrawingML line-related XML elements."""
+
+from __future__ import annotations
+
+from pptx.enum.dml import MSO_LINE_DASH_STYLE
+from pptx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute
+
+
+class CT_PresetLineDashProperties(BaseOxmlElement):
+ """`a:prstDash` custom element class"""
+
+ val = OptionalAttribute("val", MSO_LINE_DASH_STYLE)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/ns.py b/.venv/lib/python3.12/site-packages/pptx/oxml/ns.py
new file mode 100644
index 00000000..d900c33b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/ns.py
@@ -0,0 +1,129 @@
+"""Namespace related objects."""
+
+from __future__ import annotations
+
+
+# -- Maps namespace prefix to namespace name for all known PowerPoint XML namespaces --
+_nsmap = {
+ "a": "http://schemas.openxmlformats.org/drawingml/2006/main",
+ "c": "http://schemas.openxmlformats.org/drawingml/2006/chart",
+ "cp": "http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
+ "ct": "http://schemas.openxmlformats.org/package/2006/content-types",
+ "dc": "http://purl.org/dc/elements/1.1/",
+ "dcmitype": "http://purl.org/dc/dcmitype/",
+ "dcterms": "http://purl.org/dc/terms/",
+ "ep": "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties",
+ "i": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
+ "m": "http://schemas.openxmlformats.org/officeDocument/2006/math",
+ "mo": "http://schemas.microsoft.com/office/mac/office/2008/main",
+ "mv": "urn:schemas-microsoft-com:mac:vml",
+ "o": "urn:schemas-microsoft-com:office:office",
+ "p": "http://schemas.openxmlformats.org/presentationml/2006/main",
+ "pd": "http://schemas.openxmlformats.org/drawingml/2006/presentationDrawing",
+ "pic": "http://schemas.openxmlformats.org/drawingml/2006/picture",
+ "pr": "http://schemas.openxmlformats.org/package/2006/relationships",
+ "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
+ "sl": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout",
+ "v": "urn:schemas-microsoft-com:vml",
+ "ve": "http://schemas.openxmlformats.org/markup-compatibility/2006",
+ "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
+ "w10": "urn:schemas-microsoft-com:office:word",
+ "wne": "http://schemas.microsoft.com/office/word/2006/wordml",
+ "wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
+ "xsi": "http://www.w3.org/2001/XMLSchema-instance",
+}
+
+pfxmap = {value: key for key, value in _nsmap.items()}
+
+
+class NamespacePrefixedTag(str):
+ """Value object that knows the semantics of an XML tag having a namespace prefix."""
+
+ def __new__(cls, nstag: str):
+ return super(NamespacePrefixedTag, cls).__new__(cls, nstag)
+
+ def __init__(self, nstag: str):
+ self._pfx, self._local_part = nstag.split(":")
+ self._ns_uri = _nsmap[self._pfx]
+
+ @classmethod
+ def from_clark_name(cls, clark_name: str) -> NamespacePrefixedTag:
+ nsuri, local_name = clark_name[1:].split("}")
+ nstag = "%s:%s" % (pfxmap[nsuri], local_name)
+ return cls(nstag)
+
+ @property
+ def clark_name(self):
+ return "{%s}%s" % (self._ns_uri, self._local_part)
+
+ @property
+ def local_part(self):
+ """
+ Return the local part of the tag as a string. E.g. 'foobar' is
+ returned for tag 'f:foobar'.
+ """
+ return self._local_part
+
+ @property
+ def nsmap(self):
+ """
+ Return a dict having a single member, mapping the namespace prefix of
+ this tag to it's namespace name (e.g. {'f': 'http://foo/bar'}). This
+ is handy for passing to xpath calls and other uses.
+ """
+ return {self._pfx: self._ns_uri}
+
+ @property
+ def nspfx(self):
+ """
+ Return the string namespace prefix for the tag, e.g. 'f' is returned
+ for tag 'f:foobar'.
+ """
+ return self._pfx
+
+ @property
+ def nsuri(self):
+ """
+ Return the namespace URI for the tag, e.g. 'http://foo/bar' would be
+ returned for tag 'f:foobar' if the 'f' prefix maps to
+ 'http://foo/bar' in _nsmap.
+ """
+ return self._ns_uri
+
+
+def namespaces(*prefixes: str):
+ """Return a dict containing the subset namespace prefix mappings specified by *prefixes*.
+
+ Any number of namespace prefixes can be supplied, e.g. namespaces('a', 'r', 'p').
+ """
+ return {pfx: _nsmap[pfx] for pfx in prefixes}
+
+
+nsmap = namespaces # alias for more compact use with Element()
+
+
+def nsdecls(*prefixes: str):
+ return " ".join(['xmlns:%s="%s"' % (pfx, _nsmap[pfx]) for pfx in prefixes])
+
+
+def nsuri(nspfx: str):
+ """Return the namespace URI corresponding to `nspfx`.
+
+ Example:
+
+ >>> nsuri("p")
+ "http://schemas.openxmlformats.org/presentationml/2006/main"
+ """
+ return _nsmap[nspfx]
+
+
+def qn(namespace_prefixed_tag: str) -> str:
+ """Return a Clark-notation qualified tag name corresponding to `namespace_prefixed_tag`.
+
+ `namespace_prefixed_tag` is a string like 'p:body'. 'qn' stands for `qualified name`.
+
+ As an example, `qn("p:cSld")` returns:
+ `"{http://schemas.openxmlformats.org/drawingml/2006/main}cSld"`.
+ """
+ nsptag = NamespacePrefixedTag(namespace_prefixed_tag)
+ return nsptag.clark_name
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/presentation.py b/.venv/lib/python3.12/site-packages/pptx/oxml/presentation.py
new file mode 100644
index 00000000..17997c2b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/presentation.py
@@ -0,0 +1,130 @@
+"""Custom element classes for presentation-related XML elements."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, cast
+
+from pptx.oxml.simpletypes import ST_SlideId, ST_SlideSizeCoordinate, XsdString
+from pptx.oxml.xmlchemy import BaseOxmlElement, RequiredAttribute, ZeroOrMore, ZeroOrOne
+
+if TYPE_CHECKING:
+ from pptx.util import Length
+
+
+class CT_Presentation(BaseOxmlElement):
+ """`p:presentation` element, root of the Presentation part stored as `/ppt/presentation.xml`."""
+
+ get_or_add_sldSz: Callable[[], CT_SlideSize]
+ get_or_add_sldIdLst: Callable[[], CT_SlideIdList]
+ get_or_add_sldMasterIdLst: Callable[[], CT_SlideMasterIdList]
+
+ sldMasterIdLst: CT_SlideMasterIdList | None = (
+ ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "p:sldMasterIdLst",
+ successors=(
+ "p:notesMasterIdLst",
+ "p:handoutMasterIdLst",
+ "p:sldIdLst",
+ "p:sldSz",
+ "p:notesSz",
+ ),
+ )
+ )
+ sldIdLst: CT_SlideIdList | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "p:sldIdLst", successors=("p:sldSz", "p:notesSz")
+ )
+ sldSz: CT_SlideSize | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "p:sldSz", successors=("p:notesSz",)
+ )
+
+
+class CT_SlideId(BaseOxmlElement):
+ """`p:sldId` element.
+
+ Direct child of `p:sldIdLst` that contains an `rId` reference to a slide in the presentation.
+ """
+
+ id: int = RequiredAttribute("id", ST_SlideId) # pyright: ignore[reportAssignmentType]
+ rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType]
+
+
+class CT_SlideIdList(BaseOxmlElement):
+ """`p:sldIdLst` element.
+
+ Direct child of <p:presentation> that contains a list of the slide parts in the presentation.
+ """
+
+ sldId_lst: list[CT_SlideId]
+
+ _add_sldId: Callable[..., CT_SlideId]
+ sldId = ZeroOrMore("p:sldId")
+
+ def add_sldId(self, rId: str) -> CT_SlideId:
+ """Create and return a reference to a new `p:sldId` child element.
+
+ The new `p:sldId` element has its r:id attribute set to `rId`.
+ """
+ return self._add_sldId(id=self._next_id, rId=rId)
+
+ @property
+ def _next_id(self) -> int:
+ """The next available slide ID as an `int`.
+
+ Valid slide IDs start at 256. The next integer value greater than the max value in use is
+ chosen, which minimizes that chance of reusing the id of a deleted slide.
+ """
+ MIN_SLIDE_ID = 256
+ MAX_SLIDE_ID = 2147483647
+
+ used_ids = [int(s) for s in cast("list[str]", self.xpath("./p:sldId/@id"))]
+ simple_next = max([MIN_SLIDE_ID - 1] + used_ids) + 1
+ if simple_next <= MAX_SLIDE_ID:
+ return simple_next
+
+ # -- fall back to search for next unused from bottom --
+ valid_used_ids = sorted(id for id in used_ids if (MIN_SLIDE_ID <= id <= MAX_SLIDE_ID))
+ return (
+ next(
+ candidate_id
+ for candidate_id, used_id in enumerate(valid_used_ids, start=MIN_SLIDE_ID)
+ if candidate_id != used_id
+ )
+ if valid_used_ids
+ else 256
+ )
+
+
+class CT_SlideMasterIdList(BaseOxmlElement):
+ """`p:sldMasterIdLst` element.
+
+ Child of `p:presentation` containing references to the slide masters that belong to the
+ presentation.
+ """
+
+ sldMasterId_lst: list[CT_SlideMasterIdListEntry]
+
+ sldMasterId = ZeroOrMore("p:sldMasterId")
+
+
+class CT_SlideMasterIdListEntry(BaseOxmlElement):
+ """
+ ``<p:sldMasterId>`` element, child of ``<p:sldMasterIdLst>`` containing
+ a reference to a slide master.
+ """
+
+ rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType]
+
+
+class CT_SlideSize(BaseOxmlElement):
+ """`p:sldSz` element.
+
+ Direct child of <p:presentation> that contains the width and height of slides in the
+ presentation.
+ """
+
+ cx: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType]
+ "cx", ST_SlideSizeCoordinate
+ )
+ cy: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType]
+ "cy", ST_SlideSizeCoordinate
+ )
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/__init__.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/__init__.py
new file mode 100644
index 00000000..37f8ef60
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/__init__.py
@@ -0,0 +1,19 @@
+"""Base shape-related objects such as BaseShape."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing_extensions import TypeAlias
+
+ from pptx.oxml.shapes.autoshape import CT_Shape
+ from pptx.oxml.shapes.connector import CT_Connector
+ from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame
+ from pptx.oxml.shapes.groupshape import CT_GroupShape
+ from pptx.oxml.shapes.picture import CT_Picture
+
+
+ShapeElement: TypeAlias = (
+ "CT_Connector | CT_GraphicalObjectFrame | CT_GroupShape | CT_Picture | CT_Shape"
+)
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/autoshape.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/autoshape.py
new file mode 100644
index 00000000..5d78f624
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/autoshape.py
@@ -0,0 +1,455 @@
+# pyright: reportPrivateUsage=false
+
+"""lxml custom element classes for shape-related XML elements."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, cast
+
+from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, PP_PLACEHOLDER
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.shapes.shared import BaseShapeElement
+from pptx.oxml.simpletypes import (
+ ST_Coordinate,
+ ST_PositiveCoordinate,
+ XsdBoolean,
+ XsdString,
+)
+from pptx.oxml.text import CT_TextBody
+from pptx.oxml.xmlchemy import (
+ BaseOxmlElement,
+ OneAndOnlyOne,
+ OptionalAttribute,
+ RequiredAttribute,
+ ZeroOrMore,
+ ZeroOrOne,
+)
+
+if TYPE_CHECKING:
+ from pptx.oxml.shapes.shared import (
+ CT_ApplicationNonVisualDrawingProps,
+ CT_NonVisualDrawingProps,
+ CT_ShapeProperties,
+ )
+ from pptx.util import Length
+
+
+class CT_AdjPoint2D(BaseOxmlElement):
+ """`a:pt` custom element class."""
+
+ x: Length = RequiredAttribute("x", ST_Coordinate) # pyright: ignore[reportAssignmentType]
+ y: Length = RequiredAttribute("y", ST_Coordinate) # pyright: ignore[reportAssignmentType]
+
+
+class CT_CustomGeometry2D(BaseOxmlElement):
+ """`a:custGeom` custom element class."""
+
+ get_or_add_pathLst: Callable[[], CT_Path2DList]
+
+ _tag_seq = ("a:avLst", "a:gdLst", "a:ahLst", "a:cxnLst", "a:rect", "a:pathLst")
+ pathLst: CT_Path2DList | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:pathLst", successors=_tag_seq[6:]
+ )
+
+
+class CT_GeomGuide(BaseOxmlElement):
+ """`a:gd` custom element class.
+
+ Defines a "guide", corresponding to a yellow diamond-shaped handle on an autoshape.
+ """
+
+ name: str = RequiredAttribute("name", XsdString) # pyright: ignore[reportAssignmentType]
+ fmla: str = RequiredAttribute("fmla", XsdString) # pyright: ignore[reportAssignmentType]
+
+
+class CT_GeomGuideList(BaseOxmlElement):
+ """`a:avLst` custom element class."""
+
+ _add_gd: Callable[[], CT_GeomGuide]
+
+ gd_lst: list[CT_GeomGuide]
+
+ gd = ZeroOrMore("a:gd")
+
+
+class CT_NonVisualDrawingShapeProps(BaseShapeElement):
+ """`p:cNvSpPr` custom element class."""
+
+ spLocks = ZeroOrOne("a:spLocks")
+ txBox: bool | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "txBox", XsdBoolean
+ )
+
+
+class CT_Path2D(BaseOxmlElement):
+ """`a:path` custom element class."""
+
+ _add_close: Callable[[], CT_Path2DClose]
+ _add_lnTo: Callable[[], CT_Path2DLineTo]
+ _add_moveTo: Callable[[], CT_Path2DMoveTo]
+
+ close = ZeroOrMore("a:close", successors=())
+ lnTo = ZeroOrMore("a:lnTo", successors=())
+ moveTo = ZeroOrMore("a:moveTo", successors=())
+ w: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "w", ST_PositiveCoordinate
+ )
+ h: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "h", ST_PositiveCoordinate
+ )
+
+ def add_close(self) -> CT_Path2DClose:
+ """Return a newly created `a:close` element.
+
+ The new `a:close` element is appended to this `a:path` element.
+ """
+ return self._add_close()
+
+ def add_lnTo(self, x: Length, y: Length) -> CT_Path2DLineTo:
+ """Return a newly created `a:lnTo` subtree with end point *(x, y)*.
+
+ The new `a:lnTo` element is appended to this `a:path` element.
+ """
+ lnTo = self._add_lnTo()
+ pt = lnTo._add_pt()
+ pt.x, pt.y = x, y
+ return lnTo
+
+ def add_moveTo(self, x: Length, y: Length):
+ """Return a newly created `a:moveTo` subtree with point `(x, y)`.
+
+ The new `a:moveTo` element is appended to this `a:path` element.
+ """
+ moveTo = self._add_moveTo()
+ pt = moveTo._add_pt()
+ pt.x, pt.y = x, y
+ return moveTo
+
+
+class CT_Path2DClose(BaseOxmlElement):
+ """`a:close` custom element class."""
+
+
+class CT_Path2DLineTo(BaseOxmlElement):
+ """`a:lnTo` custom element class."""
+
+ _add_pt: Callable[[], CT_AdjPoint2D]
+
+ pt = ZeroOrOne("a:pt", successors=())
+
+
+class CT_Path2DList(BaseOxmlElement):
+ """`a:pathLst` custom element class."""
+
+ _add_path: Callable[[], CT_Path2D]
+
+ path = ZeroOrMore("a:path", successors=())
+
+ def add_path(self, w: Length, h: Length):
+ """Return a newly created `a:path` child element."""
+ path = self._add_path()
+ path.w, path.h = w, h
+ return path
+
+
+class CT_Path2DMoveTo(BaseOxmlElement):
+ """`a:moveTo` custom element class."""
+
+ _add_pt: Callable[[], CT_AdjPoint2D]
+
+ pt = ZeroOrOne("a:pt", successors=())
+
+
+class CT_PresetGeometry2D(BaseOxmlElement):
+ """`a:prstGeom` custom element class."""
+
+ _add_avLst: Callable[[], CT_GeomGuideList]
+ _remove_avLst: Callable[[], None]
+
+ avLst: CT_GeomGuideList | None = ZeroOrOne("a:avLst") # pyright: ignore[reportAssignmentType]
+ prst: MSO_AUTO_SHAPE_TYPE = RequiredAttribute( # pyright: ignore[reportAssignmentType]
+ "prst", MSO_AUTO_SHAPE_TYPE
+ )
+
+ @property
+ def gd_lst(self) -> list[CT_GeomGuide]:
+ """Sequence of `a:gd` element children of `a:avLst`. Empty if none are present."""
+ avLst = self.avLst
+ if avLst is None:
+ return []
+ return avLst.gd_lst
+
+ def rewrite_guides(self, guides: list[tuple[str, int]]):
+ """Replace any `a:gd` element children of `a:avLst` with ones forme from `guides`."""
+ self._remove_avLst()
+ avLst = self._add_avLst()
+ for name, val in guides:
+ gd = avLst._add_gd()
+ gd.name = name
+ gd.fmla = "val %d" % val
+
+
+class CT_Shape(BaseShapeElement):
+ """`p:sp` custom element class."""
+
+ get_or_add_txBody: Callable[[], CT_TextBody]
+
+ nvSpPr: CT_ShapeNonVisual = OneAndOnlyOne("p:nvSpPr") # pyright: ignore[reportAssignmentType]
+ spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr") # pyright: ignore[reportAssignmentType]
+ txBody: CT_TextBody | None = ZeroOrOne("p:txBody", successors=("p:extLst",)) # pyright: ignore
+
+ def add_path(self, w: Length, h: Length) -> CT_Path2D:
+ custGeom = self.spPr.custGeom
+ if custGeom is None:
+ raise ValueError("shape must be freeform")
+ pathLst = custGeom.get_or_add_pathLst()
+ return pathLst.add_path(w=w, h=h)
+
+ def get_or_add_ln(self):
+ """Return the `a:ln` grandchild element, newly added if not present."""
+ return self.spPr.get_or_add_ln()
+
+ @property
+ def has_custom_geometry(self):
+ """True if this shape has custom geometry, i.e. is a freeform shape.
+
+ A shape has custom geometry if it has a `p:spPr/a:custGeom`
+ descendant (instead of `p:spPr/a:prstGeom`).
+ """
+ return self.spPr.custGeom is not None
+
+ @property
+ def is_autoshape(self):
+ """True if this shape is an auto shape.
+
+ A shape is an auto shape if it has a `a:prstGeom` element and does not have a txBox="1"
+ attribute on cNvSpPr.
+ """
+ prstGeom = self.prstGeom
+ if prstGeom is None:
+ return False
+ return self.nvSpPr.cNvSpPr.txBox is not True
+
+ @property
+ def is_textbox(self):
+ """True if this shape is a text box.
+
+ A shape is a text box if it has a `txBox` attribute on cNvSpPr that resolves to |True|.
+ The default when the txBox attribute is missing is |False|.
+ """
+ return self.nvSpPr.cNvSpPr.txBox is True
+
+ @property
+ def ln(self):
+ """`a:ln` grand-child element or |None| if not present."""
+ return self.spPr.ln
+
+ @staticmethod
+ def new_autoshape_sp(
+ id_: int, name: str, prst: str, left: int, top: int, width: int, height: int
+ ) -> CT_Shape:
+ """Return a new `p:sp` element tree configured as a base auto shape."""
+ xml = (
+ "<p:sp %s>\n"
+ " <p:nvSpPr>\n"
+ ' <p:cNvPr id="%s" name="%s"/>\n'
+ " <p:cNvSpPr/>\n"
+ " <p:nvPr/>\n"
+ " </p:nvSpPr>\n"
+ " <p:spPr>\n"
+ " <a:xfrm>\n"
+ ' <a:off x="%s" y="%s"/>\n'
+ ' <a:ext cx="%s" cy="%s"/>\n'
+ " </a:xfrm>\n"
+ ' <a:prstGeom prst="%s">\n'
+ " <a:avLst/>\n"
+ " </a:prstGeom>\n"
+ " </p:spPr>\n"
+ " <p:style>\n"
+ ' <a:lnRef idx="1">\n'
+ ' <a:schemeClr val="accent1"/>\n'
+ " </a:lnRef>\n"
+ ' <a:fillRef idx="3">\n'
+ ' <a:schemeClr val="accent1"/>\n'
+ " </a:fillRef>\n"
+ ' <a:effectRef idx="2">\n'
+ ' <a:schemeClr val="accent1"/>\n'
+ " </a:effectRef>\n"
+ ' <a:fontRef idx="minor">\n'
+ ' <a:schemeClr val="lt1"/>\n'
+ " </a:fontRef>\n"
+ " </p:style>\n"
+ " <p:txBody>\n"
+ ' <a:bodyPr rtlCol="0" anchor="ctr"/>\n'
+ " <a:lstStyle/>\n"
+ " <a:p>\n"
+ ' <a:pPr algn="ctr"/>\n'
+ " </a:p>\n"
+ " </p:txBody>\n"
+ "</p:sp>" % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d", "%s")
+ ) % (id_, name, left, top, width, height, prst)
+ return cast(CT_Shape, parse_xml(xml))
+
+ @staticmethod
+ def new_freeform_sp(shape_id: int, name: str, x: int, y: int, cx: int, cy: int):
+ """Return new `p:sp` element tree configured as freeform shape.
+
+ The returned shape has a `a:custGeom` subtree but no paths in its
+ path list.
+ """
+ xml = (
+ "<p:sp %s>\n"
+ " <p:nvSpPr>\n"
+ ' <p:cNvPr id="%s" name="%s"/>\n'
+ " <p:cNvSpPr/>\n"
+ " <p:nvPr/>\n"
+ " </p:nvSpPr>\n"
+ " <p:spPr>\n"
+ " <a:xfrm>\n"
+ ' <a:off x="%s" y="%s"/>\n'
+ ' <a:ext cx="%s" cy="%s"/>\n'
+ " </a:xfrm>\n"
+ " <a:custGeom>\n"
+ " <a:avLst/>\n"
+ " <a:gdLst/>\n"
+ " <a:ahLst/>\n"
+ " <a:cxnLst/>\n"
+ ' <a:rect l="l" t="t" r="r" b="b"/>\n'
+ " <a:pathLst/>\n"
+ " </a:custGeom>\n"
+ " </p:spPr>\n"
+ " <p:style>\n"
+ ' <a:lnRef idx="1">\n'
+ ' <a:schemeClr val="accent1"/>\n'
+ " </a:lnRef>\n"
+ ' <a:fillRef idx="3">\n'
+ ' <a:schemeClr val="accent1"/>\n'
+ " </a:fillRef>\n"
+ ' <a:effectRef idx="2">\n'
+ ' <a:schemeClr val="accent1"/>\n'
+ " </a:effectRef>\n"
+ ' <a:fontRef idx="minor">\n'
+ ' <a:schemeClr val="lt1"/>\n'
+ " </a:fontRef>\n"
+ " </p:style>\n"
+ " <p:txBody>\n"
+ ' <a:bodyPr rtlCol="0" anchor="ctr"/>\n'
+ " <a:lstStyle/>\n"
+ " <a:p>\n"
+ ' <a:pPr algn="ctr"/>\n'
+ " </a:p>\n"
+ " </p:txBody>\n"
+ "</p:sp>" % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d")
+ ) % (shape_id, name, x, y, cx, cy)
+ return cast(CT_Shape, parse_xml(xml))
+
+ @staticmethod
+ def new_placeholder_sp(
+ id_: int, name: str, ph_type: PP_PLACEHOLDER, orient: str, sz, idx
+ ) -> CT_Shape:
+ """Return a new `p:sp` element tree configured as a placeholder shape."""
+ sp = cast(
+ CT_Shape,
+ parse_xml(
+ f"<p:sp {nsdecls('a', 'p')}>\n"
+ f" <p:nvSpPr>\n"
+ f' <p:cNvPr id="{id_}" name="{name}"/>\n'
+ f" <p:cNvSpPr>\n"
+ f' <a:spLocks noGrp="1"/>\n'
+ f" </p:cNvSpPr>\n"
+ f" <p:nvPr/>\n"
+ f" </p:nvSpPr>\n"
+ f" <p:spPr/>\n"
+ f"</p:sp>"
+ ),
+ )
+
+ ph = sp.nvSpPr.nvPr.get_or_add_ph()
+ ph.type = ph_type
+ ph.idx = idx
+ ph.orient = orient
+ ph.sz = sz
+
+ placeholder_types_that_have_a_text_frame = (
+ PP_PLACEHOLDER.TITLE,
+ PP_PLACEHOLDER.CENTER_TITLE,
+ PP_PLACEHOLDER.SUBTITLE,
+ PP_PLACEHOLDER.BODY,
+ PP_PLACEHOLDER.OBJECT,
+ )
+
+ if ph_type in placeholder_types_that_have_a_text_frame:
+ sp.append(CT_TextBody.new())
+
+ return sp
+
+ @staticmethod
+ def new_textbox_sp(id_, name, left, top, width, height):
+ """Return a new `p:sp` element tree configured as a base textbox shape."""
+ tmpl = CT_Shape._textbox_sp_tmpl()
+ xml = tmpl % (id_, name, left, top, width, height)
+ sp = parse_xml(xml)
+ return sp
+
+ @property
+ def prst(self):
+ """Value of `prst` attribute of `a:prstGeom` element or |None| if not present."""
+ prstGeom = self.prstGeom
+ if prstGeom is None:
+ return None
+ return prstGeom.prst
+
+ @property
+ def prstGeom(self) -> CT_PresetGeometry2D:
+ """Reference to `a:prstGeom` child element.
+
+ |None| if this shape doesn't have one, for example, if it's a placeholder shape.
+ """
+ return self.spPr.prstGeom
+
+ def _new_txBody(self):
+ return CT_TextBody.new_p_txBody()
+
+ @staticmethod
+ def _textbox_sp_tmpl():
+ return (
+ "<p:sp %s>\n"
+ " <p:nvSpPr>\n"
+ ' <p:cNvPr id="%s" name="%s"/>\n'
+ ' <p:cNvSpPr txBox="1"/>\n'
+ " <p:nvPr/>\n"
+ " </p:nvSpPr>\n"
+ " <p:spPr>\n"
+ " <a:xfrm>\n"
+ ' <a:off x="%s" y="%s"/>\n'
+ ' <a:ext cx="%s" cy="%s"/>\n'
+ " </a:xfrm>\n"
+ ' <a:prstGeom prst="rect">\n'
+ " <a:avLst/>\n"
+ " </a:prstGeom>\n"
+ " <a:noFill/>\n"
+ " </p:spPr>\n"
+ " <p:txBody>\n"
+ ' <a:bodyPr wrap="none">\n'
+ " <a:spAutoFit/>\n"
+ " </a:bodyPr>\n"
+ " <a:lstStyle/>\n"
+ " <a:p/>\n"
+ " </p:txBody>\n"
+ "</p:sp>" % (nsdecls("a", "p"), "%d", "%s", "%d", "%d", "%d", "%d")
+ )
+
+
+class CT_ShapeNonVisual(BaseShapeElement):
+ """`p:nvSpPr` custom element class."""
+
+ cNvPr: CT_NonVisualDrawingProps = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
+ "p:cNvPr"
+ )
+ cNvSpPr: CT_NonVisualDrawingShapeProps = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
+ "p:cNvSpPr"
+ )
+ nvPr: CT_ApplicationNonVisualDrawingProps = ( # pyright: ignore[reportAssignmentType]
+ OneAndOnlyOne("p:nvPr")
+ )
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/connector.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/connector.py
new file mode 100644
index 00000000..91261f78
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/connector.py
@@ -0,0 +1,107 @@
+"""lxml custom element classes for XML elements related to the Connector shape."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.shapes.shared import BaseShapeElement
+from pptx.oxml.simpletypes import ST_DrawingElementId, XsdUnsignedInt
+from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne, RequiredAttribute, ZeroOrOne
+
+if TYPE_CHECKING:
+ from pptx.oxml.shapes.shared import CT_ShapeProperties
+
+
+class CT_Connection(BaseShapeElement):
+ """A `a:stCxn` or `a:endCxn` element.
+
+ Specifies a connection between an end-point of a connector and a shape connection point.
+ """
+
+ id = RequiredAttribute("id", ST_DrawingElementId)
+ idx = RequiredAttribute("idx", XsdUnsignedInt)
+
+
+class CT_Connector(BaseShapeElement):
+ """A line/connector shape `p:cxnSp` element"""
+
+ _tag_seq = ("p:nvCxnSpPr", "p:spPr", "p:style", "p:extLst")
+ nvCxnSpPr = OneAndOnlyOne("p:nvCxnSpPr")
+ spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr") # pyright: ignore[reportAssignmentType]
+ del _tag_seq
+
+ @classmethod
+ def new_cxnSp(
+ cls,
+ id_: int,
+ name: str,
+ prst: str,
+ x: int,
+ y: int,
+ cx: int,
+ cy: int,
+ flipH: bool,
+ flipV: bool,
+ ) -> CT_Connector:
+ """Return a new `p:cxnSp` element tree configured as a base connector."""
+ flip = (' flipH="1"' if flipH else "") + (' flipV="1"' if flipV else "")
+ return cast(
+ CT_Connector,
+ parse_xml(
+ f"<p:cxnSp {nsdecls('a', 'p')}>\n"
+ f" <p:nvCxnSpPr>\n"
+ f' <p:cNvPr id="{id_}" name="{name}"/>\n'
+ f" <p:cNvCxnSpPr/>\n"
+ f" <p:nvPr/>\n"
+ f" </p:nvCxnSpPr>\n"
+ f" <p:spPr>\n"
+ f" <a:xfrm{flip}>\n"
+ f' <a:off x="{x}" y="{y}"/>\n'
+ f' <a:ext cx="{cx}" cy="{cy}"/>\n'
+ f" </a:xfrm>\n"
+ f' <a:prstGeom prst="{prst}">\n'
+ f" <a:avLst/>\n"
+ f" </a:prstGeom>\n"
+ f" </p:spPr>\n"
+ f" <p:style>\n"
+ f' <a:lnRef idx="2">\n'
+ f' <a:schemeClr val="accent1"/>\n'
+ f" </a:lnRef>\n"
+ f' <a:fillRef idx="0">\n'
+ f' <a:schemeClr val="accent1"/>\n'
+ f" </a:fillRef>\n"
+ f' <a:effectRef idx="1">\n'
+ f' <a:schemeClr val="accent1"/>\n'
+ f" </a:effectRef>\n"
+ f' <a:fontRef idx="minor">\n'
+ f' <a:schemeClr val="tx1"/>\n'
+ f" </a:fontRef>\n"
+ f" </p:style>\n"
+ f"</p:cxnSp>"
+ ),
+ )
+
+
+class CT_ConnectorNonVisual(BaseOxmlElement):
+ """
+ `p:nvCxnSpPr` element, container for the non-visual properties of
+ a connector, such as name, id, etc.
+ """
+
+ cNvPr = OneAndOnlyOne("p:cNvPr")
+ cNvCxnSpPr = OneAndOnlyOne("p:cNvCxnSpPr")
+ nvPr = OneAndOnlyOne("p:nvPr")
+
+
+class CT_NonVisualConnectorProperties(BaseOxmlElement):
+ """
+ `p:cNvCxnSpPr` element, container for the non-visual properties specific
+ to a connector shape, such as connections and connector locking.
+ """
+
+ _tag_seq = ("a:cxnSpLocks", "a:stCxn", "a:endCxn", "a:extLst")
+ stCxn = ZeroOrOne("a:stCxn", successors=_tag_seq[2:])
+ endCxn = ZeroOrOne("a:endCxn", successors=_tag_seq[3:])
+ del _tag_seq
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/graphfrm.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/graphfrm.py
new file mode 100644
index 00000000..efa0b363
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/graphfrm.py
@@ -0,0 +1,342 @@
+"""lxml custom element class for CT_GraphicalObjectFrame XML element."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+
+from pptx.oxml import parse_xml
+from pptx.oxml.chart.chart import CT_Chart
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.shapes.shared import BaseShapeElement
+from pptx.oxml.simpletypes import XsdBoolean, XsdString
+from pptx.oxml.table import CT_Table
+from pptx.oxml.xmlchemy import (
+ BaseOxmlElement,
+ OneAndOnlyOne,
+ OptionalAttribute,
+ RequiredAttribute,
+ ZeroOrOne,
+)
+from pptx.spec import (
+ GRAPHIC_DATA_URI_CHART,
+ GRAPHIC_DATA_URI_OLEOBJ,
+ GRAPHIC_DATA_URI_TABLE,
+)
+
+if TYPE_CHECKING:
+ from pptx.oxml.shapes.shared import (
+ CT_ApplicationNonVisualDrawingProps,
+ CT_NonVisualDrawingProps,
+ CT_Transform2D,
+ )
+
+
+class CT_GraphicalObject(BaseOxmlElement):
+ """`a:graphic` element.
+
+ The container for the reference to or definition of the framed graphical object (table, chart,
+ etc.).
+ """
+
+ graphicData: CT_GraphicalObjectData = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
+ "a:graphicData"
+ )
+
+ @property
+ def chart(self) -> CT_Chart | None:
+ """The `c:chart` grandchild element, or |None| if not present."""
+ return self.graphicData.chart
+
+
+class CT_GraphicalObjectData(BaseShapeElement):
+ """`p:graphicData` element.
+
+ The direct container for a table, a chart, or another graphical object.
+ """
+
+ chart: CT_Chart | None = ZeroOrOne("c:chart") # pyright: ignore[reportAssignmentType]
+ tbl: CT_Table | None = ZeroOrOne("a:tbl") # pyright: ignore[reportAssignmentType]
+ uri: str = RequiredAttribute("uri", XsdString) # pyright: ignore[reportAssignmentType]
+
+ @property
+ def blob_rId(self) -> str | None:
+ """Optional `r:id` attribute value of `p:oleObj` descendent element.
+
+ This value is `None` when this `p:graphicData` element does not enclose an OLE object.
+ This value could also be `None` if an enclosed OLE object does not specify this attribute
+ (it is specified optional in the schema) but so far, all OLE objects we've encountered
+ specify this value.
+ """
+ return None if self._oleObj is None else self._oleObj.rId
+
+ @property
+ def is_embedded_ole_obj(self) -> bool | None:
+ """Optional boolean indicating an embedded OLE object.
+
+ Returns `None` when this `p:graphicData` element does not enclose an OLE object. `True`
+ indicates an embedded OLE object and `False` indicates a linked OLE object.
+ """
+ return None if self._oleObj is None else self._oleObj.is_embedded
+
+ @property
+ def progId(self) -> str | None:
+ """Optional str value of "progId" attribute of `p:oleObj` descendent.
+
+ This value identifies the "type" of the embedded object in terms of the application used
+ to open it.
+
+ This value is `None` when this `p:graphicData` element does not enclose an OLE object.
+ This could also be `None` if an enclosed OLE object does not specify this attribute (it is
+ specified optional in the schema) but so far, all OLE objects we've encountered specify
+ this value.
+ """
+ return None if self._oleObj is None else self._oleObj.progId
+
+ @property
+ def showAsIcon(self) -> bool | None:
+ """Optional value of "showAsIcon" attribute value of `p:oleObj` descendent.
+
+ This value is `None` when this `p:graphicData` element does not enclose an OLE object. It
+ is False when the `showAsIcon` attribute is omitted on the `p:oleObj` element.
+ """
+ return None if self._oleObj is None else self._oleObj.showAsIcon
+
+ @property
+ def _oleObj(self) -> CT_OleObject | None:
+ """Optional `p:oleObj` element contained in this `p:graphicData' element.
+
+ Returns `None` when this graphic-data element does not enclose an OLE object. Note that
+ this returns the last `p:oleObj` element found. There can be more than one `p:oleObj`
+ element because an `mc.AlternateContent` element may appear as the child of
+ `p:graphicData` and that alternate-content subtree can contain multiple compatibility
+ choices. The last one should suit best for reading purposes because it contains the lowest
+ common denominator.
+ """
+ oleObjs = cast("list[CT_OleObject]", self.xpath(".//p:oleObj"))
+ return oleObjs[-1] if oleObjs else None
+
+
+class CT_GraphicalObjectFrame(BaseShapeElement):
+ """`p:graphicFrame` element.
+
+ A container for a table, a chart, or another graphical object.
+ """
+
+ nvGraphicFramePr: CT_GraphicalObjectFrameNonVisual = ( # pyright: ignore[reportAssignmentType]
+ OneAndOnlyOne("p:nvGraphicFramePr")
+ )
+ xfrm: CT_Transform2D = OneAndOnlyOne("p:xfrm") # pyright: ignore
+ graphic: CT_GraphicalObject = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
+ "a:graphic"
+ )
+
+ @property
+ def chart(self) -> CT_Chart | None:
+ """The `c:chart` great-grandchild element, or |None| if not present."""
+ return self.graphic.chart
+
+ @property
+ def chart_rId(self) -> str | None:
+ """The `rId` attribute of the `c:chart` great-grandchild element.
+
+ |None| if not present.
+ """
+ chart = self.chart
+ if chart is None:
+ return None
+ return chart.rId
+
+ def get_or_add_xfrm(self) -> CT_Transform2D:
+ """Return the required `p:xfrm` child element.
+
+ Overrides version on BaseShapeElement.
+ """
+ return self.xfrm
+
+ @property
+ def graphicData(self) -> CT_GraphicalObjectData:
+ """`a:graphicData` grandchild of this graphic-frame element."""
+ return self.graphic.graphicData
+
+ @property
+ def graphicData_uri(self) -> str:
+ """str value of `uri` attribute of `a:graphicData` grandchild."""
+ return self.graphic.graphicData.uri
+
+ @property
+ def has_oleobj(self) -> bool:
+ """`True` for graphicFrame containing an OLE object, `False` otherwise."""
+ return self.graphicData.uri == GRAPHIC_DATA_URI_OLEOBJ
+
+ @property
+ def is_embedded_ole_obj(self) -> bool | None:
+ """Optional boolean indicating an embedded OLE object.
+
+ Returns `None` when this `p:graphicFrame` element does not enclose an OLE object. `True`
+ indicates an embedded OLE object and `False` indicates a linked OLE object.
+ """
+ return self.graphicData.is_embedded_ole_obj
+
+ @classmethod
+ def new_chart_graphicFrame(
+ cls, id_: int, name: str, rId: str, x: int, y: int, cx: int, cy: int
+ ) -> CT_GraphicalObjectFrame:
+ """Return a `p:graphicFrame` element tree populated with a chart element."""
+ graphicFrame = CT_GraphicalObjectFrame.new_graphicFrame(id_, name, x, y, cx, cy)
+ graphicData = graphicFrame.graphic.graphicData
+ graphicData.uri = GRAPHIC_DATA_URI_CHART
+ graphicData.append(CT_Chart.new_chart(rId))
+ return graphicFrame
+
+ @classmethod
+ def new_graphicFrame(
+ cls, id_: int, name: str, x: int, y: int, cx: int, cy: int
+ ) -> CT_GraphicalObjectFrame:
+ """Return a new `p:graphicFrame` element tree suitable for containing a table or chart.
+
+ Note that a graphicFrame element is not a valid shape until it contains a graphical object
+ such as a table.
+ """
+ return cast(
+ CT_GraphicalObjectFrame,
+ parse_xml(
+ f"<p:graphicFrame {nsdecls('a', 'p')}>\n"
+ f" <p:nvGraphicFramePr>\n"
+ f' <p:cNvPr id="{id_}" name="{name}"/>\n'
+ f" <p:cNvGraphicFramePr>\n"
+ f' <a:graphicFrameLocks noGrp="1"/>\n'
+ f" </p:cNvGraphicFramePr>\n"
+ f" <p:nvPr/>\n"
+ f" </p:nvGraphicFramePr>\n"
+ f" <p:xfrm>\n"
+ f' <a:off x="{x}" y="{y}"/>\n'
+ f' <a:ext cx="{cx}" cy="{cy}"/>\n'
+ f" </p:xfrm>\n"
+ f" <a:graphic>\n"
+ f" <a:graphicData/>\n"
+ f" </a:graphic>\n"
+ f"</p:graphicFrame>"
+ ),
+ )
+
+ @classmethod
+ def new_ole_object_graphicFrame(
+ cls,
+ id_: int,
+ name: str,
+ ole_object_rId: str,
+ progId: str,
+ icon_rId: str,
+ x: int,
+ y: int,
+ cx: int,
+ cy: int,
+ imgW: int,
+ imgH: int,
+ ) -> CT_GraphicalObjectFrame:
+ """Return newly-created `p:graphicFrame` for embedded OLE-object.
+
+ `ole_object_rId` identifies the relationship to the OLE-object part.
+
+ `progId` is a str identifying the object-type in terms of the application (program) used
+ to open it. This becomes an attribute of the same name in the `p:oleObj` element.
+
+ `icon_rId` identifies the relationship to an image part used to display the OLE-object as
+ an icon (vs. a preview).
+ """
+ return cast(
+ CT_GraphicalObjectFrame,
+ parse_xml(
+ f"<p:graphicFrame {nsdecls('a', 'p', 'r')}>\n"
+ f" <p:nvGraphicFramePr>\n"
+ f' <p:cNvPr id="{id_}" name="{name}"/>\n'
+ f" <p:cNvGraphicFramePr>\n"
+ f' <a:graphicFrameLocks noGrp="1"/>\n'
+ f" </p:cNvGraphicFramePr>\n"
+ f" <p:nvPr/>\n"
+ f" </p:nvGraphicFramePr>\n"
+ f" <p:xfrm>\n"
+ f' <a:off x="{x}" y="{y}"/>\n'
+ f' <a:ext cx="{cx}" cy="{cy}"/>\n'
+ f" </p:xfrm>\n"
+ f" <a:graphic>\n"
+ f" <a:graphicData"
+ f' uri="http://schemas.openxmlformats.org/presentationml/2006/ole">\n'
+ f' <p:oleObj showAsIcon="1"'
+ f' r:id="{ole_object_rId}"'
+ f' imgW="{imgW}"'
+ f' imgH="{imgH}"'
+ f' progId="{progId}">\n'
+ f" <p:embed/>\n"
+ f" <p:pic>\n"
+ f" <p:nvPicPr>\n"
+ f' <p:cNvPr id="0" name=""/>\n'
+ f" <p:cNvPicPr/>\n"
+ f" <p:nvPr/>\n"
+ f" </p:nvPicPr>\n"
+ f" <p:blipFill>\n"
+ f' <a:blip r:embed="{icon_rId}"/>\n'
+ f" <a:stretch>\n"
+ f" <a:fillRect/>\n"
+ f" </a:stretch>\n"
+ f" </p:blipFill>\n"
+ f" <p:spPr>\n"
+ f" <a:xfrm>\n"
+ f' <a:off x="{x}" y="{y}"/>\n'
+ f' <a:ext cx="{cx}" cy="{cy}"/>\n'
+ f" </a:xfrm>\n"
+ f' <a:prstGeom prst="rect">\n'
+ f" <a:avLst/>\n"
+ f" </a:prstGeom>\n"
+ f" </p:spPr>\n"
+ f" </p:pic>\n"
+ f" </p:oleObj>\n"
+ f" </a:graphicData>\n"
+ f" </a:graphic>\n"
+ f"</p:graphicFrame>"
+ ),
+ )
+
+ @classmethod
+ def new_table_graphicFrame(
+ cls, id_: int, name: str, rows: int, cols: int, x: int, y: int, cx: int, cy: int
+ ) -> CT_GraphicalObjectFrame:
+ """Return a `p:graphicFrame` element tree populated with a table element."""
+ graphicFrame = cls.new_graphicFrame(id_, name, x, y, cx, cy)
+ graphicFrame.graphic.graphicData.uri = GRAPHIC_DATA_URI_TABLE
+ graphicFrame.graphic.graphicData.append(CT_Table.new_tbl(rows, cols, cx, cy))
+ return graphicFrame
+
+
+class CT_GraphicalObjectFrameNonVisual(BaseOxmlElement):
+ """`p:nvGraphicFramePr` element.
+
+ This contains the non-visual properties of a graphic frame, such as name, id, etc.
+ """
+
+ cNvPr: CT_NonVisualDrawingProps = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
+ "p:cNvPr"
+ )
+ nvPr: CT_ApplicationNonVisualDrawingProps = ( # pyright: ignore[reportAssignmentType]
+ OneAndOnlyOne("p:nvPr")
+ )
+
+
+class CT_OleObject(BaseOxmlElement):
+ """`p:oleObj` element, container for an OLE object (e.g. Excel file).
+
+ An OLE object can be either linked or embedded (hence the name).
+ """
+
+ progId: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "progId", XsdString
+ )
+ rId: str | None = OptionalAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType]
+ showAsIcon: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "showAsIcon", XsdBoolean, default=False
+ )
+
+ @property
+ def is_embedded(self) -> bool:
+ """True when this OLE object is embedded, False when it is linked."""
+ return len(self.xpath("./p:embed")) > 0
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/groupshape.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/groupshape.py
new file mode 100644
index 00000000..f62bc666
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/groupshape.py
@@ -0,0 +1,280 @@
+"""lxml custom element classes for shape-tree-related XML elements."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, Iterator
+
+from pptx.enum.shapes import MSO_CONNECTOR_TYPE
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls, qn
+from pptx.oxml.shapes.autoshape import CT_Shape
+from pptx.oxml.shapes.connector import CT_Connector
+from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame
+from pptx.oxml.shapes.picture import CT_Picture
+from pptx.oxml.shapes.shared import BaseShapeElement
+from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne, ZeroOrOne
+from pptx.util import Emu
+
+if TYPE_CHECKING:
+ from pptx.enum.shapes import PP_PLACEHOLDER
+ from pptx.oxml.shapes import ShapeElement
+ from pptx.oxml.shapes.shared import CT_Transform2D
+
+
+class CT_GroupShape(BaseShapeElement):
+ """Used for shape tree (`p:spTree`) as well as the group shape (`p:grpSp`) elements."""
+
+ nvGrpSpPr: CT_GroupShapeNonVisual = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
+ "p:nvGrpSpPr"
+ )
+ grpSpPr: CT_GroupShapeProperties = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
+ "p:grpSpPr"
+ )
+
+ _shape_tags = (
+ qn("p:sp"),
+ qn("p:grpSp"),
+ qn("p:graphicFrame"),
+ qn("p:cxnSp"),
+ qn("p:pic"),
+ qn("p:contentPart"),
+ )
+
+ def add_autoshape(
+ self, id_: int, name: str, prst: str, x: int, y: int, cx: int, cy: int
+ ) -> CT_Shape:
+ """Return new `p:sp` appended to the group/shapetree with specified attributes."""
+ sp = CT_Shape.new_autoshape_sp(id_, name, prst, x, y, cx, cy)
+ self.insert_element_before(sp, "p:extLst")
+ return sp
+
+ def add_cxnSp(
+ self,
+ id_: int,
+ name: str,
+ type_member: MSO_CONNECTOR_TYPE,
+ x: int,
+ y: int,
+ cx: int,
+ cy: int,
+ flipH: bool,
+ flipV: bool,
+ ) -> CT_Connector:
+ """Return new `p:cxnSp` appended to the group/shapetree with the specified attribues."""
+ prst = MSO_CONNECTOR_TYPE.to_xml(type_member)
+ cxnSp = CT_Connector.new_cxnSp(id_, name, prst, x, y, cx, cy, flipH, flipV)
+ self.insert_element_before(cxnSp, "p:extLst")
+ return cxnSp
+
+ def add_freeform_sp(self, x: int, y: int, cx: int, cy: int) -> CT_Shape:
+ """Append a new freeform `p:sp` with specified position and size."""
+ shape_id = self._next_shape_id
+ name = "Freeform %d" % (shape_id - 1,)
+ sp = CT_Shape.new_freeform_sp(shape_id, name, x, y, cx, cy)
+ self.insert_element_before(sp, "p:extLst")
+ return sp
+
+ def add_grpSp(self) -> CT_GroupShape:
+ """Return `p:grpSp` element newly appended to this shape tree.
+
+ The element contains no sub-shapes, is positioned at (0, 0), and has
+ width and height of zero.
+ """
+ shape_id = self._next_shape_id
+ name = "Group %d" % (shape_id - 1,)
+ grpSp = CT_GroupShape.new_grpSp(shape_id, name)
+ self.insert_element_before(grpSp, "p:extLst")
+ return grpSp
+
+ def add_pic(
+ self, id_: int, name: str, desc: str, rId: str, x: int, y: int, cx: int, cy: int
+ ) -> CT_Picture:
+ """Append a `p:pic` shape to the group/shapetree having properties as specified in call."""
+ pic = CT_Picture.new_pic(id_, name, desc, rId, x, y, cx, cy)
+ self.insert_element_before(pic, "p:extLst")
+ return pic
+
+ def add_placeholder(
+ self, id_: int, name: str, ph_type: PP_PLACEHOLDER, orient: str, sz: str, idx: int
+ ) -> CT_Shape:
+ """Append a newly-created placeholder `p:sp` shape having the specified properties."""
+ sp = CT_Shape.new_placeholder_sp(id_, name, ph_type, orient, sz, idx)
+ self.insert_element_before(sp, "p:extLst")
+ return sp
+
+ def add_table(
+ self, id_: int, name: str, rows: int, cols: int, x: int, y: int, cx: int, cy: int
+ ) -> CT_GraphicalObjectFrame:
+ """Append a `p:graphicFrame` shape containing a table as specified in call."""
+ graphicFrame = CT_GraphicalObjectFrame.new_table_graphicFrame(
+ id_, name, rows, cols, x, y, cx, cy
+ )
+ self.insert_element_before(graphicFrame, "p:extLst")
+ return graphicFrame
+
+ def add_textbox(self, id_: int, name: str, x: int, y: int, cx: int, cy: int) -> CT_Shape:
+ """Append a newly-created textbox `p:sp` shape having the specified position and size."""
+ sp = CT_Shape.new_textbox_sp(id_, name, x, y, cx, cy)
+ self.insert_element_before(sp, "p:extLst")
+ return sp
+
+ @property
+ def chExt(self):
+ """Descendent `p:grpSpPr/a:xfrm/a:chExt` element."""
+ return self.grpSpPr.get_or_add_xfrm().get_or_add_chExt()
+
+ @property
+ def chOff(self):
+ """Descendent `p:grpSpPr/a:xfrm/a:chOff` element."""
+ return self.grpSpPr.get_or_add_xfrm().get_or_add_chOff()
+
+ def get_or_add_xfrm(self) -> CT_Transform2D:
+ """Return the `a:xfrm` grandchild element, newly-added if not present."""
+ return self.grpSpPr.get_or_add_xfrm()
+
+ def iter_ph_elms(self):
+ """Generate each placeholder shape child element in document order."""
+ for e in self.iter_shape_elms():
+ if e.has_ph_elm:
+ yield e
+
+ def iter_shape_elms(self) -> Iterator[ShapeElement]:
+ """Generate each child of this `p:spTree` element that corresponds to a shape.
+
+ Items appear in XML document order.
+ """
+ for elm in self.iterchildren():
+ if elm.tag in self._shape_tags:
+ yield elm
+
+ @property
+ def max_shape_id(self) -> int:
+ """Maximum int value assigned as @id in this slide.
+
+ This is generally a shape-id, but ids can be assigned to other
+ objects so we just check all @id values anywhere in the document
+ (XML id-values have document scope).
+
+ In practice, its minimum value is 1 because the spTree element itself
+ is always assigned id="1".
+ """
+ id_str_lst = self.xpath("//@id")
+ used_ids = [int(id_str) for id_str in id_str_lst if id_str.isdigit()]
+ return max(used_ids) if used_ids else 0
+
+ @classmethod
+ def new_grpSp(cls, id_: int, name: str) -> CT_GroupShape:
+ """Return new "loose" `p:grpSp` element having `id_` and `name`."""
+ xml = (
+ "<p:grpSp %s>\n"
+ " <p:nvGrpSpPr>\n"
+ ' <p:cNvPr id="%%d" name="%%s"/>\n'
+ " <p:cNvGrpSpPr/>\n"
+ " <p:nvPr/>\n"
+ " </p:nvGrpSpPr>\n"
+ " <p:grpSpPr>\n"
+ " <a:xfrm>\n"
+ ' <a:off x="0" y="0"/>\n'
+ ' <a:ext cx="0" cy="0"/>\n'
+ ' <a:chOff x="0" y="0"/>\n'
+ ' <a:chExt cx="0" cy="0"/>\n'
+ " </a:xfrm>\n"
+ " </p:grpSpPr>\n"
+ "</p:grpSp>" % nsdecls("a", "p", "r")
+ ) % (id_, name)
+ grpSp = parse_xml(xml)
+ return grpSp
+
+ def recalculate_extents(self) -> None:
+ """Adjust x, y, cx, and cy to incorporate all contained shapes.
+
+ This would typically be called when a contained shape is added,
+ removed, or its position or size updated.
+
+ This method is recursive "upwards" since a change in a group shape
+ can change the position and size of its containing group.
+ """
+ if not self.tag == qn("p:grpSp"):
+ return
+
+ x, y, cx, cy = self._child_extents
+
+ self.chOff.x = self.x = x
+ self.chOff.y = self.y = y
+ self.chExt.cx = self.cx = cx
+ self.chExt.cy = self.cy = cy
+ self.getparent().recalculate_extents()
+
+ @property
+ def xfrm(self) -> CT_Transform2D | None:
+ """The `a:xfrm` grandchild element or |None| if not found."""
+ return self.grpSpPr.xfrm
+
+ @property
+ def _child_extents(self) -> tuple[int, int, int, int]:
+ """(x, y, cx, cy) tuple representing net position and size.
+
+ The values are formed as a composite of the contained child shapes.
+ """
+ child_shape_elms = list(self.iter_shape_elms())
+
+ if not child_shape_elms:
+ return Emu(0), Emu(0), Emu(0), Emu(0)
+
+ min_x = min([xSp.x for xSp in child_shape_elms])
+ min_y = min([xSp.y for xSp in child_shape_elms])
+ max_x = max([(xSp.x + xSp.cx) for xSp in child_shape_elms])
+ max_y = max([(xSp.y + xSp.cy) for xSp in child_shape_elms])
+
+ x = min_x
+ y = min_y
+ cx = max_x - min_x
+ cy = max_y - min_y
+
+ return x, y, cx, cy
+
+ @property
+ def _next_shape_id(self) -> int:
+ """Return unique shape id suitable for use with a new shape element.
+
+ The returned id is the next available positive integer drawing object
+ id in shape tree, starting from 1 and making use of any gaps in
+ numbering. In practice, the minimum id is 2 because the spTree
+ element itself is always assigned id="1".
+ """
+ id_str_lst = self.xpath("//@id")
+ used_ids = [int(id_str) for id_str in id_str_lst if id_str.isdigit()]
+ for n in range(1, len(used_ids) + 2):
+ if n not in used_ids:
+ return n
+
+
+class CT_GroupShapeNonVisual(BaseShapeElement):
+ """`p:nvGrpSpPr` element."""
+
+ cNvPr = OneAndOnlyOne("p:cNvPr")
+
+
+class CT_GroupShapeProperties(BaseOxmlElement):
+ """p:grpSpPr element"""
+
+ get_or_add_xfrm: Callable[[], CT_Transform2D]
+
+ _tag_seq = (
+ "a:xfrm",
+ "a:noFill",
+ "a:solidFill",
+ "a:gradFill",
+ "a:blipFill",
+ "a:pattFill",
+ "a:grpFill",
+ "a:effectLst",
+ "a:effectDag",
+ "a:scene3d",
+ "a:extLst",
+ )
+ xfrm: CT_Transform2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:xfrm", successors=_tag_seq[1:]
+ )
+ effectLst = ZeroOrOne("a:effectLst", successors=_tag_seq[8:])
+ del _tag_seq
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/picture.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/picture.py
new file mode 100644
index 00000000..bacc9719
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/picture.py
@@ -0,0 +1,270 @@
+"""lxml custom element classes for picture-related XML elements."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+from xml.sax.saxutils import escape
+
+from pptx.oxml import parse_xml
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.shapes.shared import BaseShapeElement
+from pptx.oxml.xmlchemy import BaseOxmlElement, OneAndOnlyOne
+
+if TYPE_CHECKING:
+ from pptx.oxml.shapes.shared import CT_ShapeProperties
+ from pptx.util import Length
+
+
+class CT_Picture(BaseShapeElement):
+ """`p:pic` element.
+
+ Represents a picture shape (an image placement on a slide).
+ """
+
+ nvPicPr = OneAndOnlyOne("p:nvPicPr")
+ blipFill = OneAndOnlyOne("p:blipFill")
+ spPr: CT_ShapeProperties = OneAndOnlyOne("p:spPr") # pyright: ignore[reportAssignmentType]
+
+ @property
+ def blip_rId(self) -> str | None:
+ """Value of `p:blipFill/a:blip/@r:embed`.
+
+ Returns |None| if not present.
+ """
+ blip = self.blipFill.blip
+ if blip is not None and blip.rEmbed is not None:
+ return blip.rEmbed
+ return None
+
+ def crop_to_fit(self, image_size, view_size):
+ """
+ Set cropping values in `p:blipFill/a:srcRect` such that an image of
+ *image_size* will stretch to exactly fit *view_size* when its aspect
+ ratio is preserved.
+ """
+ self.blipFill.crop(self._fill_cropping(image_size, view_size))
+
+ def get_or_add_ln(self):
+ """
+ Return the <a:ln> grandchild element, newly added if not present.
+ """
+ return self.spPr.get_or_add_ln()
+
+ @property
+ def ln(self):
+ """
+ ``<a:ln>`` grand-child element or |None| if not present
+ """
+ return self.spPr.ln
+
+ @classmethod
+ def new_ph_pic(cls, id_, name, desc, rId):
+ """
+ Return a new `p:pic` placeholder element populated with the supplied
+ parameters.
+ """
+ return parse_xml(cls._pic_ph_tmpl() % (id_, name, desc, rId))
+
+ @classmethod
+ def new_pic(cls, shape_id, name, desc, rId, x, y, cx, cy):
+ """Return new `<p:pic>` element tree configured with supplied parameters."""
+ return parse_xml(cls._pic_tmpl() % (shape_id, name, escape(desc), rId, x, y, cx, cy))
+
+ @classmethod
+ def new_video_pic(
+ cls,
+ shape_id: int,
+ shape_name: str,
+ video_rId: str,
+ media_rId: str,
+ poster_frame_rId: str,
+ x: Length,
+ y: Length,
+ cx: Length,
+ cy: Length,
+ ) -> CT_Picture:
+ """Return a new `p:pic` populated with the specified video."""
+ return cast(
+ CT_Picture,
+ parse_xml(
+ cls._pic_video_tmpl()
+ % (
+ shape_id,
+ shape_name,
+ video_rId,
+ media_rId,
+ poster_frame_rId,
+ x,
+ y,
+ cx,
+ cy,
+ )
+ ),
+ )
+
+ @property
+ def srcRect_b(self):
+ """Value of `p:blipFill/a:srcRect/@b` or 0.0 if not present."""
+ return self._srcRect_x("b")
+
+ @srcRect_b.setter
+ def srcRect_b(self, value):
+ self.blipFill.get_or_add_srcRect().b = value
+
+ @property
+ def srcRect_l(self):
+ """Value of `p:blipFill/a:srcRect/@l` or 0.0 if not present."""
+ return self._srcRect_x("l")
+
+ @srcRect_l.setter
+ def srcRect_l(self, value):
+ self.blipFill.get_or_add_srcRect().l = value # noqa
+
+ @property
+ def srcRect_r(self):
+ """Value of `p:blipFill/a:srcRect/@r` or 0.0 if not present."""
+ return self._srcRect_x("r")
+
+ @srcRect_r.setter
+ def srcRect_r(self, value):
+ self.blipFill.get_or_add_srcRect().r = value
+
+ @property
+ def srcRect_t(self):
+ """Value of `p:blipFill/a:srcRect/@t` or 0.0 if not present."""
+ return self._srcRect_x("t")
+
+ @srcRect_t.setter
+ def srcRect_t(self, value):
+ self.blipFill.get_or_add_srcRect().t = value
+
+ def _fill_cropping(self, image_size, view_size):
+ """
+ Return a (left, top, right, bottom) 4-tuple containing the cropping
+ values required to display an image of *image_size* in *view_size*
+ when stretched proportionately. Each value is a percentage expressed
+ as a fraction of 1.0, e.g. 0.425 represents 42.5%. *image_size* and
+ *view_size* are each (width, height) pairs.
+ """
+
+ def aspect_ratio(width, height):
+ return width / height
+
+ ar_view = aspect_ratio(*view_size)
+ ar_image = aspect_ratio(*image_size)
+
+ if ar_view < ar_image: # image too wide
+ crop = (1.0 - (ar_view / ar_image)) / 2.0
+ return (crop, 0.0, crop, 0.0)
+ if ar_view > ar_image: # image too tall
+ crop = (1.0 - (ar_image / ar_view)) / 2.0
+ return (0.0, crop, 0.0, crop)
+ return (0.0, 0.0, 0.0, 0.0)
+
+ @classmethod
+ def _pic_ph_tmpl(cls):
+ return (
+ "<p:pic %s>\n"
+ " <p:nvPicPr>\n"
+ ' <p:cNvPr id="%%d" name="%%s" descr="%%s"/>\n'
+ " <p:cNvPicPr>\n"
+ ' <a:picLocks noGrp="1" noChangeAspect="1"/>\n'
+ " </p:cNvPicPr>\n"
+ " <p:nvPr/>\n"
+ " </p:nvPicPr>\n"
+ " <p:blipFill>\n"
+ ' <a:blip r:embed="%%s"/>\n'
+ " <a:stretch>\n"
+ " <a:fillRect/>\n"
+ " </a:stretch>\n"
+ " </p:blipFill>\n"
+ " <p:spPr/>\n"
+ "</p:pic>" % nsdecls("p", "a", "r")
+ )
+
+ @classmethod
+ def _pic_tmpl(cls):
+ return (
+ "<p:pic %s>\n"
+ " <p:nvPicPr>\n"
+ ' <p:cNvPr id="%%d" name="%%s" descr="%%s"/>\n'
+ " <p:cNvPicPr>\n"
+ ' <a:picLocks noChangeAspect="1"/>\n'
+ " </p:cNvPicPr>\n"
+ " <p:nvPr/>\n"
+ " </p:nvPicPr>\n"
+ " <p:blipFill>\n"
+ ' <a:blip r:embed="%%s"/>\n'
+ " <a:stretch>\n"
+ " <a:fillRect/>\n"
+ " </a:stretch>\n"
+ " </p:blipFill>\n"
+ " <p:spPr>\n"
+ " <a:xfrm>\n"
+ ' <a:off x="%%d" y="%%d"/>\n'
+ ' <a:ext cx="%%d" cy="%%d"/>\n'
+ " </a:xfrm>\n"
+ ' <a:prstGeom prst="rect">\n'
+ " <a:avLst/>\n"
+ " </a:prstGeom>\n"
+ " </p:spPr>\n"
+ "</p:pic>" % nsdecls("a", "p", "r")
+ )
+
+ @classmethod
+ def _pic_video_tmpl(cls):
+ return (
+ "<p:pic %s>\n"
+ " <p:nvPicPr>\n"
+ ' <p:cNvPr id="%%d" name="%%s">\n'
+ ' <a:hlinkClick r:id="" action="ppaction://media"/>\n'
+ " </p:cNvPr>\n"
+ " <p:cNvPicPr>\n"
+ ' <a:picLocks noChangeAspect="1"/>\n'
+ " </p:cNvPicPr>\n"
+ " <p:nvPr>\n"
+ ' <a:videoFile r:link="%%s"/>\n'
+ " <p:extLst>\n"
+ ' <p:ext uri="{DAA4B4D4-6D71-4841-9C94-3DE7FCFB9230}">\n'
+ ' <p14:media xmlns:p14="http://schemas.microsoft.com/of'
+ 'fice/powerpoint/2010/main" r:embed="%%s"/>\n'
+ " </p:ext>\n"
+ " </p:extLst>\n"
+ " </p:nvPr>\n"
+ " </p:nvPicPr>\n"
+ " <p:blipFill>\n"
+ ' <a:blip r:embed="%%s"/>\n'
+ " <a:stretch>\n"
+ " <a:fillRect/>\n"
+ " </a:stretch>\n"
+ " </p:blipFill>\n"
+ " <p:spPr>\n"
+ " <a:xfrm>\n"
+ ' <a:off x="%%d" y="%%d"/>\n'
+ ' <a:ext cx="%%d" cy="%%d"/>\n'
+ " </a:xfrm>\n"
+ ' <a:prstGeom prst="rect">\n'
+ " <a:avLst/>\n"
+ " </a:prstGeom>\n"
+ " </p:spPr>\n"
+ "</p:pic>" % nsdecls("a", "p", "r")
+ )
+
+ def _srcRect_x(self, attr_name):
+ """
+ Value of `p:blipFill/a:srcRect/@{attr_name}` or 0.0 if not present.
+ """
+ srcRect = self.blipFill.srcRect
+ if srcRect is None:
+ return 0.0
+ return getattr(srcRect, attr_name)
+
+
+class CT_PictureNonVisual(BaseOxmlElement):
+ """
+ ``<p:nvPicPr>`` element, containing non-visual properties for a picture
+ shape.
+ """
+
+ cNvPr = OneAndOnlyOne("p:cNvPr")
+ nvPr = OneAndOnlyOne("p:nvPr")
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/shared.py b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/shared.py
new file mode 100644
index 00000000..d9f94569
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/shapes/shared.py
@@ -0,0 +1,523 @@
+"""Common shape-related oxml objects."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable
+
+from pptx.dml.fill import CT_GradientFillProperties
+from pptx.enum.shapes import PP_PLACEHOLDER
+from pptx.oxml.ns import qn
+from pptx.oxml.simpletypes import (
+ ST_Angle,
+ ST_Coordinate,
+ ST_Direction,
+ ST_DrawingElementId,
+ ST_LineWidth,
+ ST_PlaceholderSize,
+ ST_PositiveCoordinate,
+ XsdBoolean,
+ XsdString,
+ XsdUnsignedInt,
+)
+from pptx.oxml.xmlchemy import (
+ BaseOxmlElement,
+ Choice,
+ OptionalAttribute,
+ OxmlElement,
+ RequiredAttribute,
+ ZeroOrOne,
+ ZeroOrOneChoice,
+)
+from pptx.util import Emu
+
+if TYPE_CHECKING:
+ from pptx.oxml.action import CT_Hyperlink
+ from pptx.oxml.shapes.autoshape import CT_CustomGeometry2D, CT_PresetGeometry2D
+ from pptx.util import Length
+
+
+class BaseShapeElement(BaseOxmlElement):
+ """Provides common behavior for shape element classes like CT_Shape, CT_Picture, etc."""
+
+ spPr: CT_ShapeProperties
+
+ @property
+ def cx(self) -> Length:
+ return self._get_xfrm_attr("cx")
+
+ @cx.setter
+ def cx(self, value):
+ self._set_xfrm_attr("cx", value)
+
+ @property
+ def cy(self) -> Length:
+ return self._get_xfrm_attr("cy")
+
+ @cy.setter
+ def cy(self, value):
+ self._set_xfrm_attr("cy", value)
+
+ @property
+ def flipH(self):
+ return bool(self._get_xfrm_attr("flipH"))
+
+ @flipH.setter
+ def flipH(self, value):
+ self._set_xfrm_attr("flipH", value)
+
+ @property
+ def flipV(self):
+ return bool(self._get_xfrm_attr("flipV"))
+
+ @flipV.setter
+ def flipV(self, value):
+ self._set_xfrm_attr("flipV", value)
+
+ def get_or_add_xfrm(self):
+ """Return the `a:xfrm` grandchild element, newly-added if not present.
+
+ This version works for `p:sp`, `p:cxnSp`, and `p:pic` elements, others will need to
+ override.
+ """
+ return self.spPr.get_or_add_xfrm()
+
+ @property
+ def has_ph_elm(self):
+ """
+ True if this shape element has a `p:ph` descendant, indicating it
+ is a placeholder shape. False otherwise.
+ """
+ return self.ph is not None
+
+ @property
+ def ph(self) -> CT_Placeholder | None:
+ """The `p:ph` descendant element if there is one, None otherwise."""
+ ph_elms = self.xpath("./*[1]/p:nvPr/p:ph")
+ if len(ph_elms) == 0:
+ return None
+ return ph_elms[0]
+
+ @property
+ def ph_idx(self) -> int:
+ """Integer value of placeholder idx attribute.
+
+ Raises |ValueError| if shape is not a placeholder.
+ """
+ ph = self.ph
+ if ph is None:
+ raise ValueError("not a placeholder shape")
+ return ph.idx
+
+ @property
+ def ph_orient(self) -> str:
+ """Placeholder orientation, e.g. 'vert'.
+
+ Raises |ValueError| if shape is not a placeholder.
+ """
+ ph = self.ph
+ if ph is None:
+ raise ValueError("not a placeholder shape")
+ return ph.orient
+
+ @property
+ def ph_sz(self) -> str:
+ """Placeholder size, e.g. ST_PlaceholderSize.HALF.
+
+ Raises `ValueError` if shape is not a placeholder.
+ """
+ ph = self.ph
+ if ph is None:
+ raise ValueError("not a placeholder shape")
+ return ph.sz
+
+ @property
+ def ph_type(self):
+ """Placeholder type, e.g. ST_PlaceholderType.TITLE ('title').
+
+ Raises `ValueError` if shape is not a placeholder.
+ """
+ ph = self.ph
+ if ph is None:
+ raise ValueError("not a placeholder shape")
+ return ph.type
+
+ @property
+ def rot(self) -> float:
+ """Float representing degrees this shape is rotated clockwise."""
+ xfrm = self.xfrm
+ if xfrm is None or xfrm.rot is None:
+ return 0.0
+ return xfrm.rot
+
+ @rot.setter
+ def rot(self, value: float):
+ self.get_or_add_xfrm().rot = value
+
+ @property
+ def shape_id(self):
+ """
+ Integer id of this shape
+ """
+ return self._nvXxPr.cNvPr.id
+
+ @property
+ def shape_name(self):
+ """
+ Name of this shape
+ """
+ return self._nvXxPr.cNvPr.name
+
+ @property
+ def txBody(self):
+ """Child `p:txBody` element, None if not present."""
+ return self.find(qn("p:txBody"))
+
+ @property
+ def x(self) -> Length:
+ return self._get_xfrm_attr("x")
+
+ @x.setter
+ def x(self, value):
+ self._set_xfrm_attr("x", value)
+
+ @property
+ def xfrm(self):
+ """The `a:xfrm` grandchild element or |None| if not found.
+
+ This version works for `p:sp`, `p:cxnSp`, and `p:pic` elements, others will need to
+ override.
+ """
+ return self.spPr.xfrm
+
+ @property
+ def y(self) -> Length:
+ return self._get_xfrm_attr("y")
+
+ @y.setter
+ def y(self, value):
+ self._set_xfrm_attr("y", value)
+
+ @property
+ def _nvXxPr(self):
+ """
+ Required non-visual shape properties element for this shape. Actual
+ name depends on the shape type, e.g. `p:nvPicPr` for picture
+ shape.
+ """
+ return self.xpath("./*[1]")[0]
+
+ def _get_xfrm_attr(self, name: str) -> Length | None:
+ xfrm = self.xfrm
+ if xfrm is None:
+ return None
+ return getattr(xfrm, name)
+
+ def _set_xfrm_attr(self, name, value):
+ xfrm = self.get_or_add_xfrm()
+ setattr(xfrm, name, value)
+
+
+class CT_ApplicationNonVisualDrawingProps(BaseOxmlElement):
+ """`p:nvPr` element."""
+
+ get_or_add_ph: Callable[[], CT_Placeholder]
+
+ ph = ZeroOrOne(
+ "p:ph",
+ successors=(
+ "a:audioCd",
+ "a:wavAudioFile",
+ "a:audioFile",
+ "a:videoFile",
+ "a:quickTimeFile",
+ "p:custDataLst",
+ "p:extLst",
+ ),
+ )
+
+
+class CT_LineProperties(BaseOxmlElement):
+ """Custom element class for <a:ln> element"""
+
+ _tag_seq = (
+ "a:noFill",
+ "a:solidFill",
+ "a:gradFill",
+ "a:pattFill",
+ "a:prstDash",
+ "a:custDash",
+ "a:round",
+ "a:bevel",
+ "a:miter",
+ "a:headEnd",
+ "a:tailEnd",
+ "a:extLst",
+ )
+ eg_lineFillProperties = ZeroOrOneChoice(
+ (
+ Choice("a:noFill"),
+ Choice("a:solidFill"),
+ Choice("a:gradFill"),
+ Choice("a:pattFill"),
+ ),
+ successors=_tag_seq[4:],
+ )
+ prstDash = ZeroOrOne("a:prstDash", successors=_tag_seq[5:])
+ custDash = ZeroOrOne("a:custDash", successors=_tag_seq[6:])
+ del _tag_seq
+ w = OptionalAttribute("w", ST_LineWidth, default=Emu(0))
+
+ @property
+ def eg_fillProperties(self):
+ """
+ Required to fulfill the interface used by dml.fill.
+ """
+ return self.eg_lineFillProperties
+
+ @property
+ def prstDash_val(self):
+ """Return value of `val` attribute of `a:prstDash` child.
+
+ Return |None| if not present.
+ """
+ prstDash = self.prstDash
+ if prstDash is None:
+ return None
+ return prstDash.val
+
+ @prstDash_val.setter
+ def prstDash_val(self, val):
+ self._remove_custDash()
+ prstDash = self.get_or_add_prstDash()
+ prstDash.val = val
+
+
+class CT_NonVisualDrawingProps(BaseOxmlElement):
+ """`p:cNvPr` custom element class."""
+
+ get_or_add_hlinkClick: Callable[[], CT_Hyperlink]
+ get_or_add_hlinkHover: Callable[[], CT_Hyperlink]
+
+ _tag_seq = ("a:hlinkClick", "a:hlinkHover", "a:extLst")
+ hlinkClick: CT_Hyperlink | None = ZeroOrOne("a:hlinkClick", successors=_tag_seq[1:])
+ hlinkHover: CT_Hyperlink | None = ZeroOrOne("a:hlinkHover", successors=_tag_seq[2:])
+ id = RequiredAttribute("id", ST_DrawingElementId)
+ name = RequiredAttribute("name", XsdString)
+ del _tag_seq
+
+
+class CT_Placeholder(BaseOxmlElement):
+ """`p:ph` custom element class."""
+
+ type: PP_PLACEHOLDER = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "type", PP_PLACEHOLDER, default=PP_PLACEHOLDER.OBJECT
+ )
+ orient: str = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "orient", ST_Direction, default=ST_Direction.HORZ
+ )
+ sz: str = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "sz", ST_PlaceholderSize, default=ST_PlaceholderSize.FULL
+ )
+ idx: int = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "idx", XsdUnsignedInt, default=0
+ )
+
+
+class CT_Point2D(BaseOxmlElement):
+ """
+ Custom element class for <a:off> element.
+ """
+
+ x: Length = RequiredAttribute("x", ST_Coordinate) # pyright: ignore[reportAssignmentType]
+ y: Length = RequiredAttribute("y", ST_Coordinate) # pyright: ignore[reportAssignmentType]
+
+
+class CT_PositiveSize2D(BaseOxmlElement):
+ """
+ Custom element class for <a:ext> element.
+ """
+
+ cx = RequiredAttribute("cx", ST_PositiveCoordinate)
+ cy = RequiredAttribute("cy", ST_PositiveCoordinate)
+
+
+class CT_ShapeProperties(BaseOxmlElement):
+ """Custom element class for `p:spPr` element.
+
+ Shared by `p:sp`, `p:cxnSp`, and `p:pic` elements as well as a few more obscure ones.
+ """
+
+ get_or_add_xfrm: Callable[[], CT_Transform2D]
+ get_or_add_ln: Callable[[], CT_LineProperties]
+ _add_prstGeom: Callable[[], CT_PresetGeometry2D]
+ _remove_custGeom: Callable[[], None]
+
+ _tag_seq = (
+ "a:xfrm",
+ "a:custGeom",
+ "a:prstGeom",
+ "a:noFill",
+ "a:solidFill",
+ "a:gradFill",
+ "a:blipFill",
+ "a:pattFill",
+ "a:grpFill",
+ "a:ln",
+ "a:effectLst",
+ "a:effectDag",
+ "a:scene3d",
+ "a:sp3d",
+ "a:extLst",
+ )
+ xfrm: CT_Transform2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:xfrm", successors=_tag_seq[1:]
+ )
+ custGeom: CT_CustomGeometry2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:custGeom", successors=_tag_seq[2:]
+ )
+ prstGeom: CT_PresetGeometry2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:prstGeom", successors=_tag_seq[3:]
+ )
+ eg_fillProperties = ZeroOrOneChoice(
+ (
+ Choice("a:noFill"),
+ Choice("a:solidFill"),
+ Choice("a:gradFill"),
+ Choice("a:blipFill"),
+ Choice("a:pattFill"),
+ Choice("a:grpFill"),
+ ),
+ successors=_tag_seq[9:],
+ )
+ ln: CT_LineProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:ln", successors=_tag_seq[10:]
+ )
+ effectLst = ZeroOrOne("a:effectLst", successors=_tag_seq[11:])
+ del _tag_seq
+
+ @property
+ def cx(self):
+ """
+ Shape width as an instance of Emu, or None if not present.
+ """
+ cx_str_lst = self.xpath("./a:xfrm/a:ext/@cx")
+ if not cx_str_lst:
+ return None
+ return Emu(cx_str_lst[0])
+
+ @property
+ def cy(self):
+ """
+ Shape height as an instance of Emu, or None if not present.
+ """
+ cy_str_lst = self.xpath("./a:xfrm/a:ext/@cy")
+ if not cy_str_lst:
+ return None
+ return Emu(cy_str_lst[0])
+
+ @property
+ def x(self) -> Length | None:
+ """Distance between the left edge of the slide and left edge of the shape.
+
+ 0 if not present.
+ """
+ x_str_lst = self.xpath("./a:xfrm/a:off/@x")
+ if not x_str_lst:
+ return None
+ return Emu(x_str_lst[0])
+
+ @property
+ def y(self):
+ """
+ The offset of the top of the shape from the top of the slide, as an
+ instance of Emu. None if not present.
+ """
+ y_str_lst = self.xpath("./a:xfrm/a:off/@y")
+ if not y_str_lst:
+ return None
+ return Emu(y_str_lst[0])
+
+ def _new_gradFill(self):
+ return CT_GradientFillProperties.new_gradFill()
+
+
+class CT_Transform2D(BaseOxmlElement):
+ """`a:xfrm` custom element class.
+
+ NOTE: this is a composite including CT_GroupTransform2D, which appears
+ with the `a:xfrm` tag in a group shape (including a slide `p:spTree`).
+ """
+
+ _tag_seq = ("a:off", "a:ext", "a:chOff", "a:chExt")
+ off: CT_Point2D | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:off", successors=_tag_seq[1:]
+ )
+ ext = ZeroOrOne("a:ext", successors=_tag_seq[2:])
+ chOff = ZeroOrOne("a:chOff", successors=_tag_seq[3:])
+ chExt = ZeroOrOne("a:chExt", successors=_tag_seq[4:])
+ del _tag_seq
+ rot: float | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "rot", ST_Angle, default=0.0
+ )
+ flipH = OptionalAttribute("flipH", XsdBoolean, default=False)
+ flipV = OptionalAttribute("flipV", XsdBoolean, default=False)
+
+ @property
+ def x(self):
+ off = self.off
+ if off is None:
+ return None
+ return off.x
+
+ @x.setter
+ def x(self, value):
+ off = self.get_or_add_off()
+ off.x = value
+
+ @property
+ def y(self):
+ off = self.off
+ if off is None:
+ return None
+ return off.y
+
+ @y.setter
+ def y(self, value):
+ off = self.get_or_add_off()
+ off.y = value
+
+ @property
+ def cx(self):
+ ext = self.ext
+ if ext is None:
+ return None
+ return ext.cx
+
+ @cx.setter
+ def cx(self, value):
+ ext = self.get_or_add_ext()
+ ext.cx = value
+
+ @property
+ def cy(self):
+ ext = self.ext
+ if ext is None:
+ return None
+ return ext.cy
+
+ @cy.setter
+ def cy(self, value):
+ ext = self.get_or_add_ext()
+ ext.cy = value
+
+ def _new_ext(self):
+ ext = OxmlElement("a:ext")
+ ext.cx = 0
+ ext.cy = 0
+ return ext
+
+ def _new_off(self):
+ off = OxmlElement("a:off")
+ off.x = 0
+ off.y = 0
+ return off
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/simpletypes.py b/.venv/lib/python3.12/site-packages/pptx/oxml/simpletypes.py
new file mode 100644
index 00000000..6ceb06f7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/simpletypes.py
@@ -0,0 +1,740 @@
+"""Simple-type classes.
+
+A "simple-type" is a scalar type, generally serving as an XML attribute. This is in contrast to a
+"complex-type" which would specify an XML element.
+
+These objects providing validation and format translation for values stored in XML element
+attributes. Naming generally corresponds to the simple type in the associated XML schema.
+"""
+
+from __future__ import annotations
+
+import numbers
+from typing import Any
+
+from pptx.exc import InvalidXmlError
+from pptx.util import Centipoints, Emu
+
+
+class BaseSimpleType:
+ @classmethod
+ def from_xml(cls, xml_value: str) -> Any:
+ return cls.convert_from_xml(xml_value)
+
+ @classmethod
+ def to_xml(cls, value: Any) -> str:
+ cls.validate(value)
+ str_value = cls.convert_to_xml(value)
+ return str_value
+
+ @classmethod
+ def validate_float(cls, value: Any):
+ """Note that int values are accepted."""
+ if not isinstance(value, (int, float)):
+ raise TypeError("value must be a number, got %s" % type(value))
+
+ @classmethod
+ def validate_int(cls, value):
+ if not isinstance(value, numbers.Integral):
+ raise TypeError("value must be an integral type, got %s" % type(value))
+
+ @classmethod
+ def validate_float_in_range(cls, value, min_inclusive, max_inclusive):
+ cls.validate_float(value)
+ if value < min_inclusive or value > max_inclusive:
+ raise ValueError(
+ "value must be in range %s to %s inclusive, got %s"
+ % (min_inclusive, max_inclusive, value)
+ )
+
+ @classmethod
+ def validate_int_in_range(cls, value, min_inclusive, max_inclusive):
+ cls.validate_int(value)
+ if value < min_inclusive or value > max_inclusive:
+ raise ValueError(
+ "value must be in range %d to %d inclusive, got %d"
+ % (min_inclusive, max_inclusive, value)
+ )
+
+ @classmethod
+ def validate_string(cls, value):
+ if isinstance(value, str):
+ return value
+ try:
+ if isinstance(value, basestring):
+ return value
+ except NameError: # means we're on Python 3
+ pass
+ raise TypeError("value must be a string, got %s" % type(value))
+
+
+class BaseFloatType(BaseSimpleType):
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ return float(str_value)
+
+ @classmethod
+ def convert_to_xml(cls, value):
+ return str(float(value))
+
+ @classmethod
+ def validate(cls, value):
+ if not isinstance(value, (int, float)):
+ raise TypeError("value must be a number, got %s" % type(value))
+
+
+class BaseIntType(BaseSimpleType):
+ @classmethod
+ def convert_from_percent_literal(cls, str_value):
+ int_str = str_value.replace("%", "")
+ return int(int_str)
+
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ return int(str_value)
+
+ @classmethod
+ def convert_to_xml(cls, value):
+ return str(value)
+
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int(value)
+
+
+class BaseStringType(BaseSimpleType):
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ return str_value
+
+ @classmethod
+ def convert_to_xml(cls, value):
+ return value
+
+ @classmethod
+ def validate(cls, value):
+ cls.validate_string(value)
+
+
+class BaseStringEnumerationType(BaseStringType):
+ @classmethod
+ def validate(cls, value):
+ cls.validate_string(value)
+ if value not in cls._members:
+ raise ValueError("must be one of %s, got '%s'" % (cls._members, value))
+
+
+class XsdAnyUri(BaseStringType):
+ """
+ There's a regular expression this is supposed to meet but so far thinking
+ spending cycles on validating wouldn't be worth it for the number of
+ programming errors it would catch.
+ """
+
+
+class XsdBoolean(BaseSimpleType):
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ if str_value not in ("1", "0", "true", "false"):
+ raise InvalidXmlError(
+ "value must be one of '1', '0', 'true' or 'false', got '%s'" % str_value
+ )
+ return str_value in ("1", "true")
+
+ @classmethod
+ def convert_to_xml(cls, value):
+ return {True: "1", False: "0"}[value]
+
+ @classmethod
+ def validate(cls, value):
+ if value not in (True, False):
+ raise TypeError(
+ "only True or False (and possibly None) may be assigned, got" " '%s'" % value
+ )
+
+
+class XsdDouble(BaseFloatType):
+ pass
+
+
+class XsdId(BaseStringType):
+ """
+ String that must begin with a letter or underscore and cannot contain any
+ colons. Not fully validated because not used in external API.
+ """
+
+
+class XsdInt(BaseIntType):
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, -2147483648, 2147483647)
+
+
+class XsdLong(BaseIntType):
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, -9223372036854775808, 9223372036854775807)
+
+
+class XsdString(BaseStringType):
+ pass
+
+
+class XsdStringEnumeration(BaseStringEnumerationType):
+ """
+ Set of enumerated xsd:string values.
+ """
+
+
+class XsdToken(BaseStringType):
+ """
+ xsd:string with whitespace collapsing, e.g. multiple spaces reduced to
+ one, leading and trailing space stripped.
+ """
+
+
+class XsdTokenEnumeration(BaseStringEnumerationType):
+ """
+ xsd:string with whitespace collapsing, e.g. multiple spaces reduced to
+ one, leading and trailing space stripped.
+ """
+
+
+class XsdUnsignedByte(BaseIntType):
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, 0, 255)
+
+
+class XsdUnsignedInt(BaseIntType):
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, 0, 4294967295)
+
+
+class XsdUnsignedShort(BaseIntType):
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, 0, 65535)
+
+
+class ST_Angle(XsdInt):
+ """
+ Valid values for `rot` attribute on `<a:xfrm>` element. 60000ths of
+ a degree rotation.
+ """
+
+ DEGREE_INCREMENTS = 60000
+ THREE_SIXTY = 360 * DEGREE_INCREMENTS
+
+ @classmethod
+ def convert_from_xml(cls, str_value: str) -> float:
+ rot = int(str_value) % cls.THREE_SIXTY
+ return float(rot) / cls.DEGREE_INCREMENTS
+
+ @classmethod
+ def convert_to_xml(cls, value):
+ """
+ Convert signed angle float like -42.42 to int 60000 per degree,
+ normalized to positive value.
+ """
+ # modulo normalizes negative and >360 degree values
+ rot = int(round(value * cls.DEGREE_INCREMENTS)) % cls.THREE_SIXTY
+ return str(rot)
+
+ @classmethod
+ def validate(cls, value):
+ BaseFloatType.validate(value)
+
+
+class ST_AxisUnit(XsdDouble):
+ """
+ Valid values for val attribute on c:majorUnit and others.
+ """
+
+ @classmethod
+ def validate(cls, value):
+ super(ST_AxisUnit, cls).validate(value)
+ if value <= 0.0:
+ raise ValueError("must be positive numeric value, got %s" % value)
+
+
+class ST_BarDir(XsdStringEnumeration):
+ """
+ Valid values for <c:barDir val="?"> attribute
+ """
+
+ BAR = "bar"
+ COL = "col"
+
+ _members = (BAR, COL)
+
+
+class ST_BubbleScale(BaseIntType):
+ """
+ String value is an integer in range 0-300, representing a percent,
+ optionally including a '%' suffix.
+ """
+
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ if "%" in str_value:
+ return cls.convert_from_percent_literal(str_value)
+ return super(ST_BubbleScale, cls).convert_from_xml(str_value)
+
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, 0, 300)
+
+
+class ST_ContentType(XsdString):
+ """
+ Has a pretty wicked regular expression it needs to match in the schema,
+ but figuring it's not worth the trouble or run time to identify
+ a programming error (as opposed to a user/runtime error).
+ """
+
+ pass
+
+
+class ST_Coordinate(BaseSimpleType):
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ if "i" in str_value or "m" in str_value or "p" in str_value:
+ return ST_UniversalMeasure.convert_from_xml(str_value)
+ return Emu(int(str_value))
+
+ @classmethod
+ def convert_to_xml(cls, value):
+ return str(value)
+
+ @classmethod
+ def validate(cls, value):
+ ST_CoordinateUnqualified.validate(value)
+
+
+class ST_Coordinate32(BaseSimpleType):
+ """
+ xsd:union of ST_Coordinate32Unqualified, ST_UniversalMeasure
+ """
+
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ if "i" in str_value or "m" in str_value or "p" in str_value:
+ return ST_UniversalMeasure.convert_from_xml(str_value)
+ return ST_Coordinate32Unqualified.convert_from_xml(str_value)
+
+ @classmethod
+ def convert_to_xml(cls, value):
+ return ST_Coordinate32Unqualified.convert_to_xml(value)
+
+ @classmethod
+ def validate(cls, value):
+ ST_Coordinate32Unqualified.validate(value)
+
+
+class ST_Coordinate32Unqualified(XsdInt):
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ return Emu(int(str_value))
+
+
+class ST_CoordinateUnqualified(XsdLong):
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, -27273042329600, 27273042316900)
+
+
+class ST_Direction(XsdTokenEnumeration):
+ """Valid values for `<p:ph orient="...">` attribute."""
+
+ HORZ = "horz"
+ VERT = "vert"
+
+ _members = (HORZ, VERT)
+
+
+class ST_DrawingElementId(XsdUnsignedInt):
+ pass
+
+
+class ST_Extension(XsdString):
+ """
+ Has a regular expression it needs to match in the schema, but figuring
+ it's not worth the trouble or run time to identify a programming error
+ (as opposed to a user/runtime error).
+ """
+
+ pass
+
+
+class ST_GapAmount(BaseIntType):
+ """
+ String value is an integer in range 0-500, representing a percent,
+ optionally including a '%' suffix.
+ """
+
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ if "%" in str_value:
+ return cls.convert_from_percent_literal(str_value)
+ return super(ST_GapAmount, cls).convert_from_xml(str_value)
+
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, 0, 500)
+
+
+class ST_Grouping(XsdStringEnumeration):
+ """
+ Valid values for <c:grouping val=""> attribute. Overloaded for use as
+ ST_BarGrouping using same tag name.
+ """
+
+ CLUSTERED = "clustered"
+ PERCENT_STACKED = "percentStacked"
+ STACKED = "stacked"
+ STANDARD = "standard"
+
+ _members = (CLUSTERED, PERCENT_STACKED, STACKED, STANDARD)
+
+
+class ST_HexColorRGB(BaseStringType):
+ @classmethod
+ def convert_to_xml(cls, value):
+ """
+ Keep alpha characters all uppercase just for consistency.
+ """
+ return value.upper()
+
+ @classmethod
+ def validate(cls, value):
+ # must be string ---------------
+ str_value = cls.validate_string(value)
+
+ # must be 6 chars long----------
+ if len(str_value) != 6:
+ raise ValueError("RGB string must be six characters long, got '%s'" % str_value)
+
+ # must parse as hex int --------
+ try:
+ int(str_value, 16)
+ except ValueError:
+ raise ValueError("RGB string must be valid hex string, got '%s'" % str_value)
+
+
+class ST_LayoutMode(XsdStringEnumeration):
+ """
+ Valid values for `val` attribute on c:xMode and other elements of type
+ CT_LayoutMode.
+ """
+
+ EDGE = "edge"
+ FACTOR = "factor"
+
+ _members = (EDGE, FACTOR)
+
+
+class ST_LblOffset(XsdUnsignedShort):
+ """
+ Unsigned integer value between 0 and 1000 inclusive, with optional
+ percent character ('%') suffix.
+ """
+
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ if str_value.endswith("%"):
+ return cls.convert_from_percent_literal(str_value)
+ return int(str_value)
+
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, 0, 1000)
+
+
+class ST_LineWidth(XsdInt):
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ return Emu(int(str_value))
+
+ @classmethod
+ def validate(cls, value):
+ super(ST_LineWidth, cls).validate(value)
+ if value < 0 or value > 20116800:
+ raise ValueError(
+ "value must be in range 0-20116800 inclusive (0-1584 points)" ", got %d" % value
+ )
+
+
+class ST_MarkerSize(XsdUnsignedByte):
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, 2, 72)
+
+
+class ST_Orientation(XsdStringEnumeration):
+ """Valid values for `val` attribute on c:orientation (CT_Orientation)."""
+
+ MAX_MIN = "maxMin"
+ MIN_MAX = "minMax"
+
+ _members = (MAX_MIN, MIN_MAX)
+
+
+class ST_Overlap(BaseIntType):
+ """
+ String value is an integer in range -100..100, representing a percent,
+ optionally including a '%' suffix.
+ """
+
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ if "%" in str_value:
+ return cls.convert_from_percent_literal(str_value)
+ return super(ST_Overlap, cls).convert_from_xml(str_value)
+
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, -100, 100)
+
+
+class ST_Percentage(BaseIntType):
+ """Percentage value like 42000 or '42.0%'
+
+ Either an integer literal representing 1000ths of a percent
+ (e.g. "42000"), or a floating point literal with a '%' suffix
+ (e.g. "42.0%).
+ """
+
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ if "%" in str_value:
+ return cls._convert_from_percent_literal(str_value)
+ return int(str_value) / 100000.0
+
+ @classmethod
+ def convert_to_xml(cls, value):
+ return str(int(round(value * 100000.0)))
+
+ @classmethod
+ def validate(cls, value):
+ cls.validate_float_in_range(value, -21474.83648, 21474.83647)
+
+ @classmethod
+ def _convert_from_percent_literal(cls, str_value):
+ float_part = str_value[:-1] # trim off '%' character
+ return float(float_part) / 100.0
+
+
+class ST_PlaceholderSize(XsdTokenEnumeration):
+ """
+ Valid values for <p:ph> sz (size) attribute
+ """
+
+ FULL = "full"
+ HALF = "half"
+ QUARTER = "quarter"
+
+ _members = (FULL, HALF, QUARTER)
+
+
+class ST_PositiveCoordinate(XsdLong):
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ int_value = super(ST_PositiveCoordinate, cls).convert_from_xml(str_value)
+ return Emu(int_value)
+
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, 0, 27273042316900)
+
+
+class ST_PositiveFixedAngle(ST_Angle):
+ """Valid values for `a:lin@ang`.
+
+ 60000ths of a degree rotation, constained to positive angles less than
+ 360 degrees.
+ """
+
+ @classmethod
+ def convert_to_xml(cls, degrees):
+ """Convert signed angle float like -427.42 to int 60000 per degree.
+
+ Value is normalized to a positive value less than 360 degrees.
+ """
+ if degrees < 0.0:
+ degrees %= -360
+ degrees += 360
+ elif degrees > 0.0:
+ degrees %= 360
+
+ return str(int(round(degrees * cls.DEGREE_INCREMENTS)))
+
+
+class ST_PositiveFixedPercentage(ST_Percentage):
+ """Percentage value between 0 and 100% like 42000 or '42.0%'
+
+ Either an integer literal representing 1000ths of a percent
+ (e.g. "42000"), or a floating point literal with a '%' suffix
+ (e.g. "42.0%). Value is constrained to range of 0% to 100%. The source
+ value is a float between 0.0 and 1.0.
+ """
+
+ @classmethod
+ def validate(cls, value):
+ cls.validate_float_in_range(value, 0.0, 1.0)
+
+
+class ST_RelationshipId(XsdString):
+ pass
+
+
+class ST_SlideId(XsdUnsignedInt):
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, 256, 2147483647)
+
+
+class ST_SlideSizeCoordinate(BaseIntType):
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ return Emu(str_value)
+
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int(value)
+ if value < 914400 or value > 51206400:
+ raise ValueError(
+ "value must be in range(914400, 51206400) (1-56 inches), got" " %d" % value
+ )
+
+
+class ST_Style(XsdUnsignedByte):
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, 1, 48)
+
+
+class ST_TargetMode(XsdString):
+ """
+ The valid values for the ``TargetMode`` attribute in a Relationship
+ element, either 'External' or 'Internal'.
+ """
+
+ @classmethod
+ def validate(cls, value):
+ cls.validate_string(value)
+ if value not in ("External", "Internal"):
+ raise ValueError("must be one of 'Internal' or 'External', got '%s'" % value)
+
+
+class ST_TextFontScalePercentOrPercentString(BaseFloatType):
+ """
+ Valid values for the `fontScale` attribute of ``<a:normAutofit>``.
+ Translates to a float value.
+ """
+
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ if str_value.endswith("%"):
+ return float(str_value[:-1]) # trim off '%' character
+ return int(str_value) / 1000.0
+
+ @classmethod
+ def convert_to_xml(cls, value):
+ return str(int(value * 1000.0))
+
+ @classmethod
+ def validate(cls, value):
+ BaseFloatType.validate(value)
+ if value < 1.0 or value > 100.0:
+ raise ValueError("value must be in range 1.0..100.0 (percent), got %s" % value)
+
+
+class ST_TextFontSize(BaseIntType):
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, 100, 400000)
+
+
+class ST_TextIndentLevelType(BaseIntType):
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, 0, 8)
+
+
+class ST_TextSpacingPercentOrPercentString(BaseFloatType):
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ if str_value.endswith("%"):
+ return cls._convert_from_percent_literal(str_value)
+ return int(str_value) / 100000.0
+
+ @classmethod
+ def _convert_from_percent_literal(cls, str_value):
+ float_part = str_value[:-1] # trim off '%' character
+ percent_value = float(float_part)
+ lines_value = percent_value / 100.0
+ return lines_value
+
+ @classmethod
+ def convert_to_xml(cls, value):
+ """
+ 1.75 -> '175000'
+ """
+ lines = value * 100000.0
+ return str(int(round(lines)))
+
+ @classmethod
+ def validate(cls, value):
+ cls.validate_float_in_range(value, 0.0, 132.0)
+
+
+class ST_TextSpacingPoint(BaseIntType):
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ """
+ Reads string integer centipoints, returns |Length| value.
+ """
+ return Centipoints(int(str_value))
+
+ @classmethod
+ def convert_to_xml(cls, value):
+ length = Emu(value) # just to make sure
+ return str(length.centipoints)
+
+ @classmethod
+ def validate(cls, value):
+ cls.validate_int_in_range(value, 0, 20116800)
+
+
+class ST_TextTypeface(XsdString):
+ pass
+
+
+class ST_TextWrappingType(XsdTokenEnumeration):
+ """
+ Valid values for <a:bodyPr wrap=""> attribute
+ """
+
+ NONE = "none"
+ SQUARE = "square"
+
+ _members = (NONE, SQUARE)
+
+
+class ST_UniversalMeasure(BaseSimpleType):
+ @classmethod
+ def convert_from_xml(cls, str_value):
+ float_part, units_part = str_value[:-2], str_value[-2:]
+ quantity = float(float_part)
+ multiplier = {
+ "mm": 36000,
+ "cm": 360000,
+ "in": 914400,
+ "pt": 12700,
+ "pc": 152400,
+ "pi": 152400,
+ }[units_part]
+ emu_value = Emu(int(round(quantity * multiplier)))
+ return emu_value
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/slide.py b/.venv/lib/python3.12/site-packages/pptx/oxml/slide.py
new file mode 100644
index 00000000..37a9780f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/slide.py
@@ -0,0 +1,347 @@
+"""Slide-related custom element classes, including those for masters."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, cast
+
+from pptx.oxml import parse_from_template, parse_xml
+from pptx.oxml.dml.fill import CT_GradientFillProperties
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.simpletypes import XsdString
+from pptx.oxml.xmlchemy import (
+ BaseOxmlElement,
+ Choice,
+ OneAndOnlyOne,
+ OptionalAttribute,
+ RequiredAttribute,
+ ZeroOrMore,
+ ZeroOrOne,
+ ZeroOrOneChoice,
+)
+
+if TYPE_CHECKING:
+ from pptx.oxml.shapes.groupshape import CT_GroupShape
+
+
+class _BaseSlideElement(BaseOxmlElement):
+ """Base class for the six slide types, providing common methods."""
+
+ cSld: CT_CommonSlideData
+
+ @property
+ def spTree(self) -> CT_GroupShape:
+ """Return required `p:cSld/p:spTree` grandchild."""
+ return self.cSld.spTree
+
+
+class CT_Background(BaseOxmlElement):
+ """`p:bg` element."""
+
+ _insert_bgPr: Callable[[CT_BackgroundProperties], None]
+
+ # ---these two are actually a choice, not a sequence, but simpler for
+ # ---present purposes this way.
+ _tag_seq = ("p:bgPr", "p:bgRef")
+ bgPr: CT_BackgroundProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "p:bgPr", successors=()
+ )
+ bgRef = ZeroOrOne("p:bgRef", successors=())
+ del _tag_seq
+
+ def add_noFill_bgPr(self):
+ """Return a new `p:bgPr` element with noFill properties."""
+ xml = "<p:bgPr %s>\n" " <a:noFill/>\n" " <a:effectLst/>\n" "</p:bgPr>" % nsdecls("a", "p")
+ bgPr = cast(CT_BackgroundProperties, parse_xml(xml))
+ self._insert_bgPr(bgPr)
+ return bgPr
+
+
+class CT_BackgroundProperties(BaseOxmlElement):
+ """`p:bgPr` element."""
+
+ _tag_seq = (
+ "a:noFill",
+ "a:solidFill",
+ "a:gradFill",
+ "a:blipFill",
+ "a:pattFill",
+ "a:grpFill",
+ "a:effectLst",
+ "a:effectDag",
+ "a:extLst",
+ )
+ eg_fillProperties = ZeroOrOneChoice(
+ (
+ Choice("a:noFill"),
+ Choice("a:solidFill"),
+ Choice("a:gradFill"),
+ Choice("a:blipFill"),
+ Choice("a:pattFill"),
+ Choice("a:grpFill"),
+ ),
+ successors=_tag_seq[6:],
+ )
+ del _tag_seq
+
+ def _new_gradFill(self):
+ """Override default to add default gradient subtree."""
+ return CT_GradientFillProperties.new_gradFill()
+
+
+class CT_CommonSlideData(BaseOxmlElement):
+ """`p:cSld` element."""
+
+ _remove_bg: Callable[[], None]
+ get_or_add_bg: Callable[[], CT_Background]
+
+ _tag_seq = ("p:bg", "p:spTree", "p:custDataLst", "p:controls", "p:extLst")
+ bg: CT_Background | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "p:bg", successors=_tag_seq[1:]
+ )
+ spTree: CT_GroupShape = OneAndOnlyOne("p:spTree") # pyright: ignore[reportAssignmentType]
+ del _tag_seq
+ name: str = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "name", XsdString, default=""
+ )
+
+ def get_or_add_bgPr(self) -> CT_BackgroundProperties:
+ """Return `p:bg/p:bgPr` grandchild.
+
+ If no such grandchild is present, any existing `p:bg` child is first removed and a new
+ default `p:bg` with noFill settings is added.
+ """
+ bg = self.bg
+ if bg is None or bg.bgPr is None:
+ bg = self._change_to_noFill_bg()
+ return cast(CT_BackgroundProperties, bg.bgPr)
+
+ def _change_to_noFill_bg(self) -> CT_Background:
+ """Establish a `p:bg` child with no-fill settings.
+
+ Any existing `p:bg` child is first removed.
+ """
+ self._remove_bg()
+ bg = self.get_or_add_bg()
+ bg.add_noFill_bgPr()
+ return bg
+
+
+class CT_NotesMaster(_BaseSlideElement):
+ """`p:notesMaster` element, root of a notes master part."""
+
+ _tag_seq = ("p:cSld", "p:clrMap", "p:hf", "p:notesStyle", "p:extLst")
+ cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
+ del _tag_seq
+
+ @classmethod
+ def new_default(cls) -> CT_NotesMaster:
+ """Return a new `p:notesMaster` element based on the built-in default template."""
+ return cast(CT_NotesMaster, parse_from_template("notesMaster"))
+
+
+class CT_NotesSlide(_BaseSlideElement):
+ """`p:notes` element, root of a notes slide part."""
+
+ _tag_seq = ("p:cSld", "p:clrMapOvr", "p:extLst")
+ cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
+ del _tag_seq
+
+ @classmethod
+ def new(cls) -> CT_NotesSlide:
+ """Return a new ``<p:notes>`` element based on the default template.
+
+ Note that the template does not include placeholders, which must be subsequently cloned
+ from the notes master.
+ """
+ return cast(CT_NotesSlide, parse_from_template("notes"))
+
+
+class CT_Slide(_BaseSlideElement):
+ """`p:sld` element, root element of a slide part (XML document)."""
+
+ _tag_seq = ("p:cSld", "p:clrMapOvr", "p:transition", "p:timing", "p:extLst")
+ cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
+ clrMapOvr = ZeroOrOne("p:clrMapOvr", successors=_tag_seq[2:])
+ timing = ZeroOrOne("p:timing", successors=_tag_seq[4:])
+ del _tag_seq
+
+ @classmethod
+ def new(cls) -> CT_Slide:
+ """Return new `p:sld` element configured as base slide shape."""
+ return cast(CT_Slide, parse_xml(cls._sld_xml()))
+
+ @property
+ def bg(self):
+ """Return `p:bg` grandchild or None if not present."""
+ return self.cSld.bg
+
+ def get_or_add_childTnLst(self):
+ """Return parent element for a new `p:video` child element.
+
+ The `p:video` element causes play controls to appear under a video
+ shape (pic shape containing video). There can be more than one video
+ shape on a slide, which causes the precondition to vary. It needs to
+ handle the case when there is no `p:sld/p:timing` element and when
+ that element already exists. If the case isn't simple, it just nukes
+ what's there and adds a fresh one. This could theoretically remove
+ desired existing timing information, but there isn't any evidence
+ available to me one way or the other, so I've taken the simple
+ approach.
+ """
+ childTnLst = self._childTnLst
+ if childTnLst is None:
+ childTnLst = self._add_childTnLst()
+ return childTnLst
+
+ def _add_childTnLst(self):
+ """Add `./p:timing/p:tnLst/p:par/p:cTn/p:childTnLst` descendant.
+
+ Any existing `p:timing` child element is ruthlessly removed and
+ replaced.
+ """
+ self.remove(self.get_or_add_timing())
+ timing = parse_xml(self._childTnLst_timing_xml())
+ self._insert_timing(timing)
+ return timing.xpath("./p:tnLst/p:par/p:cTn/p:childTnLst")[0]
+
+ @property
+ def _childTnLst(self):
+ """Return `./p:timing/p:tnLst/p:par/p:cTn/p:childTnLst` descendant.
+
+ Return None if that element is not present.
+ """
+ childTnLsts = self.xpath("./p:timing/p:tnLst/p:par/p:cTn/p:childTnLst")
+ if not childTnLsts:
+ return None
+ return childTnLsts[0]
+
+ @staticmethod
+ def _childTnLst_timing_xml():
+ return (
+ "<p:timing %s>\n"
+ " <p:tnLst>\n"
+ " <p:par>\n"
+ ' <p:cTn id="1" dur="indefinite" restart="never" nodeType="'
+ 'tmRoot">\n'
+ " <p:childTnLst/>\n"
+ " </p:cTn>\n"
+ " </p:par>\n"
+ " </p:tnLst>\n"
+ "</p:timing>" % nsdecls("p")
+ )
+
+ @staticmethod
+ def _sld_xml():
+ return (
+ "<p:sld %s>\n"
+ " <p:cSld>\n"
+ " <p:spTree>\n"
+ " <p:nvGrpSpPr>\n"
+ ' <p:cNvPr id="1" name=""/>\n'
+ " <p:cNvGrpSpPr/>\n"
+ " <p:nvPr/>\n"
+ " </p:nvGrpSpPr>\n"
+ " <p:grpSpPr/>\n"
+ " </p:spTree>\n"
+ " </p:cSld>\n"
+ " <p:clrMapOvr>\n"
+ " <a:masterClrMapping/>\n"
+ " </p:clrMapOvr>\n"
+ "</p:sld>" % nsdecls("a", "p", "r")
+ )
+
+
+class CT_SlideLayout(_BaseSlideElement):
+ """`p:sldLayout` element, root of a slide layout part."""
+
+ _tag_seq = ("p:cSld", "p:clrMapOvr", "p:transition", "p:timing", "p:hf", "p:extLst")
+ cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
+ del _tag_seq
+
+
+class CT_SlideLayoutIdList(BaseOxmlElement):
+ """`p:sldLayoutIdLst` element, child of `p:sldMaster`.
+
+ Contains references to the slide layouts that inherit from the slide master.
+ """
+
+ sldLayoutId_lst: list[CT_SlideLayoutIdListEntry]
+
+ sldLayoutId = ZeroOrMore("p:sldLayoutId")
+
+
+class CT_SlideLayoutIdListEntry(BaseOxmlElement):
+ """`p:sldLayoutId` element, child of `p:sldLayoutIdLst`.
+
+ Contains a reference to a slide layout.
+ """
+
+ rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType]
+
+
+class CT_SlideMaster(_BaseSlideElement):
+ """`p:sldMaster` element, root of a slide master part."""
+
+ get_or_add_sldLayoutIdLst: Callable[[], CT_SlideLayoutIdList]
+
+ _tag_seq = (
+ "p:cSld",
+ "p:clrMap",
+ "p:sldLayoutIdLst",
+ "p:transition",
+ "p:timing",
+ "p:hf",
+ "p:txStyles",
+ "p:extLst",
+ )
+ cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
+ sldLayoutIdLst: CT_SlideLayoutIdList = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "p:sldLayoutIdLst", successors=_tag_seq[3:]
+ )
+ del _tag_seq
+
+
+class CT_SlideTiming(BaseOxmlElement):
+ """`p:timing` element, specifying animations and timed behaviors."""
+
+ _tag_seq = ("p:tnLst", "p:bldLst", "p:extLst")
+ tnLst = ZeroOrOne("p:tnLst", successors=_tag_seq[1:])
+ del _tag_seq
+
+
+class CT_TimeNodeList(BaseOxmlElement):
+ """`p:tnLst` or `p:childTnList` element."""
+
+ def add_video(self, shape_id):
+ """Add a new `p:video` child element for movie having *shape_id*."""
+ video_xml = (
+ "<p:video %s>\n"
+ ' <p:cMediaNode vol="80000">\n'
+ ' <p:cTn id="%d" fill="hold" display="0">\n'
+ " <p:stCondLst>\n"
+ ' <p:cond delay="indefinite"/>\n'
+ " </p:stCondLst>\n"
+ " </p:cTn>\n"
+ " <p:tgtEl>\n"
+ ' <p:spTgt spid="%d"/>\n'
+ " </p:tgtEl>\n"
+ " </p:cMediaNode>\n"
+ "</p:video>\n" % (nsdecls("p"), self._next_cTn_id, shape_id)
+ )
+ video = parse_xml(video_xml)
+ self.append(video)
+
+ @property
+ def _next_cTn_id(self):
+ """Return the next available unique ID (int) for p:cTn element."""
+ cTn_id_strs = self.xpath("/p:sld/p:timing//p:cTn/@id")
+ ids = [int(id_str) for id_str in cTn_id_strs]
+ return max(ids) + 1
+
+
+class CT_TLMediaNodeVideo(BaseOxmlElement):
+ """`p:video` element, specifying video media details."""
+
+ _tag_seq = ("p:cMediaNode",)
+ cMediaNode = OneAndOnlyOne("p:cMediaNode")
+ del _tag_seq
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/table.py b/.venv/lib/python3.12/site-packages/pptx/oxml/table.py
new file mode 100644
index 00000000..cd3e9ebc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/table.py
@@ -0,0 +1,588 @@
+"""Custom element classes for table-related XML elements"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable, Iterator, cast
+
+from pptx.enum.text import MSO_VERTICAL_ANCHOR
+from pptx.oxml import parse_xml
+from pptx.oxml.dml.fill import CT_GradientFillProperties
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.simpletypes import ST_Coordinate, ST_Coordinate32, XsdBoolean, XsdInt
+from pptx.oxml.text import CT_TextBody
+from pptx.oxml.xmlchemy import (
+ BaseOxmlElement,
+ Choice,
+ OneAndOnlyOne,
+ OptionalAttribute,
+ RequiredAttribute,
+ ZeroOrMore,
+ ZeroOrOne,
+ ZeroOrOneChoice,
+)
+from pptx.util import Emu, lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.util import Length
+
+
+class CT_Table(BaseOxmlElement):
+ """`a:tbl` custom element class"""
+
+ get_or_add_tblPr: Callable[[], CT_TableProperties]
+ tr_lst: list[CT_TableRow]
+ _add_tr: Callable[..., CT_TableRow]
+
+ _tag_seq = ("a:tblPr", "a:tblGrid", "a:tr")
+ tblPr: CT_TableProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:tblPr", successors=_tag_seq[1:]
+ )
+ tblGrid: CT_TableGrid = OneAndOnlyOne("a:tblGrid") # pyright: ignore[reportAssignmentType]
+ tr = ZeroOrMore("a:tr", successors=_tag_seq[3:])
+ del _tag_seq
+
+ def add_tr(self, height: Length) -> CT_TableRow:
+ """Return a newly created `a:tr` child element having its `h` attribute set to `height`."""
+ return self._add_tr(h=height)
+
+ @property
+ def bandCol(self) -> bool:
+ return self._get_boolean_property("bandCol")
+
+ @bandCol.setter
+ def bandCol(self, value: bool):
+ self._set_boolean_property("bandCol", value)
+
+ @property
+ def bandRow(self) -> bool:
+ return self._get_boolean_property("bandRow")
+
+ @bandRow.setter
+ def bandRow(self, value: bool):
+ self._set_boolean_property("bandRow", value)
+
+ @property
+ def firstCol(self) -> bool:
+ return self._get_boolean_property("firstCol")
+
+ @firstCol.setter
+ def firstCol(self, value: bool):
+ self._set_boolean_property("firstCol", value)
+
+ @property
+ def firstRow(self) -> bool:
+ return self._get_boolean_property("firstRow")
+
+ @firstRow.setter
+ def firstRow(self, value: bool):
+ self._set_boolean_property("firstRow", value)
+
+ def iter_tcs(self) -> Iterator[CT_TableCell]:
+ """Generate each `a:tc` element in this tbl.
+
+ `a:tc` elements are generated left-to-right, top-to-bottom.
+ """
+ return (tc for tr in self.tr_lst for tc in tr.tc_lst)
+
+ @property
+ def lastCol(self) -> bool:
+ return self._get_boolean_property("lastCol")
+
+ @lastCol.setter
+ def lastCol(self, value: bool):
+ self._set_boolean_property("lastCol", value)
+
+ @property
+ def lastRow(self) -> bool:
+ return self._get_boolean_property("lastRow")
+
+ @lastRow.setter
+ def lastRow(self, value: bool):
+ self._set_boolean_property("lastRow", value)
+
+ @classmethod
+ def new_tbl(
+ cls, rows: int, cols: int, width: int, height: int, tableStyleId: str | None = None
+ ) -> CT_Table:
+ """Return a new `p:tbl` element tree."""
+ # working hypothesis is this is the default table style GUID
+ if tableStyleId is None:
+ tableStyleId = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"
+
+ xml = cls._tbl_tmpl() % (tableStyleId)
+ tbl = cast(CT_Table, parse_xml(xml))
+
+ # add specified number of rows and columns
+ rowheight = height // rows
+ colwidth = width // cols
+
+ for col in range(cols):
+ # adjust width of last col to absorb any div error
+ if col == cols - 1:
+ colwidth = width - ((cols - 1) * colwidth)
+ tbl.tblGrid.add_gridCol(width=Emu(colwidth))
+
+ for row in range(rows):
+ # adjust height of last row to absorb any div error
+ if row == rows - 1:
+ rowheight = height - ((rows - 1) * rowheight)
+ tr = tbl.add_tr(height=Emu(rowheight))
+ for col in range(cols):
+ tr.add_tc()
+
+ return tbl
+
+ def tc(self, row_idx: int, col_idx: int) -> CT_TableCell:
+ """Return `a:tc` element at `row_idx`, `col_idx`."""
+ return self.tr_lst[row_idx].tc_lst[col_idx]
+
+ def _get_boolean_property(self, propname: str) -> bool:
+ """Generalized getter for the boolean properties on the `a:tblPr` child element.
+
+ Defaults to False if `propname` attribute is missing or `a:tblPr` element itself is not
+ present.
+ """
+ tblPr = self.tblPr
+ if tblPr is None:
+ return False
+ propval = getattr(tblPr, propname)
+ return {True: True, False: False, None: False}[propval]
+
+ def _set_boolean_property(self, propname: str, value: bool) -> None:
+ """Generalized setter for boolean properties on the `a:tblPr` child element.
+
+ Sets `propname` attribute appropriately based on `value`. If `value` is True, the
+ attribute is set to "1"; a tblPr child element is added if necessary. If `value` is False,
+ the `propname` attribute is removed if present, allowing its default value of False to be
+ its effective value.
+ """
+ if value not in (True, False):
+ raise ValueError("assigned value must be either True or False, got %s" % value)
+ tblPr = self.get_or_add_tblPr()
+ setattr(tblPr, propname, value)
+
+ @classmethod
+ def _tbl_tmpl(cls):
+ return (
+ "<a:tbl %s>\n"
+ ' <a:tblPr firstRow="1" bandRow="1">\n'
+ " <a:tableStyleId>%s</a:tableStyleId>\n"
+ " </a:tblPr>\n"
+ " <a:tblGrid/>\n"
+ "</a:tbl>" % (nsdecls("a"), "%s")
+ )
+
+
+class CT_TableCell(BaseOxmlElement):
+ """`a:tc` custom element class"""
+
+ get_or_add_tcPr: Callable[[], CT_TableCellProperties]
+ get_or_add_txBody: Callable[[], CT_TextBody]
+
+ _tag_seq = ("a:txBody", "a:tcPr", "a:extLst")
+ txBody: CT_TextBody | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:txBody", successors=_tag_seq[1:]
+ )
+ tcPr: CT_TableCellProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:tcPr", successors=_tag_seq[2:]
+ )
+ del _tag_seq
+
+ gridSpan: int = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "gridSpan", XsdInt, default=1
+ )
+ rowSpan: int = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "rowSpan", XsdInt, default=1
+ )
+ hMerge: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "hMerge", XsdBoolean, default=False
+ )
+ vMerge: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "vMerge", XsdBoolean, default=False
+ )
+
+ @property
+ def anchor(self) -> MSO_VERTICAL_ANCHOR | None:
+ """String held in `anchor` attribute of `a:tcPr` child element of this `a:tc` element."""
+ if self.tcPr is None:
+ return None
+ return self.tcPr.anchor
+
+ @anchor.setter
+ def anchor(self, anchor_enum_idx: MSO_VERTICAL_ANCHOR | None):
+ """Set value of anchor attribute on `a:tcPr` child element."""
+ if anchor_enum_idx is None and self.tcPr is None:
+ return
+ tcPr = self.get_or_add_tcPr()
+ tcPr.anchor = anchor_enum_idx
+
+ def append_ps_from(self, spanned_tc: CT_TableCell):
+ """Append `a:p` elements taken from `spanned_tc`.
+
+ Any non-empty paragraph elements in `spanned_tc` are removed and appended to the
+ text-frame of this cell. If `spanned_tc` is left with no content after this process, a
+ single empty `a:p` element is added to ensure the cell is compliant with the spec.
+ """
+ source_txBody = spanned_tc.get_or_add_txBody()
+ target_txBody = self.get_or_add_txBody()
+
+ # ---if source is empty, there's nothing to do---
+ if source_txBody.is_empty:
+ return
+
+ # ---a single empty paragraph in target is overwritten---
+ if target_txBody.is_empty:
+ target_txBody.clear_content()
+
+ for p in source_txBody.p_lst:
+ target_txBody.append(p)
+
+ # ---neither source nor target can be left without ps---
+ source_txBody.unclear_content()
+ target_txBody.unclear_content()
+
+ @property
+ def col_idx(self) -> int:
+ """Offset of this cell's column in its table."""
+ # ---tc elements come before any others in `a:tr` element---
+ return cast(CT_TableRow, self.getparent()).index(self)
+
+ @property
+ def is_merge_origin(self) -> bool:
+ """True if cell is top-left in merged cell range."""
+ if self.gridSpan > 1 and not self.vMerge:
+ return True
+ return self.rowSpan > 1 and not self.hMerge
+
+ @property
+ def is_spanned(self) -> bool:
+ """True if cell is in merged cell range but not merge origin cell."""
+ return self.hMerge or self.vMerge
+
+ @property
+ def marT(self) -> Length:
+ """Top margin for this cell.
+
+ This value is stored in the `marT` attribute of the `a:tcPr` child element of this `a:tc`.
+
+ Read/write. If the attribute is not present, the default value `45720` (0.05 inches) is
+ returned for top and bottom; `91440` (0.10 inches) is the default for left and right.
+ Assigning |None| to any `marX` property clears that attribute from the element,
+ effectively setting it to the default value.
+ """
+ return self._get_marX("marT", Emu(45720))
+
+ @marT.setter
+ def marT(self, value: Length | None):
+ self._set_marX("marT", value)
+
+ @property
+ def marR(self) -> Length:
+ """Right margin value represented in `marR` attribute."""
+ return self._get_marX("marR", Emu(91440))
+
+ @marR.setter
+ def marR(self, value: Length | None):
+ self._set_marX("marR", value)
+
+ @property
+ def marB(self) -> Length:
+ """Bottom margin value represented in `marB` attribute."""
+ return self._get_marX("marB", Emu(45720))
+
+ @marB.setter
+ def marB(self, value: Length | None):
+ self._set_marX("marB", value)
+
+ @property
+ def marL(self) -> Length:
+ """Left margin value represented in `marL` attribute."""
+ return self._get_marX("marL", Emu(91440))
+
+ @marL.setter
+ def marL(self, value: Length | None):
+ self._set_marX("marL", value)
+
+ @classmethod
+ def new(cls) -> CT_TableCell:
+ """Return a new `a:tc` element subtree."""
+ return cast(
+ CT_TableCell,
+ parse_xml(
+ f"<a:tc {nsdecls('a')}>\n"
+ f" <a:txBody>\n"
+ f" <a:bodyPr/>\n"
+ f" <a:lstStyle/>\n"
+ f" <a:p/>\n"
+ f" </a:txBody>\n"
+ f" <a:tcPr/>\n"
+ f"</a:tc>"
+ ),
+ )
+
+ @property
+ def row_idx(self) -> int:
+ """Offset of this cell's row in its table."""
+ return cast(CT_TableRow, self.getparent()).row_idx
+
+ @property
+ def tbl(self) -> CT_Table:
+ """Table element this cell belongs to."""
+ return cast(CT_Table, self.xpath("ancestor::a:tbl")[0])
+
+ @property
+ def text(self) -> str: # pyright: ignore[reportIncompatibleMethodOverride]
+ """str text contained in cell"""
+ # ---note this shadows lxml _Element.text---
+ txBody = self.txBody
+ if txBody is None:
+ return ""
+ return "\n".join([p.text for p in txBody.p_lst])
+
+ def _get_marX(self, attr_name: str, default: Length) -> Length:
+ """Generalized method to get margin values."""
+ if self.tcPr is None:
+ return Emu(default)
+ return Emu(int(self.tcPr.get(attr_name, default)))
+
+ def _new_txBody(self) -> CT_TextBody:
+ return CT_TextBody.new_a_txBody()
+
+ def _set_marX(self, marX: str, value: Length | None) -> None:
+ """Set value of marX attribute on `a:tcPr` child element.
+
+ If `marX` is |None|, the marX attribute is removed. `marX` is a string, one of `('marL',
+ 'marR', 'marT', 'marB')`.
+ """
+ if value is None and self.tcPr is None:
+ return
+ tcPr = self.get_or_add_tcPr()
+ setattr(tcPr, marX, value)
+
+
+class CT_TableCellProperties(BaseOxmlElement):
+ """`a:tcPr` custom element class"""
+
+ eg_fillProperties = ZeroOrOneChoice(
+ (
+ Choice("a:noFill"),
+ Choice("a:solidFill"),
+ Choice("a:gradFill"),
+ Choice("a:blipFill"),
+ Choice("a:pattFill"),
+ Choice("a:grpFill"),
+ ),
+ successors=("a:headers", "a:extLst"),
+ )
+ anchor: MSO_VERTICAL_ANCHOR | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "anchor", MSO_VERTICAL_ANCHOR
+ )
+ marL: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "marL", ST_Coordinate32
+ )
+ marR: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "marR", ST_Coordinate32
+ )
+ marT: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "marT", ST_Coordinate32
+ )
+ marB: Length | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "marB", ST_Coordinate32
+ )
+
+ def _new_gradFill(self):
+ return CT_GradientFillProperties.new_gradFill()
+
+
+class CT_TableCol(BaseOxmlElement):
+ """`a:gridCol` custom element class."""
+
+ w: Length = RequiredAttribute("w", ST_Coordinate) # pyright: ignore[reportAssignmentType]
+
+
+class CT_TableGrid(BaseOxmlElement):
+ """`a:tblGrid` custom element class."""
+
+ gridCol_lst: list[CT_TableCol]
+ _add_gridCol: Callable[..., CT_TableCol]
+
+ gridCol = ZeroOrMore("a:gridCol")
+
+ def add_gridCol(self, width: Length) -> CT_TableCol:
+ """A newly appended `a:gridCol` child element having its `w` attribute set to `width`."""
+ return self._add_gridCol(w=width)
+
+
+class CT_TableProperties(BaseOxmlElement):
+ """`a:tblPr` custom element class."""
+
+ bandRow = OptionalAttribute("bandRow", XsdBoolean, default=False)
+ bandCol = OptionalAttribute("bandCol", XsdBoolean, default=False)
+ firstRow = OptionalAttribute("firstRow", XsdBoolean, default=False)
+ firstCol = OptionalAttribute("firstCol", XsdBoolean, default=False)
+ lastRow = OptionalAttribute("lastRow", XsdBoolean, default=False)
+ lastCol = OptionalAttribute("lastCol", XsdBoolean, default=False)
+
+
+class CT_TableRow(BaseOxmlElement):
+ """`a:tr` custom element class."""
+
+ tc_lst: list[CT_TableCell]
+ _add_tc: Callable[[], CT_TableCell]
+
+ tc = ZeroOrMore("a:tc", successors=("a:extLst",))
+ h: Length = RequiredAttribute("h", ST_Coordinate) # pyright: ignore[reportAssignmentType]
+
+ def add_tc(self) -> CT_TableCell:
+ """A newly added minimal valid `a:tc` child element."""
+ return self._add_tc()
+
+ @property
+ def row_idx(self) -> int:
+ """Offset of this row in its table."""
+ return cast(CT_Table, self.getparent()).tr_lst.index(self)
+
+ def _new_tc(self):
+ return CT_TableCell.new()
+
+
+class TcRange(object):
+ """A 2D block of `a:tc` cell elements in a table.
+
+ This object assumes the structure of the underlying table does not change during its lifetime.
+ Structural changes in this context would be insertion or removal of rows or columns.
+
+ The client is expected to create, use, and then abandon an instance in the context of a single
+ user operation that is known to have no structural side-effects of this type.
+ """
+
+ def __init__(self, tc: CT_TableCell, other_tc: CT_TableCell):
+ self._tc = tc
+ self._other_tc = other_tc
+
+ @classmethod
+ def from_merge_origin(cls, tc: CT_TableCell):
+ """Return instance created from merge-origin tc element."""
+ other_tc = tc.tbl.tc(
+ tc.row_idx + tc.rowSpan - 1, # ---other_row_idx
+ tc.col_idx + tc.gridSpan - 1, # ---other_col_idx
+ )
+ return cls(tc, other_tc)
+
+ @lazyproperty
+ def contains_merged_cell(self) -> bool:
+ """True if one or more cells in range are part of a merged cell."""
+ for tc in self.iter_tcs():
+ if tc.gridSpan > 1:
+ return True
+ if tc.rowSpan > 1:
+ return True
+ if tc.hMerge:
+ return True
+ if tc.vMerge:
+ return True
+ return False
+
+ @lazyproperty
+ def dimensions(self) -> tuple[int, int]:
+ """(row_count, col_count) pair describing size of range."""
+ _, _, width, height = self._extents
+ return height, width
+
+ @lazyproperty
+ def in_same_table(self):
+ """True if both cells provided to constructor are in same table."""
+ if self._tc.tbl is self._other_tc.tbl:
+ return True
+ return False
+
+ def iter_except_left_col_tcs(self):
+ """Generate each `a:tc` element not in leftmost column of range."""
+ for tr in self._tbl.tr_lst[self._top : self._bottom]:
+ for tc in tr.tc_lst[self._left + 1 : self._right]:
+ yield tc
+
+ def iter_except_top_row_tcs(self):
+ """Generate each `a:tc` element in non-first rows of range."""
+ for tr in self._tbl.tr_lst[self._top + 1 : self._bottom]:
+ for tc in tr.tc_lst[self._left : self._right]:
+ yield tc
+
+ def iter_left_col_tcs(self):
+ """Generate each `a:tc` element in leftmost column of range."""
+ col_idx = self._left
+ for tr in self._tbl.tr_lst[self._top : self._bottom]:
+ yield tr.tc_lst[col_idx]
+
+ def iter_tcs(self):
+ """Generate each `a:tc` element in this range.
+
+ Cell elements are generated left-to-right, top-to-bottom.
+ """
+ return (
+ tc
+ for tr in self._tbl.tr_lst[self._top : self._bottom]
+ for tc in tr.tc_lst[self._left : self._right]
+ )
+
+ def iter_top_row_tcs(self):
+ """Generate each `a:tc` element in topmost row of range."""
+ tr = self._tbl.tr_lst[self._top]
+ for tc in tr.tc_lst[self._left : self._right]:
+ yield tc
+
+ def move_content_to_origin(self):
+ """Move all paragraphs in range to origin cell."""
+ tcs = list(self.iter_tcs())
+ origin_tc = tcs[0]
+ for spanned_tc in tcs[1:]:
+ origin_tc.append_ps_from(spanned_tc)
+
+ @lazyproperty
+ def _bottom(self):
+ """Index of row following last row of range"""
+ _, top, _, height = self._extents
+ return top + height
+
+ @lazyproperty
+ def _extents(self) -> tuple[int, int, int, int]:
+ """A (left, top, width, height) tuple describing range extents.
+
+ Note this is normalized to accommodate the various orderings of the corner cells provided
+ on construction, which may be in any of four configurations such as (top-left,
+ bottom-right), (bottom-left, top-right), etc.
+ """
+
+ def start_and_size(idx: int, other_idx: int) -> tuple[int, int]:
+ """Return beginning and length of range based on two indexes."""
+ return min(idx, other_idx), abs(idx - other_idx) + 1
+
+ tc, other_tc = self._tc, self._other_tc
+
+ left, width = start_and_size(tc.col_idx, other_tc.col_idx)
+ top, height = start_and_size(tc.row_idx, other_tc.row_idx)
+
+ return left, top, width, height
+
+ @lazyproperty
+ def _left(self):
+ """Index of leftmost column in range."""
+ left, _, _, _ = self._extents
+ return left
+
+ @lazyproperty
+ def _right(self):
+ """Index of column following the last column in range."""
+ left, _, width, _ = self._extents
+ return left + width
+
+ @lazyproperty
+ def _tbl(self):
+ """`a:tbl` element containing this cell range."""
+ return self._tc.tbl
+
+ @lazyproperty
+ def _top(self):
+ """Index of topmost row in range."""
+ _, top, _, _ = self._extents
+ return top
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/text.py b/.venv/lib/python3.12/site-packages/pptx/oxml/text.py
new file mode 100644
index 00000000..0f9ecc15
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/text.py
@@ -0,0 +1,618 @@
+"""Custom element classes for text-related XML elements"""
+
+from __future__ import annotations
+
+import re
+from typing import TYPE_CHECKING, Callable, cast
+
+from pptx.enum.lang import MSO_LANGUAGE_ID
+from pptx.enum.text import (
+ MSO_AUTO_SIZE,
+ MSO_TEXT_UNDERLINE_TYPE,
+ MSO_VERTICAL_ANCHOR,
+ PP_PARAGRAPH_ALIGNMENT,
+)
+from pptx.exc import InvalidXmlError
+from pptx.oxml import parse_xml
+from pptx.oxml.dml.fill import CT_GradientFillProperties
+from pptx.oxml.ns import nsdecls
+from pptx.oxml.simpletypes import (
+ ST_Coordinate32,
+ ST_TextFontScalePercentOrPercentString,
+ ST_TextFontSize,
+ ST_TextIndentLevelType,
+ ST_TextSpacingPercentOrPercentString,
+ ST_TextSpacingPoint,
+ ST_TextTypeface,
+ ST_TextWrappingType,
+ XsdBoolean,
+)
+from pptx.oxml.xmlchemy import (
+ BaseOxmlElement,
+ Choice,
+ OneAndOnlyOne,
+ OneOrMore,
+ OptionalAttribute,
+ RequiredAttribute,
+ ZeroOrMore,
+ ZeroOrOne,
+ ZeroOrOneChoice,
+)
+from pptx.util import Emu, Length
+
+if TYPE_CHECKING:
+ from pptx.oxml.action import CT_Hyperlink
+
+
+class CT_RegularTextRun(BaseOxmlElement):
+ """`a:r` custom element class"""
+
+ get_or_add_rPr: Callable[[], CT_TextCharacterProperties]
+
+ rPr: CT_TextCharacterProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:rPr", successors=("a:t",)
+ )
+ t: BaseOxmlElement = OneAndOnlyOne("a:t") # pyright: ignore[reportAssignmentType]
+
+ @property
+ def text(self) -> str:
+ """All text of (required) `a:t` child."""
+ text = self.t.text
+ # -- t.text is None when t element is empty, e.g. '<a:t/>' --
+ return text or ""
+
+ @text.setter
+ def text(self, value: str): # pyright: ignore[reportIncompatibleMethodOverride]
+ self.t.text = self._escape_ctrl_chars(value)
+
+ @staticmethod
+ def _escape_ctrl_chars(s: str) -> str:
+ """Return str after replacing each control character with a plain-text escape.
+
+ For example, a BEL character (x07) would appear as "_x0007_". Horizontal-tab
+ (x09) and line-feed (x0A) are not escaped. All other characters in the range
+ x00-x1F are escaped.
+ """
+ return re.sub(r"([\x00-\x08\x0B-\x1F])", lambda match: "_x%04X_" % ord(match.group(1)), s)
+
+
+class CT_TextBody(BaseOxmlElement):
+ """`p:txBody` custom element class.
+
+ Also used for `c:txPr` in charts and perhaps other elements.
+ """
+
+ add_p: Callable[[], CT_TextParagraph]
+ p_lst: list[CT_TextParagraph]
+
+ bodyPr: CT_TextBodyProperties = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
+ "a:bodyPr"
+ )
+ p: CT_TextParagraph = OneOrMore("a:p") # pyright: ignore[reportAssignmentType]
+
+ def clear_content(self):
+ """Remove all `a:p` children, but leave any others.
+
+ cf. lxml `_Element.clear()` method which removes all children.
+ """
+ for p in self.p_lst:
+ self.remove(p)
+
+ @property
+ def defRPr(self) -> CT_TextCharacterProperties:
+ """`a:defRPr` element of required first `p` child, added with its ancestors if not present.
+
+ Used when element is a ``c:txPr`` in a chart and the `p` element is used only to specify
+ formatting, not content.
+ """
+ p = self.p_lst[0]
+ pPr = p.get_or_add_pPr()
+ defRPr = pPr.get_or_add_defRPr()
+ return defRPr
+
+ @property
+ def is_empty(self) -> bool:
+ """True if only a single empty `a:p` element is present."""
+ ps = self.p_lst
+ if len(ps) > 1:
+ return False
+
+ if not ps:
+ raise InvalidXmlError("p:txBody must have at least one a:p")
+
+ if ps[0].text != "":
+ return False
+ return True
+
+ @classmethod
+ def new(cls):
+ """Return a new `p:txBody` element tree."""
+ xml = cls._txBody_tmpl()
+ txBody = parse_xml(xml)
+ return txBody
+
+ @classmethod
+ def new_a_txBody(cls) -> CT_TextBody:
+ """Return a new `a:txBody` element tree.
+
+ Suitable for use in a table cell and possibly other situations.
+ """
+ xml = cls._a_txBody_tmpl()
+ txBody = cast(CT_TextBody, parse_xml(xml))
+ return txBody
+
+ @classmethod
+ def new_p_txBody(cls):
+ """Return a new `p:txBody` element tree, suitable for use in an `p:sp` element."""
+ xml = cls._p_txBody_tmpl()
+ return parse_xml(xml)
+
+ @classmethod
+ def new_txPr(cls):
+ """Return a `c:txPr` element tree.
+
+ Suitable for use in a chart object like data labels or tick labels.
+ """
+ xml = (
+ "<c:txPr %s>\n"
+ " <a:bodyPr/>\n"
+ " <a:lstStyle/>\n"
+ " <a:p>\n"
+ " <a:pPr>\n"
+ " <a:defRPr/>\n"
+ " </a:pPr>\n"
+ " </a:p>\n"
+ "</c:txPr>\n"
+ ) % nsdecls("c", "a")
+ txPr = parse_xml(xml)
+ return txPr
+
+ def unclear_content(self):
+ """Ensure p:txBody has at least one a:p child.
+
+ Intuitively, reverse a ".clear_content()" operation to minimum conformance with spec
+ (single empty paragraph).
+ """
+ if len(self.p_lst) > 0:
+ return
+ self.add_p()
+
+ @classmethod
+ def _a_txBody_tmpl(cls):
+ return "<a:txBody %s>\n" " <a:bodyPr/>\n" " <a:p/>\n" "</a:txBody>\n" % (nsdecls("a"))
+
+ @classmethod
+ def _p_txBody_tmpl(cls):
+ return (
+ "<p:txBody %s>\n" " <a:bodyPr/>\n" " <a:p/>\n" "</p:txBody>\n" % (nsdecls("p", "a"))
+ )
+
+ @classmethod
+ def _txBody_tmpl(cls):
+ return (
+ "<p:txBody %s>\n"
+ " <a:bodyPr/>\n"
+ " <a:lstStyle/>\n"
+ " <a:p/>\n"
+ "</p:txBody>\n" % (nsdecls("a", "p"))
+ )
+
+
+class CT_TextBodyProperties(BaseOxmlElement):
+ """`a:bodyPr` custom element class."""
+
+ _add_noAutofit: Callable[[], BaseOxmlElement]
+ _add_normAutofit: Callable[[], CT_TextNormalAutofit]
+ _add_spAutoFit: Callable[[], BaseOxmlElement]
+ _remove_eg_textAutoFit: Callable[[], None]
+
+ noAutofit: BaseOxmlElement | None
+ normAutofit: CT_TextNormalAutofit | None
+ spAutoFit: BaseOxmlElement | None
+
+ eg_textAutoFit = ZeroOrOneChoice(
+ (Choice("a:noAutofit"), Choice("a:normAutofit"), Choice("a:spAutoFit")),
+ successors=("a:scene3d", "a:sp3d", "a:flatTx", "a:extLst"),
+ )
+ lIns: Length = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "lIns", ST_Coordinate32, default=Emu(91440)
+ )
+ tIns: Length = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "tIns", ST_Coordinate32, default=Emu(45720)
+ )
+ rIns: Length = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "rIns", ST_Coordinate32, default=Emu(91440)
+ )
+ bIns: Length = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "bIns", ST_Coordinate32, default=Emu(45720)
+ )
+ anchor: MSO_VERTICAL_ANCHOR | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "anchor", MSO_VERTICAL_ANCHOR
+ )
+ wrap: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "wrap", ST_TextWrappingType
+ )
+
+ @property
+ def autofit(self):
+ """The autofit setting for the text frame, a member of the `MSO_AUTO_SIZE` enumeration."""
+ if self.noAutofit is not None:
+ return MSO_AUTO_SIZE.NONE
+ if self.normAutofit is not None:
+ return MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
+ if self.spAutoFit is not None:
+ return MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT
+ return None
+
+ @autofit.setter
+ def autofit(self, value: MSO_AUTO_SIZE | None):
+ if value is not None and value not in MSO_AUTO_SIZE:
+ raise ValueError(
+ f"only None or a member of the MSO_AUTO_SIZE enumeration can be assigned to"
+ f" CT_TextBodyProperties.autofit, got {value}"
+ )
+ self._remove_eg_textAutoFit()
+ if value == MSO_AUTO_SIZE.NONE:
+ self._add_noAutofit()
+ elif value == MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE:
+ self._add_normAutofit()
+ elif value == MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT:
+ self._add_spAutoFit()
+
+
+class CT_TextCharacterProperties(BaseOxmlElement):
+ """Custom element class for `a:rPr`, `a:defRPr`, and `a:endParaRPr`.
+
+ 'rPr' is short for 'run properties', and it corresponds to the |Font| proxy class.
+ """
+
+ get_or_add_hlinkClick: Callable[[], CT_Hyperlink]
+ get_or_add_latin: Callable[[], CT_TextFont]
+ _remove_latin: Callable[[], None]
+ _remove_hlinkClick: Callable[[], None]
+
+ eg_fillProperties = ZeroOrOneChoice(
+ (
+ Choice("a:noFill"),
+ Choice("a:solidFill"),
+ Choice("a:gradFill"),
+ Choice("a:blipFill"),
+ Choice("a:pattFill"),
+ Choice("a:grpFill"),
+ ),
+ successors=(
+ "a:effectLst",
+ "a:effectDag",
+ "a:highlight",
+ "a:uLnTx",
+ "a:uLn",
+ "a:uFillTx",
+ "a:uFill",
+ "a:latin",
+ "a:ea",
+ "a:cs",
+ "a:sym",
+ "a:hlinkClick",
+ "a:hlinkMouseOver",
+ "a:rtl",
+ "a:extLst",
+ ),
+ )
+ latin: CT_TextFont | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:latin",
+ successors=(
+ "a:ea",
+ "a:cs",
+ "a:sym",
+ "a:hlinkClick",
+ "a:hlinkMouseOver",
+ "a:rtl",
+ "a:extLst",
+ ),
+ )
+ hlinkClick: CT_Hyperlink | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:hlinkClick", successors=("a:hlinkMouseOver", "a:rtl", "a:extLst")
+ )
+
+ lang: MSO_LANGUAGE_ID | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "lang", MSO_LANGUAGE_ID
+ )
+ sz: int | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "sz", ST_TextFontSize
+ )
+ b: bool | None = OptionalAttribute("b", XsdBoolean) # pyright: ignore[reportAssignmentType]
+ i: bool | None = OptionalAttribute("i", XsdBoolean) # pyright: ignore[reportAssignmentType]
+ u: MSO_TEXT_UNDERLINE_TYPE | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "u", MSO_TEXT_UNDERLINE_TYPE
+ )
+
+ def _new_gradFill(self):
+ return CT_GradientFillProperties.new_gradFill()
+
+ def add_hlinkClick(self, rId: str) -> CT_Hyperlink:
+ """Add an `a:hlinkClick` child element with r:id attribute set to `rId`."""
+ hlinkClick = self.get_or_add_hlinkClick()
+ hlinkClick.rId = rId
+ return hlinkClick
+
+
+class CT_TextField(BaseOxmlElement):
+ """`a:fld` field element, for either a slide number or date field."""
+
+ get_or_add_rPr: Callable[[], CT_TextCharacterProperties]
+
+ rPr: CT_TextCharacterProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:rPr", successors=("a:pPr", "a:t")
+ )
+ t: BaseOxmlElement | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:t", successors=()
+ )
+
+ @property
+ def text(self) -> str: # pyright: ignore[reportIncompatibleMethodOverride]
+ """The text of the `a:t` child element."""
+ t = self.t
+ if t is None:
+ return ""
+ return t.text or ""
+
+
+class CT_TextFont(BaseOxmlElement):
+ """Custom element class for `a:latin`, `a:ea`, `a:cs`, and `a:sym`.
+
+ These occur as child elements of CT_TextCharacterProperties, e.g. `a:rPr`.
+ """
+
+ typeface: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
+ "typeface", ST_TextTypeface
+ )
+
+
+class CT_TextLineBreak(BaseOxmlElement):
+ """`a:br` line break element"""
+
+ get_or_add_rPr: Callable[[], CT_TextCharacterProperties]
+
+ rPr = ZeroOrOne("a:rPr", successors=())
+
+ @property
+ def text(self): # pyright: ignore[reportIncompatibleMethodOverride]
+ """Unconditionally a single vertical-tab character.
+
+ A line break element can contain no text other than the implicit line feed it
+ represents.
+ """
+ return "\v"
+
+
+class CT_TextNormalAutofit(BaseOxmlElement):
+ """`a:normAutofit` element specifying fit text to shape font reduction, etc."""
+
+ fontScale = OptionalAttribute(
+ "fontScale", ST_TextFontScalePercentOrPercentString, default=100.0
+ )
+
+
+class CT_TextParagraph(BaseOxmlElement):
+ """`a:p` custom element class"""
+
+ get_or_add_endParaRPr: Callable[[], CT_TextCharacterProperties]
+ get_or_add_pPr: Callable[[], CT_TextParagraphProperties]
+ r_lst: list[CT_RegularTextRun]
+ _add_br: Callable[[], CT_TextLineBreak]
+ _add_r: Callable[[], CT_RegularTextRun]
+
+ pPr: CT_TextParagraphProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:pPr", successors=("a:r", "a:br", "a:fld", "a:endParaRPr")
+ )
+ r = ZeroOrMore("a:r", successors=("a:endParaRPr",))
+ br = ZeroOrMore("a:br", successors=("a:endParaRPr",))
+ endParaRPr: CT_TextCharacterProperties | None = ZeroOrOne(
+ "a:endParaRPr", successors=()
+ ) # pyright: ignore[reportAssignmentType]
+
+ def add_br(self) -> CT_TextLineBreak:
+ """Return a newly appended `a:br` element."""
+ return self._add_br()
+
+ def add_r(self, text: str | None = None) -> CT_RegularTextRun:
+ """Return a newly appended `a:r` element."""
+ r = self._add_r()
+ if text:
+ r.text = text
+ return r
+
+ def append_text(self, text: str):
+ """Append `a:r` and `a:br` elements to `p` based on `text`.
+
+ Any `\n` or `\v` (vertical-tab) characters in `text` delimit `a:r` (run) elements and
+ themselves are translated to `a:br` (line-break) elements. The vertical-tab character
+ appears in clipboard text from PowerPoint at "soft" line-breaks (new-line, but not new
+ paragraph).
+ """
+ for idx, r_str in enumerate(re.split("\n|\v", text)):
+ # ---breaks are only added _between_ items, not at start---
+ if idx > 0:
+ self.add_br()
+ # ---runs that would be empty are not added---
+ if r_str:
+ self.add_r(r_str)
+
+ @property
+ def content_children(self) -> tuple[CT_RegularTextRun | CT_TextLineBreak | CT_TextField, ...]:
+ """Sequence containing text-container child elements of this `a:p` element.
+
+ These include `a:r`, `a:br`, and `a:fld`.
+ """
+ return tuple(
+ e for e in self if isinstance(e, (CT_RegularTextRun, CT_TextLineBreak, CT_TextField))
+ )
+
+ @property
+ def text(self) -> str: # pyright: ignore[reportIncompatibleMethodOverride]
+ """str text contained in this paragraph."""
+ # ---note this shadows the lxml _Element.text---
+ return "".join([child.text for child in self.content_children])
+
+ def _new_r(self):
+ r_xml = "<a:r %s><a:t/></a:r>" % nsdecls("a")
+ return parse_xml(r_xml)
+
+
+class CT_TextParagraphProperties(BaseOxmlElement):
+ """`a:pPr` custom element class."""
+
+ get_or_add_defRPr: Callable[[], CT_TextCharacterProperties]
+ _add_lnSpc: Callable[[], CT_TextSpacing]
+ _add_spcAft: Callable[[], CT_TextSpacing]
+ _add_spcBef: Callable[[], CT_TextSpacing]
+ _remove_lnSpc: Callable[[], None]
+ _remove_spcAft: Callable[[], None]
+ _remove_spcBef: Callable[[], None]
+
+ _tag_seq = (
+ "a:lnSpc",
+ "a:spcBef",
+ "a:spcAft",
+ "a:buClrTx",
+ "a:buClr",
+ "a:buSzTx",
+ "a:buSzPct",
+ "a:buSzPts",
+ "a:buFontTx",
+ "a:buFont",
+ "a:buNone",
+ "a:buAutoNum",
+ "a:buChar",
+ "a:buBlip",
+ "a:tabLst",
+ "a:defRPr",
+ "a:extLst",
+ )
+ lnSpc: CT_TextSpacing | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:lnSpc", successors=_tag_seq[1:]
+ )
+ spcBef: CT_TextSpacing | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:spcBef", successors=_tag_seq[2:]
+ )
+ spcAft: CT_TextSpacing | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:spcAft", successors=_tag_seq[3:]
+ )
+ defRPr: CT_TextCharacterProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:defRPr", successors=_tag_seq[16:]
+ )
+ lvl: int = OptionalAttribute( # pyright: ignore[reportAssignmentType]
+ "lvl", ST_TextIndentLevelType, default=0
+ )
+ algn: PP_PARAGRAPH_ALIGNMENT | None = OptionalAttribute(
+ "algn", PP_PARAGRAPH_ALIGNMENT
+ ) # pyright: ignore[reportAssignmentType]
+ del _tag_seq
+
+ @property
+ def line_spacing(self) -> float | Length | None:
+ """The spacing between baselines of successive lines in this paragraph.
+
+ A float value indicates a number of lines. A |Length| value indicates a fixed spacing.
+ Value is contained in `./a:lnSpc/a:spcPts/@val` or `./a:lnSpc/a:spcPct/@val`. Value is
+ |None| if no element is present.
+ """
+ lnSpc = self.lnSpc
+ if lnSpc is None:
+ return None
+ if lnSpc.spcPts is not None:
+ return lnSpc.spcPts.val
+ return cast(CT_TextSpacingPercent, lnSpc.spcPct).val
+
+ @line_spacing.setter
+ def line_spacing(self, value: float | Length | None):
+ self._remove_lnSpc()
+ if value is None:
+ return
+ if isinstance(value, Length):
+ self._add_lnSpc().set_spcPts(value)
+ else:
+ self._add_lnSpc().set_spcPct(value)
+
+ @property
+ def space_after(self) -> Length | None:
+ """The EMU equivalent of the centipoints value in `./a:spcAft/a:spcPts/@val`."""
+ spcAft = self.spcAft
+ if spcAft is None:
+ return None
+ spcPts = spcAft.spcPts
+ if spcPts is None:
+ return None
+ return spcPts.val
+
+ @space_after.setter
+ def space_after(self, value: Length | None):
+ self._remove_spcAft()
+ if value is not None:
+ self._add_spcAft().set_spcPts(value)
+
+ @property
+ def space_before(self):
+ """The EMU equivalent of the centipoints value in `./a:spcBef/a:spcPts/@val`."""
+ spcBef = self.spcBef
+ if spcBef is None:
+ return None
+ spcPts = spcBef.spcPts
+ if spcPts is None:
+ return None
+ return spcPts.val
+
+ @space_before.setter
+ def space_before(self, value: Length | None):
+ self._remove_spcBef()
+ if value is not None:
+ self._add_spcBef().set_spcPts(value)
+
+
+class CT_TextSpacing(BaseOxmlElement):
+ """Used for `a:lnSpc`, `a:spcBef`, and `a:spcAft` elements."""
+
+ get_or_add_spcPct: Callable[[], CT_TextSpacingPercent]
+ get_or_add_spcPts: Callable[[], CT_TextSpacingPoint]
+ _remove_spcPct: Callable[[], None]
+ _remove_spcPts: Callable[[], None]
+
+ # this should actually be a OneAndOnlyOneChoice, but that's not
+ # implemented yet.
+ spcPct: CT_TextSpacingPercent | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:spcPct"
+ )
+ spcPts: CT_TextSpacingPoint | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
+ "a:spcPts"
+ )
+
+ def set_spcPct(self, value: float):
+ """Set spacing to `value` lines, e.g. 1.75 lines.
+
+ A ./a:spcPts child is removed if present.
+ """
+ self._remove_spcPts()
+ spcPct = self.get_or_add_spcPct()
+ spcPct.val = value
+
+ def set_spcPts(self, value: Length):
+ """Set spacing to `value` points. A ./a:spcPct child is removed if present."""
+ self._remove_spcPct()
+ spcPts = self.get_or_add_spcPts()
+ spcPts.val = value
+
+
+class CT_TextSpacingPercent(BaseOxmlElement):
+ """`a:spcPct` element, specifying spacing in thousandths of a percent in its `val` attribute."""
+
+ val: float = RequiredAttribute( # pyright: ignore[reportAssignmentType]
+ "val", ST_TextSpacingPercentOrPercentString
+ )
+
+
+class CT_TextSpacingPoint(BaseOxmlElement):
+ """`a:spcPts` element, specifying spacing in centipoints in its `val` attribute."""
+
+ val: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType]
+ "val", ST_TextSpacingPoint
+ )
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/theme.py b/.venv/lib/python3.12/site-packages/pptx/oxml/theme.py
new file mode 100644
index 00000000..19ac8dea
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/theme.py
@@ -0,0 +1,29 @@
+"""lxml custom element classes for theme-related XML elements."""
+
+from __future__ import annotations
+
+from . import parse_from_template
+from .xmlchemy import BaseOxmlElement
+
+
+class CT_OfficeStyleSheet(BaseOxmlElement):
+ """
+ ``<a:theme>`` element, root of a theme part
+ """
+
+ _tag_seq = (
+ "a:themeElements",
+ "a:objectDefaults",
+ "a:extraClrSchemeLst",
+ "a:custClrLst",
+ "a:extLst",
+ )
+ del _tag_seq
+
+ @classmethod
+ def new_default(cls):
+ """
+ Return a new ``<a:theme>`` element containing default settings
+ suitable for use with a notes master.
+ """
+ return parse_from_template("theme")
diff --git a/.venv/lib/python3.12/site-packages/pptx/oxml/xmlchemy.py b/.venv/lib/python3.12/site-packages/pptx/oxml/xmlchemy.py
new file mode 100644
index 00000000..41fb2e17
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/oxml/xmlchemy.py
@@ -0,0 +1,717 @@
+"""Base and meta classes enabling declarative definition of custom element classes."""
+
+from __future__ import annotations
+
+import re
+from typing import Any, Callable, Iterable, Protocol, Sequence, Type, cast
+
+from lxml import etree
+from lxml.etree import ElementBase, _Element # pyright: ignore[reportPrivateUsage]
+
+from pptx.exc import InvalidXmlError
+from pptx.oxml import oxml_parser
+from pptx.oxml.ns import NamespacePrefixedTag, _nsmap, qn # pyright: ignore[reportPrivateUsage]
+from pptx.util import lazyproperty
+
+
+class AttributeType(Protocol):
+ """Interface for an object that can act as an attribute type.
+
+ An attribute-type specifies how values are transformed to and from the XML "string" value of the
+ attribute.
+ """
+
+ @classmethod
+ def from_xml(cls, xml_value: str) -> Any:
+ """Transform an attribute value to a Python value."""
+ ...
+
+ @classmethod
+ def to_xml(cls, value: Any) -> str:
+ """Transform a Python value to a str value suitable to this XML attribute."""
+ ...
+
+
+def OxmlElement(nsptag_str: str, nsmap: dict[str, str] | None = None) -> BaseOxmlElement:
+ """Return a "loose" lxml element having the tag specified by `nsptag_str`.
+
+ `nsptag_str` must contain the standard namespace prefix, e.g. 'a:tbl'. The resulting element is
+ an instance of the custom element class for this tag name if one is defined.
+ """
+ nsptag = NamespacePrefixedTag(nsptag_str)
+ nsmap = nsmap if nsmap is not None else nsptag.nsmap
+ return oxml_parser.makeelement(nsptag.clark_name, nsmap=nsmap)
+
+
+def serialize_for_reading(element: ElementBase):
+ """
+ Serialize *element* to human-readable XML suitable for tests. No XML
+ declaration.
+ """
+ xml = etree.tostring(element, encoding="unicode", pretty_print=True)
+ return XmlString(xml)
+
+
+class XmlString(str):
+ """Provides string comparison override suitable for serialized XML; useful for tests."""
+
+ # ' <w:xyz xmlns:a="http://ns/decl/a" attr_name="val">text</w:xyz>'
+ # | | || |
+ # +----------+------------------------------------------++-----------+
+ # front attrs | text
+ # close
+
+ _xml_elm_line_patt = re.compile(r"( *</?[\w:]+)(.*?)(/?>)([^<]*</[\w:]+>)?")
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, str):
+ return False
+ lines = self.splitlines()
+ lines_other = other.splitlines()
+ if len(lines) != len(lines_other):
+ return False
+ for line, line_other in zip(lines, lines_other):
+ if not self._eq_elm_strs(line, line_other):
+ return False
+ return True
+
+ def __ne__(self, other: object) -> bool:
+ return not self.__eq__(other)
+
+ def _attr_seq(self, attrs: str) -> list[str]:
+ """Return a sequence of attribute strings parsed from *attrs*.
+
+ Each attribute string is stripped of whitespace on both ends.
+ """
+ attrs = attrs.strip()
+ attr_lst = attrs.split()
+ return sorted(attr_lst)
+
+ def _eq_elm_strs(self, line: str, line_2: str) -> bool:
+ """True if the element in `line_2` is XML-equivalent to the element in `line`.
+
+ In particular, the order of attributes in XML is not significant.
+ """
+ front, attrs, close, text = self._parse_line(line)
+ front_2, attrs_2, close_2, text_2 = self._parse_line(line_2)
+ if front != front_2:
+ return False
+ if self._attr_seq(attrs) != self._attr_seq(attrs_2):
+ return False
+ if close != close_2:
+ return False
+ if text != text_2:
+ return False
+ return True
+
+ def _parse_line(self, line: str):
+ """Return front, attrs, close, text 4-tuple result of parsing XML element string `line`."""
+ match = self._xml_elm_line_patt.match(line)
+ if match is None:
+ raise ValueError("`line` does not match pattern for an XML element")
+ front, attrs, close, text = [match.group(n) for n in range(1, 5)]
+ return front, attrs, close, text
+
+
+class MetaOxmlElement(type):
+ """Metaclass for BaseOxmlElement."""
+
+ def __init__(cls, clsname: str, bases: tuple[type, ...], clsdict: dict[str, Any]):
+ dispatchable = (
+ OneAndOnlyOne,
+ OneOrMore,
+ OptionalAttribute,
+ RequiredAttribute,
+ ZeroOrMore,
+ ZeroOrOne,
+ ZeroOrOneChoice,
+ )
+ for key, value in clsdict.items():
+ if isinstance(value, dispatchable):
+ value.populate_class_members(cls, key)
+
+
+class BaseAttribute:
+ """Base class for OptionalAttribute and RequiredAttribute, providing common methods."""
+
+ def __init__(self, attr_name: str, simple_type: type[AttributeType]):
+ self._attr_name = attr_name
+ self._simple_type = simple_type
+
+ def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
+ """
+ Add the appropriate methods to *element_cls*.
+ """
+ self._element_cls = element_cls
+ self._prop_name = prop_name
+
+ self._add_attr_property()
+
+ def _add_attr_property(self):
+ """Add a read/write `{prop_name}` property to the element class.
+
+ The property returns the interpreted value of this attribute on access and changes the
+ attribute value to its ST_* counterpart on assignment.
+ """
+ property_ = property(self._getter, self._setter, None)
+ # assign unconditionally to overwrite element name definition
+ setattr(self._element_cls, self._prop_name, property_)
+
+ @property
+ def _clark_name(self):
+ if ":" in self._attr_name:
+ return qn(self._attr_name)
+ return self._attr_name
+
+ @property
+ def _getter(self) -> Callable[[BaseOxmlElement], Any]:
+ """Callable suitable for the "get" side of the attribute property descriptor."""
+ raise NotImplementedError("must be implemented by each subclass")
+
+ @property
+ def _setter(self) -> Callable[[BaseOxmlElement, Any], None]:
+ """Callable suitable for the "set" side of the attribute property descriptor."""
+ raise NotImplementedError("must be implemented by each subclass")
+
+
+class OptionalAttribute(BaseAttribute):
+ """Defines an optional attribute on a custom element class.
+
+ An optional attribute returns a default value when not present for reading. When assigned
+ |None|, the attribute is removed.
+ """
+
+ def __init__(self, attr_name: str, simple_type: type[AttributeType], default: Any = None):
+ super(OptionalAttribute, self).__init__(attr_name, simple_type)
+ self._default = default
+
+ @property
+ def _docstring(self):
+ """
+ Return the string to use as the ``__doc__`` attribute of the property
+ for this attribute.
+ """
+ return (
+ "%s type-converted value of ``%s`` attribute, or |None| (or spec"
+ "ified default value) if not present. Assigning the default valu"
+ "e causes the attribute to be removed from the element."
+ % (self._simple_type.__name__, self._attr_name)
+ )
+
+ @property
+ def _getter(self) -> Callable[[BaseOxmlElement], Any]:
+ """Callable suitable for the "get" side of the attribute property descriptor."""
+
+ def get_attr_value(obj: BaseOxmlElement) -> Any:
+ attr_str_value = obj.get(self._clark_name)
+ if attr_str_value is None:
+ return self._default
+ return self._simple_type.from_xml(attr_str_value)
+
+ get_attr_value.__doc__ = self._docstring
+ return get_attr_value
+
+ @property
+ def _setter(self) -> Callable[[BaseOxmlElement, Any], None]:
+ """Callable suitable for the "set" side of the attribute property descriptor."""
+
+ def set_attr_value(obj: BaseOxmlElement, value: Any) -> None:
+ # -- when an XML attribute has a default value, setting it to that default removes the
+ # -- attribute from the element (when it is present)
+ if value == self._default:
+ if self._clark_name in obj.attrib:
+ del obj.attrib[self._clark_name]
+ return
+ str_value = self._simple_type.to_xml(value)
+ obj.set(self._clark_name, str_value)
+
+ return set_attr_value
+
+
+class RequiredAttribute(BaseAttribute):
+ """Defines a required attribute on a custom element class.
+
+ A required attribute is assumed to be present for reading, so does not have a default value;
+ its actual value is always used. If missing on read, an |InvalidXmlError| is raised. It also
+ does not remove the attribute if |None| is assigned. Assigning |None| raises |TypeError| or
+ |ValueError|, depending on the simple type of the attribute.
+ """
+
+ @property
+ def _getter(self) -> Callable[[BaseOxmlElement], Any]:
+ """Callable suitable for the "get" side of the attribute property descriptor."""
+
+ def get_attr_value(obj: BaseOxmlElement) -> Any:
+ attr_str_value = obj.get(self._clark_name)
+ if attr_str_value is None:
+ raise InvalidXmlError(
+ "required '%s' attribute not present on element %s" % (self._attr_name, obj.tag)
+ )
+ return self._simple_type.from_xml(attr_str_value)
+
+ get_attr_value.__doc__ = self._docstring
+ return get_attr_value
+
+ @property
+ def _docstring(self):
+ """
+ Return the string to use as the ``__doc__`` attribute of the property
+ for this attribute.
+ """
+ return "%s type-converted value of ``%s`` attribute." % (
+ self._simple_type.__name__,
+ self._attr_name,
+ )
+
+ @property
+ def _setter(self) -> Callable[[BaseOxmlElement, Any], None]:
+ """Callable suitable for the "set" side of the attribute property descriptor."""
+
+ def set_attr_value(obj: BaseOxmlElement, value: Any) -> None:
+ str_value = self._simple_type.to_xml(value)
+ obj.set(self._clark_name, str_value)
+
+ return set_attr_value
+
+
+class _BaseChildElement:
+ """Base class for the child element classes corresponding to varying cardinalities.
+
+ Subclasses include ZeroOrOne and ZeroOrMore.
+ """
+
+ def __init__(self, nsptagname: str, successors: Sequence[str] = ()):
+ super(_BaseChildElement, self).__init__()
+ self._nsptagname = nsptagname
+ self._successors = successors
+
+ def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
+ """Baseline behavior for adding the appropriate methods to `element_cls`."""
+ self._element_cls = element_cls
+ self._prop_name = prop_name
+
+ def _add_adder(self):
+ """Add an ``_add_x()`` method to the element class for this child element."""
+
+ def _add_child(obj: BaseOxmlElement, **attrs: Any):
+ new_method = getattr(obj, self._new_method_name)
+ child = new_method()
+ for key, value in attrs.items():
+ setattr(child, key, value)
+ insert_method = getattr(obj, self._insert_method_name)
+ insert_method(child)
+ return child
+
+ _add_child.__doc__ = (
+ "Add a new ``<%s>`` child element unconditionally, inserted in t"
+ "he correct sequence." % self._nsptagname
+ )
+ self._add_to_class(self._add_method_name, _add_child)
+
+ def _add_creator(self):
+ """Add a `_new_{prop_name}()` method to the element class.
+
+ This method creates a new, empty element of the correct type, having no attributes.
+ """
+ creator = self._creator
+ creator.__doc__ = (
+ 'Return a "loose", newly created ``<%s>`` element having no attri'
+ "butes, text, or children." % self._nsptagname
+ )
+ self._add_to_class(self._new_method_name, creator)
+
+ def _add_getter(self):
+ """Add a read-only `{prop_name}` property to the parent element class.
+
+ The property locates and returns this child element or `None` if not present.
+ """
+ property_ = property(self._getter, None, None)
+ # assign unconditionally to overwrite element name definition
+ setattr(self._element_cls, self._prop_name, property_)
+
+ def _add_inserter(self):
+ """Add an ``_insert_x()`` method to the element class for this child element."""
+
+ def _insert_child(obj: BaseOxmlElement, child: BaseOxmlElement):
+ obj.insert_element_before(child, *self._successors)
+ return child
+
+ _insert_child.__doc__ = (
+ "Return the passed ``<%s>`` element after inserting it as a chil"
+ "d in the correct sequence." % self._nsptagname
+ )
+ self._add_to_class(self._insert_method_name, _insert_child)
+
+ def _add_list_getter(self):
+ """
+ Add a read-only ``{prop_name}_lst`` property to the element class to
+ retrieve a list of child elements matching this type.
+ """
+ prop_name = f"{self._prop_name}_lst"
+ property_ = property(self._list_getter, None, None)
+ setattr(self._element_cls, prop_name, property_)
+
+ @lazyproperty
+ def _add_method_name(self):
+ return "_add_%s" % self._prop_name
+
+ def _add_to_class(self, name: str, method: Callable[..., Any]):
+ """Add `method` to the target class as `name`, unless `name` is already defined there."""
+ if hasattr(self._element_cls, name):
+ return
+ setattr(self._element_cls, name, method)
+
+ @property
+ def _creator(self) -> Callable[[BaseOxmlElement], BaseOxmlElement]:
+ """Callable that creates a new, empty element of the child type, having no attributes."""
+
+ def new_child_element(obj: BaseOxmlElement):
+ return OxmlElement(self._nsptagname)
+
+ return new_child_element
+
+ @property
+ def _getter(self) -> Callable[[BaseOxmlElement], BaseOxmlElement | None]:
+ """Callable suitable for the "get" side of the property descriptor.
+
+ This default getter returns the child element with matching tag name or |None| if not
+ present.
+ """
+
+ def get_child_element(obj: BaseOxmlElement) -> BaseOxmlElement | None:
+ return obj.find(qn(self._nsptagname))
+
+ get_child_element.__doc__ = (
+ "``<%s>`` child element or |None| if not present." % self._nsptagname
+ )
+ return get_child_element
+
+ @lazyproperty
+ def _insert_method_name(self):
+ return "_insert_%s" % self._prop_name
+
+ @property
+ def _list_getter(self) -> Callable[[BaseOxmlElement], list[BaseOxmlElement]]:
+ """Callable suitable for the "get" side of a list property descriptor."""
+
+ def get_child_element_list(obj: BaseOxmlElement) -> list[BaseOxmlElement]:
+ return cast("list[BaseOxmlElement]", obj.findall(qn(self._nsptagname)))
+
+ get_child_element_list.__doc__ = (
+ "A list containing each of the ``<%s>`` child elements, in the o"
+ "rder they appear." % self._nsptagname
+ )
+ return get_child_element_list
+
+ @lazyproperty
+ def _remove_method_name(self):
+ return "_remove_%s" % self._prop_name
+
+ @lazyproperty
+ def _new_method_name(self):
+ return "_new_%s" % self._prop_name
+
+
+class Choice(_BaseChildElement):
+ """Defines a child element belonging to a group, only one of which may appear as a child."""
+
+ @property
+ def nsptagname(self):
+ return self._nsptagname
+
+ def populate_class_members( # pyright: ignore[reportIncompatibleMethodOverride]
+ self, element_cls: Type[BaseOxmlElement], group_prop_name: str, successors: Sequence[str]
+ ):
+ """Add the appropriate methods to `element_cls`."""
+ self._element_cls = element_cls
+ self._group_prop_name = group_prop_name
+ self._successors = successors
+
+ self._add_getter()
+ self._add_creator()
+ self._add_inserter()
+ self._add_adder()
+ self._add_get_or_change_to_method()
+
+ def _add_get_or_change_to_method(self) -> None:
+ """Add a `get_or_change_to_x()` method to the element class for this child element."""
+
+ def get_or_change_to_child(obj: BaseOxmlElement):
+ child = getattr(obj, self._prop_name)
+ if child is not None:
+ return child
+ remove_group_method = getattr(obj, self._remove_group_method_name)
+ remove_group_method()
+ add_method = getattr(obj, self._add_method_name)
+ child = add_method()
+ return child
+
+ get_or_change_to_child.__doc__ = (
+ "Return the ``<%s>`` child, replacing any other group element if" " found."
+ ) % self._nsptagname
+ self._add_to_class(self._get_or_change_to_method_name, get_or_change_to_child)
+
+ @property
+ def _prop_name(self):
+ """
+ Calculate property name from tag name, e.g. a:schemeClr -> schemeClr.
+ """
+ if ":" in self._nsptagname:
+ start = self._nsptagname.index(":") + 1
+ else:
+ start = 0
+ return self._nsptagname[start:]
+
+ @lazyproperty
+ def _get_or_change_to_method_name(self):
+ return "get_or_change_to_%s" % self._prop_name
+
+ @lazyproperty
+ def _remove_group_method_name(self):
+ return "_remove_%s" % self._group_prop_name
+
+
+class OneAndOnlyOne(_BaseChildElement):
+ """Defines a required child element for MetaOxmlElement."""
+
+ def __init__(self, nsptagname: str):
+ super(OneAndOnlyOne, self).__init__(nsptagname, ())
+
+ def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
+ """
+ Add the appropriate methods to *element_cls*.
+ """
+ super(OneAndOnlyOne, self).populate_class_members(element_cls, prop_name)
+ self._add_getter()
+
+ @property
+ def _getter(self) -> Callable[[BaseOxmlElement], BaseOxmlElement]:
+ """Callable suitable for the "get" side of the property descriptor."""
+
+ def get_child_element(obj: BaseOxmlElement) -> BaseOxmlElement:
+ child = obj.find(qn(self._nsptagname))
+ if child is None:
+ raise InvalidXmlError(
+ "required ``<%s>`` child element not present" % self._nsptagname
+ )
+ return child
+
+ get_child_element.__doc__ = "Required ``<%s>`` child element." % self._nsptagname
+ return get_child_element
+
+
+class OneOrMore(_BaseChildElement):
+ """Defines a repeating child element for MetaOxmlElement that must appear at least once."""
+
+ def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
+ """Add the appropriate methods to *element_cls*."""
+ super(OneOrMore, self).populate_class_members(element_cls, prop_name)
+ self._add_list_getter()
+ self._add_creator()
+ self._add_inserter()
+ self._add_adder()
+ self._add_public_adder()
+ delattr(element_cls, prop_name)
+
+ def _add_public_adder(self) -> None:
+ """Add a public `.add_x()` method to the parent element class."""
+
+ def add_child(obj: BaseOxmlElement) -> BaseOxmlElement:
+ private_add_method = getattr(obj, self._add_method_name)
+ child = private_add_method()
+ return child
+
+ add_child.__doc__ = (
+ "Add a new ``<%s>`` child element unconditionally, inserted in t"
+ "he correct sequence." % self._nsptagname
+ )
+ self._add_to_class(self._public_add_method_name, add_child)
+
+ @lazyproperty
+ def _public_add_method_name(self):
+ """
+ add_childElement() is public API for a repeating element, allowing
+ new elements to be added to the sequence. May be overridden to
+ provide a friendlier API to clients having domain appropriate
+ parameter names for required attributes.
+ """
+ return "add_%s" % self._prop_name
+
+
+class ZeroOrMore(_BaseChildElement):
+ """
+ Defines an optional repeating child element for MetaOxmlElement.
+ """
+
+ def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
+ """
+ Add the appropriate methods to *element_cls*.
+ """
+ super(ZeroOrMore, self).populate_class_members(element_cls, prop_name)
+ self._add_list_getter()
+ self._add_creator()
+ self._add_inserter()
+ self._add_adder()
+ delattr(element_cls, prop_name)
+
+
+class ZeroOrOne(_BaseChildElement):
+ """Defines an optional child element for MetaOxmlElement."""
+
+ def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
+ """Add the appropriate methods to `element_cls`."""
+ super(ZeroOrOne, self).populate_class_members(element_cls, prop_name)
+ self._add_getter()
+ self._add_creator()
+ self._add_inserter()
+ self._add_adder()
+ self._add_get_or_adder()
+ self._add_remover()
+
+ def _add_get_or_adder(self):
+ """Add a `.get_or_add_x()` method to the element class for this child element."""
+
+ def get_or_add_child(obj: BaseOxmlElement) -> BaseOxmlElement:
+ child = getattr(obj, self._prop_name)
+ if child is None:
+ add_method = getattr(obj, self._add_method_name)
+ child = add_method()
+ return child
+
+ get_or_add_child.__doc__ = (
+ "Return the ``<%s>`` child element, newly added if not present."
+ ) % self._nsptagname
+ self._add_to_class(self._get_or_add_method_name, get_or_add_child)
+
+ def _add_remover(self):
+ """Add a `._remove_x()` method to the element class for this child element."""
+
+ def _remove_child(obj: BaseOxmlElement) -> None:
+ obj.remove_all(self._nsptagname)
+
+ _remove_child.__doc__ = f"Remove all `{self._nsptagname}` child elements."
+ self._add_to_class(self._remove_method_name, _remove_child)
+
+ @lazyproperty
+ def _get_or_add_method_name(self):
+ return "get_or_add_%s" % self._prop_name
+
+
+class ZeroOrOneChoice(_BaseChildElement):
+ """An `EG_*` element group where at most one of its members may appear as a child."""
+
+ def __init__(self, choices: Iterable[Choice], successors: Iterable[str] = ()):
+ self._choices = tuple(choices)
+ self._successors = tuple(successors)
+
+ def populate_class_members(self, element_cls: Type[BaseOxmlElement], prop_name: str):
+ """Add the appropriate methods to `element_cls`."""
+ super(ZeroOrOneChoice, self).populate_class_members(element_cls, prop_name)
+ self._add_choice_getter()
+ for choice in self._choices:
+ choice.populate_class_members(element_cls, self._prop_name, self._successors)
+ self._add_group_remover()
+
+ def _add_choice_getter(self):
+ """Add a read-only `.{prop_name}` property to the element class.
+
+ The property returns the present member of this group, or |None| if none are present.
+ """
+ property_ = property(self._choice_getter, None, None)
+ # assign unconditionally to overwrite element name definition
+ setattr(self._element_cls, self._prop_name, property_)
+
+ def _add_group_remover(self):
+ """Add a `._remove_eg_x()` method to the element class for this choice group."""
+
+ def _remove_choice_group(obj: BaseOxmlElement) -> None:
+ for tagname in self._member_nsptagnames:
+ obj.remove_all(tagname)
+
+ _remove_choice_group.__doc__ = "Remove the current choice group child element if present."
+ self._add_to_class(self._remove_choice_group_method_name, _remove_choice_group)
+
+ @property
+ def _choice_getter(self):
+ """
+ Return a function object suitable for the "get" side of the property
+ descriptor.
+ """
+
+ def get_group_member_element(obj: BaseOxmlElement) -> BaseOxmlElement | None:
+ return cast(
+ "BaseOxmlElement | None", obj.first_child_found_in(*self._member_nsptagnames)
+ )
+
+ get_group_member_element.__doc__ = (
+ "Return the child element belonging to this element group, or "
+ "|None| if no member child is present."
+ )
+ return get_group_member_element
+
+ @lazyproperty
+ def _member_nsptagnames(self) -> list[str]:
+ """Sequence of namespace-prefixed tagnames, one for each member element of choice group."""
+ return [choice.nsptagname for choice in self._choices]
+
+ @lazyproperty
+ def _remove_choice_group_method_name(self):
+ """Function-name for choice remover."""
+ return f"_remove_{self._prop_name}"
+
+
+# -- lxml typing isn't quite right here, just ignore this error on _Element --
+class BaseOxmlElement(etree.ElementBase, metaclass=MetaOxmlElement):
+ """Effective base class for all custom element classes.
+
+ Adds standardized behavior to all classes in one place.
+ """
+
+ def __repr__(self):
+ return "<%s '<%s>' at 0x%0x>" % (
+ self.__class__.__name__,
+ self._nsptag,
+ id(self),
+ )
+
+ def first_child_found_in(self, *tagnames: str) -> _Element | None:
+ """First child with tag in `tagnames`, or None if not found."""
+ for tagname in tagnames:
+ child = self.find(qn(tagname))
+ if child is not None:
+ return child
+ return None
+
+ def insert_element_before(self, elm: ElementBase, *tagnames: str):
+ successor = self.first_child_found_in(*tagnames)
+ if successor is not None:
+ successor.addprevious(elm)
+ else:
+ self.append(elm)
+ return elm
+
+ def remove_all(self, *tagnames: str) -> None:
+ """Remove child elements with tagname (e.g. "a:p") in `tagnames`."""
+ for tagname in tagnames:
+ matching = self.findall(qn(tagname))
+ for child in matching:
+ self.remove(child)
+
+ @property
+ def xml(self) -> str:
+ """XML string for this element, suitable for testing purposes.
+
+ Pretty printed for readability and without an XML declaration at the top.
+ """
+ return serialize_for_reading(self)
+
+ def xpath(self, xpath_str: str) -> Any: # pyright: ignore[reportIncompatibleMethodOverride]
+ """Override of `lxml` _Element.xpath() method.
+
+ Provides standard Open XML namespace mapping (`nsmap`) in centralized location.
+ """
+ return super().xpath(xpath_str, namespaces=_nsmap)
+
+ @property
+ def _nsptag(self) -> str:
+ return NamespacePrefixedTag.from_clark_name(self.tag)
diff --git a/.venv/lib/python3.12/site-packages/pptx/package.py b/.venv/lib/python3.12/site-packages/pptx/package.py
new file mode 100644
index 00000000..79703cd6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/package.py
@@ -0,0 +1,222 @@
+"""Overall .pptx package."""
+
+from __future__ import annotations
+
+from typing import IO, Iterator
+
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.opc.package import OpcPackage
+from pptx.opc.packuri import PackURI
+from pptx.parts.coreprops import CorePropertiesPart
+from pptx.parts.image import Image, ImagePart
+from pptx.parts.media import MediaPart
+from pptx.util import lazyproperty
+
+
+class Package(OpcPackage):
+ """An overall .pptx package."""
+
+ @lazyproperty
+ def core_properties(self) -> CorePropertiesPart:
+ """Instance of |CoreProperties| holding read/write Dublin Core doc properties.
+
+ Creates a default core properties part if one is not present (not common).
+ """
+ try:
+ return self.part_related_by(RT.CORE_PROPERTIES)
+ except KeyError:
+ core_props = CorePropertiesPart.default(self)
+ self.relate_to(core_props, RT.CORE_PROPERTIES)
+ return core_props
+
+ def get_or_add_image_part(self, image_file: str | IO[bytes]):
+ """
+ Return an |ImagePart| object containing the image in *image_file*. If
+ the image part already exists in this package, it is reused,
+ otherwise a new one is created.
+ """
+ return self._image_parts.get_or_add_image_part(image_file)
+
+ def get_or_add_media_part(self, media):
+ """Return a |MediaPart| object containing the media in *media*.
+
+ If a media part for this media bytestream ("file") is already present
+ in this package, it is reused, otherwise a new one is created.
+ """
+ return self._media_parts.get_or_add_media_part(media)
+
+ def next_image_partname(self, ext: str) -> PackURI:
+ """Return a |PackURI| instance representing the next available image partname.
+
+ Partname uses the next available sequence number. *ext* is used as the extention on the
+ returned partname.
+ """
+
+ def first_available_image_idx():
+ image_idxs = sorted(
+ [
+ part.partname.idx
+ for part in self.iter_parts()
+ if (
+ part.partname.startswith("/ppt/media/image")
+ and part.partname.idx is not None
+ )
+ ]
+ )
+ for i, image_idx in enumerate(image_idxs):
+ idx = i + 1
+ if idx < image_idx:
+ return idx
+ return len(image_idxs) + 1
+
+ idx = first_available_image_idx()
+ return PackURI("/ppt/media/image%d.%s" % (idx, ext))
+
+ def next_media_partname(self, ext):
+ """Return |PackURI| instance for next available media partname.
+
+ Partname is first available, starting at sequence number 1. Empty
+ sequence numbers are reused. *ext* is used as the extension on the
+ returned partname.
+ """
+
+ def first_available_media_idx():
+ media_idxs = sorted(
+ [
+ part.partname.idx
+ for part in self.iter_parts()
+ if part.partname.startswith("/ppt/media/media")
+ ]
+ )
+ for i, media_idx in enumerate(media_idxs):
+ idx = i + 1
+ if idx < media_idx:
+ return idx
+ return len(media_idxs) + 1
+
+ idx = first_available_media_idx()
+ return PackURI("/ppt/media/media%d.%s" % (idx, ext))
+
+ @property
+ def presentation_part(self):
+ """
+ Reference to the |Presentation| instance contained in this package.
+ """
+ return self.main_document_part
+
+ @lazyproperty
+ def _image_parts(self):
+ """
+ |_ImageParts| object providing access to the image parts in this
+ package.
+ """
+ return _ImageParts(self)
+
+ @lazyproperty
+ def _media_parts(self):
+ """Return |_MediaParts| object for this package.
+
+ The media parts object provides access to all the media parts in this
+ package.
+ """
+ return _MediaParts(self)
+
+
+class _ImageParts(object):
+ """Provides access to the image parts in a package."""
+
+ def __init__(self, package):
+ super(_ImageParts, self).__init__()
+ self._package = package
+
+ def __iter__(self) -> Iterator[ImagePart]:
+ """Generate a reference to each |ImagePart| object in the package."""
+ image_parts = []
+ for rel in self._package.iter_rels():
+ if rel.is_external:
+ continue
+ if rel.reltype != RT.IMAGE:
+ continue
+ image_part = rel.target_part
+ if image_part in image_parts:
+ continue
+ image_parts.append(image_part)
+ yield image_part
+
+ def get_or_add_image_part(self, image_file: str | IO[bytes]) -> ImagePart:
+ """Return |ImagePart| object containing the image in `image_file`.
+
+ `image_file` can be either a path to an image file or a file-like object
+ containing an image. If an image part containing this same image already exists,
+ that instance is returned, otherwise a new image part is created.
+ """
+ image = Image.from_file(image_file)
+ image_part = self._find_by_sha1(image.sha1)
+ return image_part if image_part else ImagePart.new(self._package, image)
+
+ def _find_by_sha1(self, sha1: str) -> ImagePart | None:
+ """
+ Return an |ImagePart| object belonging to this package or |None| if
+ no matching image part is found. The image part is identified by the
+ SHA1 hash digest of the image binary it contains.
+ """
+ for image_part in self:
+ # ---skip unknown/unsupported image types, like SVG---
+ if not hasattr(image_part, "sha1"):
+ continue
+ if image_part.sha1 == sha1:
+ return image_part
+ return None
+
+
+class _MediaParts(object):
+ """Provides access to the media parts in a package.
+
+ Supports iteration and :meth:`get()` using the media object SHA1 hash as
+ its key.
+ """
+
+ def __init__(self, package):
+ super(_MediaParts, self).__init__()
+ self._package = package
+
+ def __iter__(self):
+ """Generate a reference to each |MediaPart| object in the package."""
+ # A media part can appear in more than one relationship (and commonly
+ # does in the case of video). Use media_parts to keep track of those
+ # that have been "yielded"; they can be skipped if they occur again.
+ media_parts = []
+ for rel in self._package.iter_rels():
+ if rel.is_external:
+ continue
+ if rel.reltype not in (RT.MEDIA, RT.VIDEO):
+ continue
+ media_part = rel.target_part
+ if media_part in media_parts:
+ continue
+ media_parts.append(media_part)
+ yield media_part
+
+ def get_or_add_media_part(self, media):
+ """Return a |MediaPart| object containing the media in *media*.
+
+ If this package already contains a media part for the same
+ bytestream, that instance is returned, otherwise a new media part is
+ created.
+ """
+ media_part = self._find_by_sha1(media.sha1)
+ if media_part is None:
+ media_part = MediaPart.new(self._package, media)
+ return media_part
+
+ def _find_by_sha1(self, sha1):
+ """Return |MediaPart| object having *sha1* hash or None if not found.
+
+ All media parts belonging to this package are considered. A media
+ part is identified by the SHA1 hash digest of its bytestream
+ ("file").
+ """
+ for media_part in self:
+ if media_part.sha1 == sha1:
+ return media_part
+ return None
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/__init__.py b/.venv/lib/python3.12/site-packages/pptx/parts/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/chart.py b/.venv/lib/python3.12/site-packages/pptx/parts/chart.py
new file mode 100644
index 00000000..7208071b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/chart.py
@@ -0,0 +1,95 @@
+"""Chart part objects, including Chart and Charts."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from pptx.chart.chart import Chart
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.opc.package import XmlPart
+from pptx.parts.embeddedpackage import EmbeddedXlsxPart
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.chart.data import ChartData
+ from pptx.enum.chart import XL_CHART_TYPE
+ from pptx.package import Package
+
+
+class ChartPart(XmlPart):
+ """A chart part.
+
+ Corresponds to parts having partnames matching ppt/charts/chart[1-9][0-9]*.xml
+ """
+
+ partname_template = "/ppt/charts/chart%d.xml"
+
+ @classmethod
+ def new(cls, chart_type: XL_CHART_TYPE, chart_data: ChartData, package: Package):
+ """Return new |ChartPart| instance added to `package`.
+
+ Returned chart-part contains a chart of `chart_type` depicting `chart_data`.
+ """
+ chart_part = cls.load(
+ package.next_partname(cls.partname_template),
+ CT.DML_CHART,
+ package,
+ chart_data.xml_bytes(chart_type),
+ )
+ chart_part.chart_workbook.update_from_xlsx_blob(chart_data.xlsx_blob)
+ return chart_part
+
+ @lazyproperty
+ def chart(self):
+ """|Chart| object representing the chart in this part."""
+ return Chart(self._element, self)
+
+ @lazyproperty
+ def chart_workbook(self):
+ """
+ The |ChartWorkbook| object providing access to the external chart
+ data in a linked or embedded Excel workbook.
+ """
+ return ChartWorkbook(self._element, self)
+
+
+class ChartWorkbook(object):
+ """Provides access to external chart data in a linked or embedded Excel workbook."""
+
+ def __init__(self, chartSpace, chart_part):
+ super(ChartWorkbook, self).__init__()
+ self._chartSpace = chartSpace
+ self._chart_part = chart_part
+
+ def update_from_xlsx_blob(self, xlsx_blob):
+ """
+ Replace the Excel spreadsheet in the related |EmbeddedXlsxPart| with
+ the Excel binary in *xlsx_blob*, adding a new |EmbeddedXlsxPart| if
+ there isn't one.
+ """
+ xlsx_part = self.xlsx_part
+ if xlsx_part is None:
+ self.xlsx_part = EmbeddedXlsxPart.new(xlsx_blob, self._chart_part.package)
+ return
+ xlsx_part.blob = xlsx_blob
+
+ @property
+ def xlsx_part(self):
+ """Optional |EmbeddedXlsxPart| object containing data for this chart.
+
+ This related part has its rId at `c:chartSpace/c:externalData/@rId`. This value
+ is |None| if there is no `<c:externalData>` element.
+ """
+ xlsx_part_rId = self._chartSpace.xlsx_part_rId
+ return None if xlsx_part_rId is None else self._chart_part.related_part(xlsx_part_rId)
+
+ @xlsx_part.setter
+ def xlsx_part(self, xlsx_part):
+ """
+ Set the related |EmbeddedXlsxPart| to *xlsx_part*. Assume one does
+ not already exist.
+ """
+ rId = self._chart_part.relate_to(xlsx_part, RT.PACKAGE)
+ externalData = self._chartSpace.get_or_add_externalData()
+ externalData.rId = rId
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/coreprops.py b/.venv/lib/python3.12/site-packages/pptx/parts/coreprops.py
new file mode 100644
index 00000000..8471cc8e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/coreprops.py
@@ -0,0 +1,167 @@
+"""Core properties part, corresponds to ``/docProps/core.xml`` part in package."""
+
+from __future__ import annotations
+
+import datetime as dt
+from typing import TYPE_CHECKING
+
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.opc.package import XmlPart
+from pptx.opc.packuri import PackURI
+from pptx.oxml.coreprops import CT_CoreProperties
+
+if TYPE_CHECKING:
+ from pptx.package import Package
+
+
+class CorePropertiesPart(XmlPart):
+ """Corresponds to part named `/docProps/core.xml`.
+
+ Contains the core document properties for this document package.
+ """
+
+ _element: CT_CoreProperties
+
+ @classmethod
+ def default(cls, package: Package):
+ """Return default new |CorePropertiesPart| instance suitable as starting point.
+
+ This provides a base for adding core-properties to a package that doesn't yet
+ have any.
+ """
+ core_props = cls._new(package)
+ core_props.title = "PowerPoint Presentation"
+ core_props.last_modified_by = "python-pptx"
+ core_props.revision = 1
+ core_props.modified = dt.datetime.now(dt.timezone.utc).replace(tzinfo=None)
+ return core_props
+
+ @property
+ def author(self) -> str:
+ return self._element.author_text
+
+ @author.setter
+ def author(self, value: str):
+ self._element.author_text = value
+
+ @property
+ def category(self) -> str:
+ return self._element.category_text
+
+ @category.setter
+ def category(self, value: str):
+ self._element.category_text = value
+
+ @property
+ def comments(self) -> str:
+ return self._element.comments_text
+
+ @comments.setter
+ def comments(self, value: str):
+ self._element.comments_text = value
+
+ @property
+ def content_status(self) -> str:
+ return self._element.contentStatus_text
+
+ @content_status.setter
+ def content_status(self, value: str):
+ self._element.contentStatus_text = value
+
+ @property
+ def created(self):
+ return self._element.created_datetime
+
+ @created.setter
+ def created(self, value: dt.datetime):
+ self._element.created_datetime = value
+
+ @property
+ def identifier(self) -> str:
+ return self._element.identifier_text
+
+ @identifier.setter
+ def identifier(self, value: str):
+ self._element.identifier_text = value
+
+ @property
+ def keywords(self) -> str:
+ return self._element.keywords_text
+
+ @keywords.setter
+ def keywords(self, value: str):
+ self._element.keywords_text = value
+
+ @property
+ def language(self) -> str:
+ return self._element.language_text
+
+ @language.setter
+ def language(self, value: str):
+ self._element.language_text = value
+
+ @property
+ def last_modified_by(self) -> str:
+ return self._element.lastModifiedBy_text
+
+ @last_modified_by.setter
+ def last_modified_by(self, value: str):
+ self._element.lastModifiedBy_text = value
+
+ @property
+ def last_printed(self):
+ return self._element.lastPrinted_datetime
+
+ @last_printed.setter
+ def last_printed(self, value: dt.datetime):
+ self._element.lastPrinted_datetime = value
+
+ @property
+ def modified(self):
+ return self._element.modified_datetime
+
+ @modified.setter
+ def modified(self, value: dt.datetime):
+ self._element.modified_datetime = value
+
+ @property
+ def revision(self):
+ return self._element.revision_number
+
+ @revision.setter
+ def revision(self, value: int):
+ self._element.revision_number = value
+
+ @property
+ def subject(self) -> str:
+ return self._element.subject_text
+
+ @subject.setter
+ def subject(self, value: str):
+ self._element.subject_text = value
+
+ @property
+ def title(self) -> str:
+ return self._element.title_text
+
+ @title.setter
+ def title(self, value: str):
+ self._element.title_text = value
+
+ @property
+ def version(self) -> str:
+ return self._element.version_text
+
+ @version.setter
+ def version(self, value: str):
+ self._element.version_text = value
+
+ @classmethod
+ def _new(cls, package: Package) -> CorePropertiesPart:
+ """Return new empty |CorePropertiesPart| instance."""
+ return CorePropertiesPart(
+ PackURI("/docProps/core.xml"),
+ CT.OPC_CORE_PROPERTIES,
+ package,
+ CT_CoreProperties.new_coreProperties(),
+ )
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/embeddedpackage.py b/.venv/lib/python3.12/site-packages/pptx/parts/embeddedpackage.py
new file mode 100644
index 00000000..7aa2cf40
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/embeddedpackage.py
@@ -0,0 +1,93 @@
+"""Embedded Package part objects.
+
+"Package" in this context means another OPC package, i.e. a DOCX, PPTX, or XLSX "file".
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from pptx.enum.shapes import PROG_ID
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.opc.package import Part
+
+if TYPE_CHECKING:
+ from pptx.package import Package
+
+
+class EmbeddedPackagePart(Part):
+ """A distinct OPC package, e.g. an Excel file, embedded in this PPTX package.
+
+ Has a partname like: `ppt/embeddings/Microsoft_Excel_Sheet1.xlsx`.
+ """
+
+ @classmethod
+ def factory(cls, prog_id: PROG_ID | str, object_blob: bytes, package: Package):
+ """Return a new |EmbeddedPackagePart| subclass instance added to *package*.
+
+ The subclass is determined by `prog_id` which corresponds to the "application"
+ used to open the "file-type" of `object_blob`. The returned part contains the
+ bytes of `object_blob` and has the content-type also determined by `prog_id`.
+ """
+ # --- a generic OLE object has no subclass ---
+ if not isinstance(prog_id, PROG_ID):
+ return cls(
+ package.next_partname("/ppt/embeddings/oleObject%d.bin"),
+ CT.OFC_OLE_OBJECT,
+ package,
+ object_blob,
+ )
+
+ # --- A Microsoft Office file-type is a distinguished package object ---
+ EmbeddedPartCls = {
+ PROG_ID.DOCX: EmbeddedDocxPart,
+ PROG_ID.PPTX: EmbeddedPptxPart,
+ PROG_ID.XLSX: EmbeddedXlsxPart,
+ }[prog_id]
+
+ return EmbeddedPartCls.new(object_blob, package)
+
+ @classmethod
+ def new(cls, blob: bytes, package: Package):
+ """Return new |EmbeddedPackagePart| subclass object.
+
+ The returned part object contains `blob` and is added to `package`.
+ """
+ return cls(
+ package.next_partname(cls.partname_template),
+ cls.content_type,
+ package,
+ blob,
+ )
+
+
+class EmbeddedDocxPart(EmbeddedPackagePart):
+ """A Word .docx file stored in a part.
+
+ This part-type arises when a Word document appears as an embedded OLE-object shape.
+ """
+
+ partname_template = "/ppt/embeddings/Microsoft_Word_Document%d.docx"
+ content_type = CT.WML_DOCUMENT
+
+
+class EmbeddedPptxPart(EmbeddedPackagePart):
+ """A PowerPoint file stored in a part.
+
+ This part-type arises when a PowerPoint presentation (.pptx file) appears as an
+ embedded OLE-object shape.
+ """
+
+ partname_template = "/ppt/embeddings/Microsoft_PowerPoint_Presentation%d.pptx"
+ content_type = CT.PML_PRESENTATION
+
+
+class EmbeddedXlsxPart(EmbeddedPackagePart):
+ """An Excel file stored in a part.
+
+ This part-type arises as the data source for a chart, but may also be the OLE-object
+ for an embedded object shape.
+ """
+
+ partname_template = "/ppt/embeddings/Microsoft_Excel_Sheet%d.xlsx"
+ content_type = CT.SML_SHEET
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/image.py b/.venv/lib/python3.12/site-packages/pptx/parts/image.py
new file mode 100644
index 00000000..9be5d02d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/image.py
@@ -0,0 +1,275 @@
+"""ImagePart and related objects."""
+
+from __future__ import annotations
+
+import hashlib
+import io
+import os
+from typing import IO, TYPE_CHECKING, Any, cast
+
+from PIL import Image as PIL_Image
+
+from pptx.opc.package import Part
+from pptx.opc.spec import image_content_types
+from pptx.util import Emu, lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.opc.packuri import PackURI
+ from pptx.package import Package
+ from pptx.util import Length
+
+
+class ImagePart(Part):
+ """An image part.
+
+ An image part generally has a partname matching the regex `ppt/media/image[1-9][0-9]*.*`.
+ """
+
+ def __init__(
+ self,
+ partname: PackURI,
+ content_type: str,
+ package: Package,
+ blob: bytes,
+ filename: str | None = None,
+ ):
+ super(ImagePart, self).__init__(partname, content_type, package, blob)
+ self._blob = blob
+ self._filename = filename
+
+ @classmethod
+ def new(cls, package: Package, image: Image) -> ImagePart:
+ """Return new |ImagePart| instance containing `image`.
+
+ `image` is an |Image| object.
+ """
+ return cls(
+ package.next_image_partname(image.ext),
+ image.content_type,
+ package,
+ image.blob,
+ image.filename,
+ )
+
+ @property
+ def desc(self) -> str:
+ """The filename associated with this image.
+
+ Either the filename of the original image or a generic name of the form `image.ext` where
+ `ext` is appropriate to the image file format, e.g. `'jpg'`. An image created using a path
+ will have that filename; one created with a file-like object will have a generic name.
+ """
+ # -- return generic filename if original filename is unknown --
+ if self._filename is None:
+ return f"image.{self.ext}"
+ return self._filename
+
+ @property
+ def ext(self) -> str:
+ """File-name extension for this image e.g. `'png'`."""
+ return self.partname.ext
+
+ @property
+ def image(self) -> Image:
+ """An |Image| object containing the image in this image part.
+
+ Note this is a `pptx.image.Image` object, not a PIL Image.
+ """
+ return Image(self._blob, self.desc)
+
+ def scale(self, scaled_cx: int | None, scaled_cy: int | None) -> tuple[int, int]:
+ """Return scaled image dimensions in EMU based on the combination of parameters supplied.
+
+ If `scaled_cx` and `scaled_cy` are both |None|, the native image size is returned. If
+ neither `scaled_cx` nor `scaled_cy` is |None|, their values are returned unchanged. If a
+ value is provided for either `scaled_cx` or `scaled_cy` and the other is |None|, the
+ missing value is calculated such that the image's aspect ratio is preserved.
+ """
+ image_cx, image_cy = self._native_size
+
+ if scaled_cx and scaled_cy:
+ return scaled_cx, scaled_cy
+
+ if scaled_cx and not scaled_cy:
+ scaling_factor = float(scaled_cx) / float(image_cx)
+ scaled_cy = int(round(image_cy * scaling_factor))
+ return scaled_cx, scaled_cy
+
+ if not scaled_cx and scaled_cy:
+ scaling_factor = float(scaled_cy) / float(image_cy)
+ scaled_cx = int(round(image_cx * scaling_factor))
+ return scaled_cx, scaled_cy
+
+ # -- only remaining case is both `scaled_cx` and `scaled_cy` are `None` --
+ return image_cx, image_cy
+
+ @lazyproperty
+ def sha1(self) -> str:
+ """The 40-character SHA1 hash digest for the image binary of this image part.
+
+ like: `"1be010ea47803b00e140b852765cdf84f491da47"`.
+ """
+ return hashlib.sha1(self._blob).hexdigest()
+
+ @property
+ def _dpi(self) -> tuple[int, int]:
+ """(horz_dpi, vert_dpi) pair representing the dots-per-inch resolution of this image."""
+ image = Image.from_blob(self._blob)
+ return image.dpi
+
+ @property
+ def _native_size(self) -> tuple[Length, Length]:
+ """A (width, height) 2-tuple representing the native dimensions of the image in EMU.
+
+ Calculated based on the image DPI value, if present, assuming 72 dpi as a default.
+ """
+ EMU_PER_INCH = 914400
+ horz_dpi, vert_dpi = self._dpi
+ width_px, height_px = self._px_size
+
+ width = EMU_PER_INCH * width_px / horz_dpi
+ height = EMU_PER_INCH * height_px / vert_dpi
+
+ return Emu(int(width)), Emu(int(height))
+
+ @property
+ def _px_size(self) -> tuple[int, int]:
+ """A (width, height) 2-tuple representing the dimensions of this image in pixels."""
+ image = Image.from_blob(self._blob)
+ return image.size
+
+
+class Image(object):
+ """Immutable value object representing an image such as a JPEG, PNG, or GIF."""
+
+ def __init__(self, blob: bytes, filename: str | None):
+ super(Image, self).__init__()
+ self._blob = blob
+ self._filename = filename
+
+ @classmethod
+ def from_blob(cls, blob: bytes, filename: str | None = None) -> Image:
+ """Return a new |Image| object loaded from the image binary in `blob`."""
+ return cls(blob, filename)
+
+ @classmethod
+ def from_file(cls, image_file: str | IO[bytes]) -> Image:
+ """Return a new |Image| object loaded from `image_file`.
+
+ `image_file` can be either a path (str) or a file-like object.
+ """
+ if isinstance(image_file, str):
+ # treat image_file as a path
+ with open(image_file, "rb") as f:
+ blob = f.read()
+ filename = os.path.basename(image_file)
+ else:
+ # assume image_file is a file-like object
+ # ---reposition file cursor if it has one---
+ if callable(getattr(image_file, "seek")):
+ image_file.seek(0)
+ blob = image_file.read()
+ filename = None
+
+ return cls.from_blob(blob, filename)
+
+ @property
+ def blob(self) -> bytes:
+ """The binary image bytestream of this image."""
+ return self._blob
+
+ @lazyproperty
+ def content_type(self) -> str:
+ """MIME-type of this image, e.g. `"image/jpeg"`."""
+ return image_content_types[self.ext]
+
+ @lazyproperty
+ def dpi(self) -> tuple[int, int]:
+ """A (horz_dpi, vert_dpi) 2-tuple specifying the dots-per-inch resolution of this image.
+
+ A default value of (72, 72) is used if the dpi is not specified in the image file.
+ """
+
+ def int_dpi(dpi: Any):
+ """Return an integer dots-per-inch value corresponding to `dpi`.
+
+ If `dpi` is |None|, a non-numeric type, less than 1 or greater than 2048, 72 is
+ returned.
+ """
+ try:
+ int_dpi = int(round(float(dpi)))
+ if int_dpi < 1 or int_dpi > 2048:
+ int_dpi = 72
+ except (TypeError, ValueError):
+ int_dpi = 72
+ return int_dpi
+
+ def normalize_pil_dpi(pil_dpi: tuple[int, int] | None):
+ """Return a (horz_dpi, vert_dpi) 2-tuple corresponding to `pil_dpi`.
+
+ The value for the 'dpi' key in the `info` dict of a PIL image. If the 'dpi' key is not
+ present or contains an invalid value, `(72, 72)` is returned.
+ """
+ if isinstance(pil_dpi, tuple):
+ return (int_dpi(pil_dpi[0]), int_dpi(pil_dpi[1]))
+ return (72, 72)
+
+ return normalize_pil_dpi(self._pil_props[2])
+
+ @lazyproperty
+ def ext(self) -> str:
+ """Canonical file extension for this image e.g. `'png'`.
+
+ The returned extension is all lowercase and is the canonical extension for the content type
+ of this image, regardless of what extension may have been used in its filename, if any.
+ """
+ ext_map = {
+ "BMP": "bmp",
+ "GIF": "gif",
+ "JPEG": "jpg",
+ "PNG": "png",
+ "TIFF": "tiff",
+ "WMF": "wmf",
+ }
+ format = self._format
+ if format not in ext_map:
+ tmpl = "unsupported image format, expected one of: %s, got '%s'"
+ raise ValueError(tmpl % (ext_map.keys(), format))
+ return ext_map[format]
+
+ @property
+ def filename(self) -> str | None:
+ """Filename from path used to load this image, if loaded from the filesystem.
+
+ |None| if no filename was used in loading, such as when loaded from an in-memory stream.
+ """
+ return self._filename
+
+ @lazyproperty
+ def sha1(self) -> str:
+ """SHA1 hash digest of the image blob."""
+ return hashlib.sha1(self._blob).hexdigest()
+
+ @lazyproperty
+ def size(self) -> tuple[int, int]:
+ """A (width, height) 2-tuple specifying the dimensions of this image in pixels."""
+ return self._pil_props[1]
+
+ @property
+ def _format(self) -> str | None:
+ """The PIL Image format of this image, e.g. 'PNG'."""
+ return self._pil_props[0]
+
+ @lazyproperty
+ def _pil_props(self) -> tuple[str | None, tuple[int, int], tuple[int, int] | None]:
+ """tuple of image properties extracted from this image using Pillow."""
+ stream = io.BytesIO(self._blob)
+ pil_image = PIL_Image.open(stream) # pyright: ignore[reportUnknownMemberType]
+ format = pil_image.format
+ width_px, height_px = pil_image.size
+ dpi = cast(
+ "tuple[int, int] | None",
+ pil_image.info.get("dpi"), # pyright: ignore[reportUnknownMemberType]
+ )
+ stream.close()
+ return (format, (width_px, height_px), dpi)
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/media.py b/.venv/lib/python3.12/site-packages/pptx/parts/media.py
new file mode 100644
index 00000000..7e8bc2f2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/media.py
@@ -0,0 +1,37 @@
+"""MediaPart and related objects."""
+
+from __future__ import annotations
+
+import hashlib
+
+from pptx.opc.package import Part
+from pptx.util import lazyproperty
+
+
+class MediaPart(Part):
+ """A media part, containing an audio or video resource.
+
+ A media part generally has a partname matching the regex
+ `ppt/media/media[1-9][0-9]*.*`.
+ """
+
+ @classmethod
+ def new(cls, package, media):
+ """Return new |MediaPart| instance containing `media`.
+
+ `media` must be a |Media| object.
+ """
+ return cls(
+ package.next_media_partname(media.ext),
+ media.content_type,
+ package,
+ media.blob,
+ )
+
+ @lazyproperty
+ def sha1(self):
+ """The SHA1 hash digest for the media binary of this media part.
+
+ Example: `'1be010ea47803b00e140b852765cdf84f491da47'`
+ """
+ return hashlib.sha1(self._blob).hexdigest()
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/presentation.py b/.venv/lib/python3.12/site-packages/pptx/parts/presentation.py
new file mode 100644
index 00000000..1413de45
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/presentation.py
@@ -0,0 +1,126 @@
+"""Presentation part, the main part in a .pptx package."""
+
+from __future__ import annotations
+
+from typing import IO, TYPE_CHECKING, Iterable
+
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.opc.package import XmlPart
+from pptx.opc.packuri import PackURI
+from pptx.parts.slide import NotesMasterPart, SlidePart
+from pptx.presentation import Presentation
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.parts.coreprops import CorePropertiesPart
+ from pptx.slide import NotesMaster, Slide, SlideLayout, SlideMaster
+
+
+class PresentationPart(XmlPart):
+ """Top level class in object model.
+
+ Represents the contents of the /ppt directory of a .pptx file.
+ """
+
+ def add_slide(self, slide_layout: SlideLayout):
+ """Return (rId, slide) pair of a newly created blank slide.
+
+ New slide inherits appearance from `slide_layout`.
+ """
+ partname = self._next_slide_partname
+ slide_layout_part = slide_layout.part
+ slide_part = SlidePart.new(partname, self.package, slide_layout_part)
+ rId = self.relate_to(slide_part, RT.SLIDE)
+ return rId, slide_part.slide
+
+ @property
+ def core_properties(self) -> CorePropertiesPart:
+ """A |CoreProperties| object for the presentation.
+
+ Provides read/write access to the Dublin Core properties of this presentation.
+ """
+ return self.package.core_properties
+
+ def get_slide(self, slide_id: int) -> Slide | None:
+ """Return optional related |Slide| object identified by `slide_id`.
+
+ Returns |None| if no slide with `slide_id` is related to this presentation.
+ """
+ for sldId in self._element.sldIdLst:
+ if sldId.id == slide_id:
+ return self.related_part(sldId.rId).slide
+ return None
+
+ @lazyproperty
+ def notes_master(self) -> NotesMaster:
+ """
+ Return the |NotesMaster| object for this presentation. If the
+ presentation does not have a notes master, one is created from
+ a default template. The same single instance is returned on each
+ call.
+ """
+ return self.notes_master_part.notes_master
+
+ @lazyproperty
+ def notes_master_part(self) -> NotesMasterPart:
+ """Return the |NotesMasterPart| object for this presentation.
+
+ If the presentation does not have a notes master, one is created from a default template.
+ The same single instance is returned on each call.
+ """
+ try:
+ return self.part_related_by(RT.NOTES_MASTER)
+ except KeyError:
+ notes_master_part = NotesMasterPart.create_default(self.package)
+ self.relate_to(notes_master_part, RT.NOTES_MASTER)
+ return notes_master_part
+
+ @lazyproperty
+ def presentation(self):
+ """
+ A |Presentation| object providing access to the content of this
+ presentation.
+ """
+ return Presentation(self._element, self)
+
+ def related_slide(self, rId: str) -> Slide:
+ """Return |Slide| object for related |SlidePart| related by `rId`."""
+ return self.related_part(rId).slide
+
+ def related_slide_master(self, rId: str) -> SlideMaster:
+ """Return |SlideMaster| object for |SlideMasterPart| related by `rId`."""
+ return self.related_part(rId).slide_master
+
+ def rename_slide_parts(self, rIds: Iterable[str]):
+ """Assign incrementing partnames to the slide parts identified by `rIds`.
+
+ Partnames are like `/ppt/slides/slide9.xml` and are assigned in the order their id appears
+ in the `rIds` sequence. The name portion is always `slide`. The number part forms a
+ continuous sequence starting at 1 (e.g. 1, 2, ... 10, ...). The extension is always
+ `.xml`.
+ """
+ for idx, rId in enumerate(rIds):
+ slide_part = self.related_part(rId)
+ slide_part.partname = PackURI("/ppt/slides/slide%d.xml" % (idx + 1))
+
+ def save(self, path_or_stream: str | IO[bytes]):
+ """Save this presentation package to `path_or_stream`.
+
+ `path_or_stream` can be either a path to a filesystem location (a string) or a
+ file-like object.
+ """
+ self.package.save(path_or_stream)
+
+ def slide_id(self, slide_part):
+ """Return the slide-id associated with `slide_part`."""
+ for sldId in self._element.sldIdLst:
+ if self.related_part(sldId.rId) is slide_part:
+ return sldId.id
+ raise ValueError("matching slide_part not found")
+
+ @property
+ def _next_slide_partname(self):
+ """Return |PackURI| instance containing next available slide partname."""
+ sldIdLst = self._element.get_or_add_sldIdLst()
+ partname_str = "/ppt/slides/slide%d.xml" % (len(sldIdLst) + 1)
+ return PackURI(partname_str)
diff --git a/.venv/lib/python3.12/site-packages/pptx/parts/slide.py b/.venv/lib/python3.12/site-packages/pptx/parts/slide.py
new file mode 100644
index 00000000..6650564a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/parts/slide.py
@@ -0,0 +1,297 @@
+"""Slide and related objects."""
+
+from __future__ import annotations
+
+from typing import IO, TYPE_CHECKING, cast
+
+from pptx.enum.shapes import PROG_ID
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.opc.package import XmlPart
+from pptx.opc.packuri import PackURI
+from pptx.oxml.slide import CT_NotesMaster, CT_NotesSlide, CT_Slide
+from pptx.oxml.theme import CT_OfficeStyleSheet
+from pptx.parts.chart import ChartPart
+from pptx.parts.embeddedpackage import EmbeddedPackagePart
+from pptx.slide import NotesMaster, NotesSlide, Slide, SlideLayout, SlideMaster
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.chart.data import ChartData
+ from pptx.enum.chart import XL_CHART_TYPE
+ from pptx.media import Video
+ from pptx.parts.image import Image, ImagePart
+
+
+class BaseSlidePart(XmlPart):
+ """Base class for slide parts.
+
+ This includes slide, slide-layout, and slide-master parts, but also notes-slide,
+ notes-master, and handout-master parts.
+ """
+
+ _element: CT_Slide
+
+ def get_image(self, rId: str) -> Image:
+ """Return an |Image| object containing the image related to this slide by *rId*.
+
+ Raises |KeyError| if no image is related by that id, which would generally indicate a
+ corrupted .pptx file.
+ """
+ return cast("ImagePart", self.related_part(rId)).image
+
+ def get_or_add_image_part(self, image_file: str | IO[bytes]):
+ """Return `(image_part, rId)` pair corresponding to `image_file`.
+
+ The returned |ImagePart| object contains the image in `image_file` and is
+ related to this slide with the key `rId`. If either the image part or
+ relationship already exists, they are reused, otherwise they are newly created.
+ """
+ image_part = self._package.get_or_add_image_part(image_file)
+ rId = self.relate_to(image_part, RT.IMAGE)
+ return image_part, rId
+
+ @property
+ def name(self) -> str:
+ """Internal name of this slide."""
+ return self._element.cSld.name
+
+
+class NotesMasterPart(BaseSlidePart):
+ """Notes master part.
+
+ Corresponds to package file `ppt/notesMasters/notesMaster1.xml`.
+ """
+
+ @classmethod
+ def create_default(cls, package):
+ """
+ Create and return a default notes master part, including creating the
+ new theme it requires.
+ """
+ notes_master_part = cls._new(package)
+ theme_part = cls._new_theme_part(package)
+ notes_master_part.relate_to(theme_part, RT.THEME)
+ return notes_master_part
+
+ @lazyproperty
+ def notes_master(self):
+ """
+ Return the |NotesMaster| object that proxies this notes master part.
+ """
+ return NotesMaster(self._element, self)
+
+ @classmethod
+ def _new(cls, package):
+ """
+ Create and return a standalone, default notes master part based on
+ the built-in template (without any related parts, such as theme).
+ """
+ return NotesMasterPart(
+ PackURI("/ppt/notesMasters/notesMaster1.xml"),
+ CT.PML_NOTES_MASTER,
+ package,
+ CT_NotesMaster.new_default(),
+ )
+
+ @classmethod
+ def _new_theme_part(cls, package):
+ """Return new default theme-part suitable for use with a notes master."""
+ return XmlPart(
+ package.next_partname("/ppt/theme/theme%d.xml"),
+ CT.OFC_THEME,
+ package,
+ CT_OfficeStyleSheet.new_default(),
+ )
+
+
+class NotesSlidePart(BaseSlidePart):
+ """Notes slide part.
+
+ Contains the slide notes content and the layout for the slide handout page.
+ Corresponds to package file `ppt/notesSlides/notesSlide[1-9][0-9]*.xml`.
+ """
+
+ @classmethod
+ def new(cls, package, slide_part):
+ """Return new |NotesSlidePart| for the slide in `slide_part`.
+
+ The new notes-slide part is based on the (singleton) notes master and related to
+ both the notes-master part and `slide_part`. If no notes-master is present,
+ one is created based on the default template.
+ """
+ notes_master_part = package.presentation_part.notes_master_part
+ notes_slide_part = cls._add_notes_slide_part(package, slide_part, notes_master_part)
+ notes_slide = notes_slide_part.notes_slide
+ notes_slide.clone_master_placeholders(notes_master_part.notes_master)
+ return notes_slide_part
+
+ @lazyproperty
+ def notes_master(self):
+ """Return the |NotesMaster| object this notes slide inherits from."""
+ notes_master_part = self.part_related_by(RT.NOTES_MASTER)
+ return notes_master_part.notes_master
+
+ @lazyproperty
+ def notes_slide(self):
+ """Return the |NotesSlide| object that proxies this notes slide part."""
+ return NotesSlide(self._element, self)
+
+ @classmethod
+ def _add_notes_slide_part(cls, package, slide_part, notes_master_part):
+ """Create and return a new notes-slide part.
+
+ The return part is fully related, but has no shape content (i.e. placeholders
+ not cloned).
+ """
+ notes_slide_part = NotesSlidePart(
+ package.next_partname("/ppt/notesSlides/notesSlide%d.xml"),
+ CT.PML_NOTES_SLIDE,
+ package,
+ CT_NotesSlide.new(),
+ )
+ notes_slide_part.relate_to(notes_master_part, RT.NOTES_MASTER)
+ notes_slide_part.relate_to(slide_part, RT.SLIDE)
+ return notes_slide_part
+
+
+class SlidePart(BaseSlidePart):
+ """Slide part. Corresponds to package files ppt/slides/slide[1-9][0-9]*.xml."""
+
+ @classmethod
+ def new(cls, partname, package, slide_layout_part):
+ """Return newly-created blank slide part.
+
+ The new slide-part has `partname` and a relationship to `slide_layout_part`.
+ """
+ slide_part = cls(partname, CT.PML_SLIDE, package, CT_Slide.new())
+ slide_part.relate_to(slide_layout_part, RT.SLIDE_LAYOUT)
+ return slide_part
+
+ def add_chart_part(self, chart_type: XL_CHART_TYPE, chart_data: ChartData):
+ """Return str rId of new |ChartPart| object containing chart of `chart_type`.
+
+ The chart depicts `chart_data` and is related to the slide contained in this
+ part by `rId`.
+ """
+ return self.relate_to(ChartPart.new(chart_type, chart_data, self._package), RT.CHART)
+
+ def add_embedded_ole_object_part(
+ self, prog_id: PROG_ID | str, ole_object_file: str | IO[bytes]
+ ):
+ """Return rId of newly-added OLE-object part formed from `ole_object_file`."""
+ relationship_type = RT.PACKAGE if isinstance(prog_id, PROG_ID) else RT.OLE_OBJECT
+ return self.relate_to(
+ EmbeddedPackagePart.factory(
+ prog_id, self._blob_from_file(ole_object_file), self._package
+ ),
+ relationship_type,
+ )
+
+ def get_or_add_video_media_part(self, video: Video) -> tuple[str, str]:
+ """Return rIds for media and video relationships to media part.
+
+ A new |MediaPart| object is created if it does not already exist
+ (such as would occur if the same video appeared more than once in
+ a presentation). Two relationships to the media part are created,
+ one each with MEDIA and VIDEO relationship types. The need for two
+ appears to be for legacy support for an earlier (pre-Office 2010)
+ PowerPoint media embedding strategy.
+ """
+ media_part = self._package.get_or_add_media_part(video)
+ media_rId = self.relate_to(media_part, RT.MEDIA)
+ video_rId = self.relate_to(media_part, RT.VIDEO)
+ return media_rId, video_rId
+
+ @property
+ def has_notes_slide(self):
+ """
+ Return True if this slide has a notes slide, False otherwise. A notes
+ slide is created by the :attr:`notes_slide` property when one doesn't
+ exist; use this property to test for a notes slide without the
+ possible side-effect of creating one.
+ """
+ try:
+ self.part_related_by(RT.NOTES_SLIDE)
+ except KeyError:
+ return False
+ return True
+
+ @lazyproperty
+ def notes_slide(self) -> NotesSlide:
+ """The |NotesSlide| instance associated with this slide.
+
+ If the slide does not have a notes slide, a new one is created. The same single instance
+ is returned on each call.
+ """
+ try:
+ notes_slide_part = self.part_related_by(RT.NOTES_SLIDE)
+ except KeyError:
+ notes_slide_part = self._add_notes_slide_part()
+ return notes_slide_part.notes_slide
+
+ @lazyproperty
+ def slide(self):
+ """
+ The |Slide| object representing this slide part.
+ """
+ return Slide(self._element, self)
+
+ @property
+ def slide_id(self) -> int:
+ """Return the slide identifier stored in the presentation part for this slide part."""
+ presentation_part = self.package.presentation_part
+ return presentation_part.slide_id(self)
+
+ @property
+ def slide_layout(self) -> SlideLayout:
+ """|SlideLayout| object the slide in this part inherits appearance from."""
+ slide_layout_part = self.part_related_by(RT.SLIDE_LAYOUT)
+ return slide_layout_part.slide_layout
+
+ def _add_notes_slide_part(self):
+ """
+ Return a newly created |NotesSlidePart| object related to this slide
+ part. Caller is responsible for ensuring this slide doesn't already
+ have a notes slide part.
+ """
+ notes_slide_part = NotesSlidePart.new(self.package, self)
+ self.relate_to(notes_slide_part, RT.NOTES_SLIDE)
+ return notes_slide_part
+
+
+class SlideLayoutPart(BaseSlidePart):
+ """Slide layout part.
+
+ Corresponds to package files ``ppt/slideLayouts/slideLayout[1-9][0-9]*.xml``.
+ """
+
+ @lazyproperty
+ def slide_layout(self):
+ """
+ The |SlideLayout| object representing this part.
+ """
+ return SlideLayout(self._element, self)
+
+ @property
+ def slide_master(self) -> SlideMaster:
+ """Slide master from which this slide layout inherits properties."""
+ return self.part_related_by(RT.SLIDE_MASTER).slide_master
+
+
+class SlideMasterPart(BaseSlidePart):
+ """Slide master part.
+
+ Corresponds to package files ppt/slideMasters/slideMaster[1-9][0-9]*.xml.
+ """
+
+ def related_slide_layout(self, rId: str) -> SlideLayout:
+ """Return |SlideLayout| related to this slide-master by key `rId`."""
+ return self.related_part(rId).slide_layout
+
+ @lazyproperty
+ def slide_master(self):
+ """
+ The |SlideMaster| object representing this part.
+ """
+ return SlideMaster(self._element, self)
diff --git a/.venv/lib/python3.12/site-packages/pptx/presentation.py b/.venv/lib/python3.12/site-packages/pptx/presentation.py
new file mode 100644
index 00000000..a41bfd59
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/presentation.py
@@ -0,0 +1,113 @@
+"""Main presentation object."""
+
+from __future__ import annotations
+
+from typing import IO, TYPE_CHECKING, cast
+
+from pptx.shared import PartElementProxy
+from pptx.slide import SlideMasters, Slides
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.oxml.presentation import CT_Presentation, CT_SlideId
+ from pptx.parts.presentation import PresentationPart
+ from pptx.slide import NotesMaster, SlideLayouts
+ from pptx.util import Length
+
+
+class Presentation(PartElementProxy):
+ """PresentationML (PML) presentation.
+
+ Not intended to be constructed directly. Use :func:`pptx.Presentation` to open or
+ create a presentation.
+ """
+
+ _element: CT_Presentation
+ part: PresentationPart # pyright: ignore[reportIncompatibleMethodOverride]
+
+ @property
+ def core_properties(self):
+ """|CoreProperties| instance for this presentation.
+
+ Provides read/write access to the Dublin Core document properties for the presentation.
+ """
+ return self.part.core_properties
+
+ @property
+ def notes_master(self) -> NotesMaster:
+ """Instance of |NotesMaster| for this presentation.
+
+ If the presentation does not have a notes master, one is created from a default template
+ and returned. The same single instance is returned on each call.
+ """
+ return self.part.notes_master
+
+ def save(self, file: str | IO[bytes]):
+ """Writes this presentation to `file`.
+
+ `file` can be either a file-path or a file-like object open for writing bytes.
+ """
+ self.part.save(file)
+
+ @property
+ def slide_height(self) -> Length | None:
+ """Height of slides in this presentation, in English Metric Units (EMU).
+
+ Returns |None| if no slide width is defined. Read/write.
+ """
+ sldSz = self._element.sldSz
+ if sldSz is None:
+ return None
+ return sldSz.cy
+
+ @slide_height.setter
+ def slide_height(self, height: Length):
+ sldSz = self._element.get_or_add_sldSz()
+ sldSz.cy = height
+
+ @property
+ def slide_layouts(self) -> SlideLayouts:
+ """|SlideLayouts| collection belonging to the first |SlideMaster| of this presentation.
+
+ A presentation can have more than one slide master and each master will have its own set
+ of layouts. This property is a convenience for the common case where the presentation has
+ only a single slide master.
+ """
+ return self.slide_masters[0].slide_layouts
+
+ @property
+ def slide_master(self):
+ """
+ First |SlideMaster| object belonging to this presentation. Typically,
+ presentations have only a single slide master. This property provides
+ simpler access in that common case.
+ """
+ return self.slide_masters[0]
+
+ @lazyproperty
+ def slide_masters(self) -> SlideMasters:
+ """|SlideMasters| collection of slide-masters belonging to this presentation."""
+ return SlideMasters(self._element.get_or_add_sldMasterIdLst(), self)
+
+ @property
+ def slide_width(self):
+ """
+ Width of slides in this presentation, in English Metric Units (EMU).
+ Returns |None| if no slide width is defined. Read/write.
+ """
+ sldSz = self._element.sldSz
+ if sldSz is None:
+ return None
+ return sldSz.cx
+
+ @slide_width.setter
+ def slide_width(self, width: Length):
+ sldSz = self._element.get_or_add_sldSz()
+ sldSz.cx = width
+
+ @lazyproperty
+ def slides(self):
+ """|Slides| object containing the slides in this presentation."""
+ sldIdLst = self._element.get_or_add_sldIdLst()
+ self.part.rename_slide_parts([cast("CT_SlideId", sldId).rId for sldId in sldIdLst])
+ return Slides(sldIdLst, self)
diff --git a/.venv/lib/python3.12/site-packages/pptx/py.typed b/.venv/lib/python3.12/site-packages/pptx/py.typed
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/py.typed
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/__init__.py b/.venv/lib/python3.12/site-packages/pptx/shapes/__init__.py
new file mode 100644
index 00000000..332109a3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/__init__.py
@@ -0,0 +1,26 @@
+"""Objects used across sub-package."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from pptx.opc.package import XmlPart
+ from pptx.types import ProvidesPart
+
+
+class Subshape(object):
+ """Provides access to the containing part for drawing elements that occur below a shape.
+
+ Access to the part is required for example to add or drop a relationship. Provides
+ `self._parent` attribute to subclasses.
+ """
+
+ def __init__(self, parent: ProvidesPart):
+ super(Subshape, self).__init__()
+ self._parent = parent
+
+ @property
+ def part(self) -> XmlPart:
+ """The package part containing this object."""
+ return self._parent.part
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/autoshape.py b/.venv/lib/python3.12/site-packages/pptx/shapes/autoshape.py
new file mode 100644
index 00000000..c7f8cd93
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/autoshape.py
@@ -0,0 +1,355 @@
+"""Autoshape-related objects such as Shape and Adjustment."""
+
+from __future__ import annotations
+
+from numbers import Number
+from typing import TYPE_CHECKING, Iterable
+from xml.sax import saxutils
+
+from pptx.dml.fill import FillFormat
+from pptx.dml.line import LineFormat
+from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, MSO_SHAPE_TYPE
+from pptx.shapes.base import BaseShape
+from pptx.spec import autoshape_types
+from pptx.text.text import TextFrame
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.oxml.shapes.autoshape import CT_GeomGuide, CT_PresetGeometry2D, CT_Shape
+ from pptx.spec import AdjustmentValue
+ from pptx.types import ProvidesPart
+
+
+class Adjustment:
+ """An adjustment value for an autoshape.
+
+ An adjustment value corresponds to the position of an adjustment handle on an auto shape.
+ Adjustment handles are the small yellow diamond-shaped handles that appear on certain auto
+ shapes and allow the outline of the shape to be adjusted. For example, a rounded rectangle has
+ an adjustment handle that allows the radius of its corner rounding to be adjusted.
+
+ Values are |float| and generally range from 0.0 to 1.0, although the value can be negative or
+ greater than 1.0 in certain circumstances.
+ """
+
+ def __init__(self, name: str, def_val: int, actual: int | None = None):
+ super(Adjustment, self).__init__()
+ self.name = name
+ self.def_val = def_val
+ self.actual = actual
+
+ @property
+ def effective_value(self) -> float:
+ """Read/write |float| representing normalized adjustment value for this adjustment.
+
+ Actual values are a large-ish integer expressed in shape coordinates, nominally between 0
+ and 100,000. The effective value is normalized to a corresponding value nominally between
+ 0.0 and 1.0. Intuitively this represents the proportion of the width or height of the shape
+ at which the adjustment value is located from its starting point. For simple shapes such as
+ a rounded rectangle, this intuitive correspondence holds. For more complicated shapes and
+ at more extreme shape proportions (e.g. width is much greater than height), the value can
+ become negative or greater than 1.0.
+ """
+ raw_value = self.actual if self.actual is not None else self.def_val
+ return self._normalize(raw_value)
+
+ @effective_value.setter
+ def effective_value(self, value: float):
+ if not isinstance(value, Number):
+ raise ValueError(f"adjustment value must be numeric, got {repr(value)}")
+ self.actual = self._denormalize(value)
+
+ @staticmethod
+ def _denormalize(value: float) -> int:
+ """Return integer corresponding to normalized `raw_value` on unit basis of 100,000.
+
+ See Adjustment.normalize for additional details.
+ """
+ return int(value * 100000.0)
+
+ @staticmethod
+ def _normalize(raw_value: int) -> float:
+ """Return normalized value for `raw_value`.
+
+ A normalized value is a |float| between 0.0 and 1.0 for nominal raw values between 0 and
+ 100,000. Raw values less than 0 and greater than 100,000 are valid and return values
+ calculated on the same unit basis of 100,000.
+ """
+ return raw_value / 100000.0
+
+ @property
+ def val(self) -> int:
+ """Denormalized effective value.
+
+ Expressed in shape coordinates, this is suitable for using in the XML.
+ """
+ return self.actual if self.actual is not None else self.def_val
+
+
+class AdjustmentCollection:
+ """Sequence of |Adjustment| instances for an auto shape.
+
+ Each represents an available adjustment for a shape of its type. Supports `len()` and indexed
+ access, e.g. `shape.adjustments[1] = 0.15`.
+ """
+
+ def __init__(self, prstGeom: CT_PresetGeometry2D):
+ super(AdjustmentCollection, self).__init__()
+ self._adjustments_ = self._initialized_adjustments(prstGeom)
+ self._prstGeom = prstGeom
+
+ def __getitem__(self, idx: int) -> float:
+ """Provides indexed access, (e.g. 'adjustments[9]')."""
+ return self._adjustments_[idx].effective_value
+
+ def __setitem__(self, idx: int, value: float):
+ """Provides item assignment via an indexed expression, e.g. `adjustments[9] = 999.9`.
+
+ Causes all adjustment values in collection to be written to the XML.
+ """
+ self._adjustments_[idx].effective_value = value
+ self._rewrite_guides()
+
+ def _initialized_adjustments(self, prstGeom: CT_PresetGeometry2D | None) -> list[Adjustment]:
+ """Return an initialized list of adjustment values based on the contents of `prstGeom`."""
+ if prstGeom is None:
+ return []
+ davs = AutoShapeType.default_adjustment_values(prstGeom.prst)
+ adjustments = [Adjustment(name, def_val) for name, def_val in davs]
+ self._update_adjustments_with_actuals(adjustments, prstGeom.gd_lst)
+ return adjustments
+
+ def _rewrite_guides(self):
+ """Write `a:gd` elements to the XML, one for each adjustment value.
+
+ Any existing guide elements are overwritten.
+ """
+ guides = [(adj.name, adj.val) for adj in self._adjustments_]
+ self._prstGeom.rewrite_guides(guides)
+
+ @staticmethod
+ def _update_adjustments_with_actuals(
+ adjustments: Iterable[Adjustment], guides: Iterable[CT_GeomGuide]
+ ):
+ """Update |Adjustment| instances in `adjustments` with actual values held in `guides`.
+
+ `guides` is a list of `a:gd` elements. Guides with a name that does not match an adjustment
+ object are skipped.
+ """
+ adjustments_by_name = dict((adj.name, adj) for adj in adjustments)
+ for gd in guides:
+ name = gd.name
+ actual = int(gd.fmla[4:])
+ try:
+ adjustment = adjustments_by_name[name]
+ except KeyError:
+ continue
+ adjustment.actual = actual
+ return
+
+ @property
+ def _adjustments(self) -> tuple[Adjustment, ...]:
+ """Sequence of |Adjustment| objects contained in collection."""
+ return tuple(self._adjustments_)
+
+ def __len__(self):
+ """Implement built-in function len()"""
+ return len(self._adjustments_)
+
+
+class AutoShapeType:
+ """Provides access to metadata for an auto-shape of type identified by `autoshape_type_id`.
+
+ Instances are cached, so no more than one instance for a particular auto shape type is in
+ memory.
+
+ Instances provide the following attributes:
+
+ .. attribute:: autoshape_type_id
+
+ Integer uniquely identifying this auto shape type. Corresponds to a
+ value in `pptx.constants.MSO` like `MSO_SHAPE.ROUNDED_RECTANGLE`.
+
+ .. attribute:: basename
+
+ Base part of shape name for auto shapes of this type, e.g. `Rounded
+ Rectangle` becomes `Rounded Rectangle 99` when the distinguishing
+ integer is added to the shape name.
+
+ .. attribute:: prst
+
+ String identifier for this auto shape type used in the `a:prstGeom`
+ element.
+
+ """
+
+ _instances: dict[MSO_AUTO_SHAPE_TYPE, AutoShapeType] = {}
+
+ def __new__(cls, autoshape_type_id: MSO_AUTO_SHAPE_TYPE) -> AutoShapeType:
+ """Only create new instance on first call for content_type.
+
+ After that, use cached instance.
+ """
+ # -- if there's not a matching instance in the cache, create one --
+ if autoshape_type_id not in cls._instances:
+ inst = super(AutoShapeType, cls).__new__(cls)
+ cls._instances[autoshape_type_id] = inst
+ # -- return the instance; note that __init__() gets called either way --
+ return cls._instances[autoshape_type_id]
+
+ def __init__(self, autoshape_type_id: MSO_AUTO_SHAPE_TYPE):
+ """Initialize attributes from constant values in `pptx.spec`."""
+ # -- skip loading if this instance is from the cache --
+ if hasattr(self, "_loaded"):
+ return
+ # -- raise on bad autoshape_type_id --
+ if autoshape_type_id not in autoshape_types:
+ raise KeyError(
+ "no autoshape type with id '%s' in pptx.spec.autoshape_types" % autoshape_type_id
+ )
+ # -- otherwise initialize new instance --
+ autoshape_type = autoshape_types[autoshape_type_id]
+ self._autoshape_type_id = autoshape_type_id
+ self._basename = autoshape_type["basename"]
+ self._loaded = True
+
+ @property
+ def autoshape_type_id(self) -> MSO_AUTO_SHAPE_TYPE:
+ """MSO_AUTO_SHAPE_TYPE enumeration member identifying this auto shape type."""
+ return self._autoshape_type_id
+
+ @property
+ def basename(self) -> str:
+ """Base of shape name for this auto shape type.
+
+ A shape name is like "Rounded Rectangle 7" and appears as an XML attribute for example at
+ `p:sp/p:nvSpPr/p:cNvPr{name}`. This basename value is the name less the distinguishing
+ integer. This value is escaped because at least one autoshape-type name includes double
+ quotes ('"No" Symbol').
+ """
+ return saxutils.escape(self._basename, {'"': "&quot;"})
+
+ @classmethod
+ def default_adjustment_values(cls, prst: MSO_AUTO_SHAPE_TYPE) -> tuple[AdjustmentValue, ...]:
+ """Sequence of (name, value) pair adjustment value defaults for `prst` autoshape-type."""
+ return autoshape_types[prst]["avLst"]
+
+ @classmethod
+ def id_from_prst(cls, prst: str) -> MSO_AUTO_SHAPE_TYPE:
+ """Select auto shape type with matching `prst`.
+
+ e.g. `MSO_SHAPE.RECTANGLE` corresponding to preset geometry keyword `"rect"`.
+ """
+ return MSO_AUTO_SHAPE_TYPE.from_xml(prst)
+
+ @property
+ def prst(self):
+ """
+ Preset geometry identifier string for this auto shape. Used in the
+ `prst` attribute of `a:prstGeom` element to specify the geometry
+ to be used in rendering the shape, for example `'roundRect'`.
+ """
+ return MSO_AUTO_SHAPE_TYPE.to_xml(self._autoshape_type_id)
+
+
+class Shape(BaseShape):
+ """A shape that can appear on a slide.
+
+ Corresponds to the `p:sp` element that can appear in any of the slide-type parts
+ (slide, slideLayout, slideMaster, notesPage, notesMaster, handoutMaster).
+ """
+
+ def __init__(self, sp: CT_Shape, parent: ProvidesPart):
+ super(Shape, self).__init__(sp, parent)
+ self._sp = sp
+
+ @lazyproperty
+ def adjustments(self) -> AdjustmentCollection:
+ """Read-only reference to |AdjustmentCollection| instance for this shape."""
+ return AdjustmentCollection(self._sp.prstGeom)
+
+ @property
+ def auto_shape_type(self):
+ """Enumeration value identifying the type of this auto shape.
+
+ Like `MSO_SHAPE.ROUNDED_RECTANGLE`. Raises |ValueError| if this shape is not an auto shape.
+ """
+ if not self._sp.is_autoshape:
+ raise ValueError("shape is not an auto shape")
+ return self._sp.prst
+
+ @lazyproperty
+ def fill(self):
+ """|FillFormat| instance for this shape.
+
+ Provides access to fill properties such as fill color.
+ """
+ return FillFormat.from_fill_parent(self._sp.spPr)
+
+ def get_or_add_ln(self):
+ """Return the `a:ln` element containing the line format properties XML for this shape."""
+ return self._sp.get_or_add_ln()
+
+ @property
+ def has_text_frame(self) -> bool:
+ """|True| if this shape can contain text. Always |True| for an AutoShape."""
+ return True
+
+ @lazyproperty
+ def line(self):
+ """|LineFormat| instance for this shape.
+
+ Provides access to line properties such as line color.
+ """
+ return LineFormat(self)
+
+ @property
+ def ln(self):
+ """The `a:ln` element containing the line format properties such as line color and width.
+
+ |None| if no `a:ln` element is present.
+ """
+ return self._sp.ln
+
+ @property
+ def shape_type(self) -> MSO_SHAPE_TYPE:
+ """Unique integer identifying the type of this shape, like `MSO_SHAPE_TYPE.TEXT_BOX`."""
+ if self.is_placeholder:
+ return MSO_SHAPE_TYPE.PLACEHOLDER
+ if self._sp.has_custom_geometry:
+ return MSO_SHAPE_TYPE.FREEFORM
+ if self._sp.is_autoshape:
+ return MSO_SHAPE_TYPE.AUTO_SHAPE
+ if self._sp.is_textbox:
+ return MSO_SHAPE_TYPE.TEXT_BOX
+ raise NotImplementedError("Shape instance of unrecognized shape type")
+
+ @property
+ def text(self) -> str:
+ """Read/write. Text in shape as a single string.
+
+ The returned string will contain a newline character (`"\\n"`) separating each paragraph
+ and a vertical-tab (`"\\v"`) character for each line break (soft carriage return) in the
+ shape's text.
+
+ Assignment to `text` replaces any text previously contained in the shape, along with any
+ paragraph or font formatting applied to it. A newline character (`"\\n"`) in the assigned
+ text causes a new paragraph to be started. A vertical-tab (`"\\v"`) character in the
+ assigned text causes a line-break (soft carriage-return) to be inserted. (The vertical-tab
+ character appears in clipboard text copied from PowerPoint as its str encoding of
+ line-breaks.)
+ """
+ return self.text_frame.text
+
+ @text.setter
+ def text(self, text: str):
+ self.text_frame.text = text
+
+ @property
+ def text_frame(self):
+ """|TextFrame| instance for this shape.
+
+ Contains the text of the shape and provides access to text formatting properties.
+ """
+ txBody = self._sp.get_or_add_txBody()
+ return TextFrame(txBody, self)
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/base.py b/.venv/lib/python3.12/site-packages/pptx/shapes/base.py
new file mode 100644
index 00000000..75123502
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/base.py
@@ -0,0 +1,244 @@
+"""Base shape-related objects such as BaseShape."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+
+from pptx.action import ActionSetting
+from pptx.dml.effect import ShadowFormat
+from pptx.shared import ElementProxy
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER
+ from pptx.oxml.shapes import ShapeElement
+ from pptx.oxml.shapes.shared import CT_Placeholder
+ from pptx.parts.slide import BaseSlidePart
+ from pptx.types import ProvidesPart
+ from pptx.util import Length
+
+
+class BaseShape(object):
+ """Base class for shape objects.
+
+ Subclasses include |Shape|, |Picture|, and |GraphicFrame|.
+ """
+
+ def __init__(self, shape_elm: ShapeElement, parent: ProvidesPart):
+ super().__init__()
+ self._element = shape_elm
+ self._parent = parent
+
+ def __eq__(self, other: object) -> bool:
+ """|True| if this shape object proxies the same element as *other*.
+
+ Equality for proxy objects is defined as referring to the same XML element, whether or not
+ they are the same proxy object instance.
+ """
+ if not isinstance(other, BaseShape):
+ return False
+ return self._element is other._element
+
+ def __ne__(self, other: object) -> bool:
+ if not isinstance(other, BaseShape):
+ return True
+ return self._element is not other._element
+
+ @lazyproperty
+ def click_action(self) -> ActionSetting:
+ """|ActionSetting| instance providing access to click behaviors.
+
+ Click behaviors are hyperlink-like behaviors including jumping to a hyperlink (web page)
+ or to another slide in the presentation. The click action is that defined on the overall
+ shape, not a run of text within the shape. An |ActionSetting| object is always returned,
+ even when no click behavior is defined on the shape.
+ """
+ cNvPr = self._element._nvXxPr.cNvPr # pyright: ignore[reportPrivateUsage]
+ return ActionSetting(cNvPr, self)
+
+ @property
+ def element(self) -> ShapeElement:
+ """`lxml` element for this shape, e.g. a CT_Shape instance.
+
+ Note that manipulating this element improperly can produce an invalid presentation file.
+ Make sure you know what you're doing if you use this to change the underlying XML.
+ """
+ return self._element
+
+ @property
+ def has_chart(self) -> bool:
+ """|True| if this shape is a graphic frame containing a chart object.
+
+ |False| otherwise. When |True|, the chart object can be accessed using the ``.chart``
+ property.
+ """
+ # This implementation is unconditionally False, the True version is
+ # on GraphicFrame subclass.
+ return False
+
+ @property
+ def has_table(self) -> bool:
+ """|True| if this shape is a graphic frame containing a table object.
+
+ |False| otherwise. When |True|, the table object can be accessed using the ``.table``
+ property.
+ """
+ # This implementation is unconditionally False, the True version is
+ # on GraphicFrame subclass.
+ return False
+
+ @property
+ def has_text_frame(self) -> bool:
+ """|True| if this shape can contain text."""
+ # overridden on Shape to return True. Only <p:sp> has text frame
+ return False
+
+ @property
+ def height(self) -> Length:
+ """Read/write. Integer distance between top and bottom extents of shape in EMUs."""
+ return self._element.cy
+
+ @height.setter
+ def height(self, value: Length):
+ self._element.cy = value
+
+ @property
+ def is_placeholder(self) -> bool:
+ """True if this shape is a placeholder.
+
+ A shape is a placeholder if it has a <p:ph> element.
+ """
+ return self._element.has_ph_elm
+
+ @property
+ def left(self) -> Length:
+ """Integer distance of the left edge of this shape from the left edge of the slide.
+
+ Read/write. Expressed in English Metric Units (EMU)
+ """
+ return self._element.x
+
+ @left.setter
+ def left(self, value: Length):
+ self._element.x = value
+
+ @property
+ def name(self) -> str:
+ """Name of this shape, e.g. 'Picture 7'."""
+ return self._element.shape_name
+
+ @name.setter
+ def name(self, value: str):
+ self._element._nvXxPr.cNvPr.name = value # pyright: ignore[reportPrivateUsage]
+
+ @property
+ def part(self) -> BaseSlidePart:
+ """The package part containing this shape.
+
+ A |BaseSlidePart| subclass in this case. Access to a slide part should only be required if
+ you are extending the behavior of |pp| API objects.
+ """
+ return cast("BaseSlidePart", self._parent.part)
+
+ @property
+ def placeholder_format(self) -> _PlaceholderFormat:
+ """Provides access to placeholder-specific properties such as placeholder type.
+
+ Raises |ValueError| on access if the shape is not a placeholder.
+ """
+ ph = self._element.ph
+ if ph is None:
+ raise ValueError("shape is not a placeholder")
+ return _PlaceholderFormat(ph)
+
+ @property
+ def rotation(self) -> float:
+ """Degrees of clockwise rotation.
+
+ Read/write float. Negative values can be assigned to indicate counter-clockwise rotation,
+ e.g. assigning -45.0 will change setting to 315.0.
+ """
+ return self._element.rot
+
+ @rotation.setter
+ def rotation(self, value: float):
+ self._element.rot = value
+
+ @lazyproperty
+ def shadow(self) -> ShadowFormat:
+ """|ShadowFormat| object providing access to shadow for this shape.
+
+ A |ShadowFormat| object is always returned, even when no shadow is
+ explicitly defined on this shape (i.e. it inherits its shadow
+ behavior).
+ """
+ return ShadowFormat(self._element.spPr)
+
+ @property
+ def shape_id(self) -> int:
+ """Read-only positive integer identifying this shape.
+
+ The id of a shape is unique among all shapes on a slide.
+ """
+ return self._element.shape_id
+
+ @property
+ def shape_type(self) -> MSO_SHAPE_TYPE:
+ """A member of MSO_SHAPE_TYPE classifying this shape by type.
+
+ Like ``MSO_SHAPE_TYPE.CHART``. Must be implemented by subclasses.
+ """
+ raise NotImplementedError(f"{type(self).__name__} does not implement `.shape_type`")
+
+ @property
+ def top(self) -> Length:
+ """Distance from the top edge of the slide to the top edge of this shape.
+
+ Read/write. Expressed in English Metric Units (EMU)
+ """
+ return self._element.y
+
+ @top.setter
+ def top(self, value: Length):
+ self._element.y = value
+
+ @property
+ def width(self) -> Length:
+ """Distance between left and right extents of this shape.
+
+ Read/write. Expressed in English Metric Units (EMU).
+ """
+ return self._element.cx
+
+ @width.setter
+ def width(self, value: Length):
+ self._element.cx = value
+
+
+class _PlaceholderFormat(ElementProxy):
+ """Provides properties specific to placeholders, such as the placeholder type.
+
+ Accessed via the :attr:`~.BaseShape.placeholder_format` property of a placeholder shape,
+ """
+
+ def __init__(self, element: CT_Placeholder):
+ super().__init__(element)
+ self._ph = element
+
+ @property
+ def element(self) -> CT_Placeholder:
+ """The `p:ph` element proxied by this object."""
+ return self._ph
+
+ @property
+ def idx(self) -> int:
+ """Integer placeholder 'idx' attribute."""
+ return self._ph.idx
+
+ @property
+ def type(self) -> PP_PLACEHOLDER:
+ """Placeholder type.
+
+ A member of the :ref:`PpPlaceholderType` enumeration, e.g. PP_PLACEHOLDER.CHART
+ """
+ return self._ph.type
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/connector.py b/.venv/lib/python3.12/site-packages/pptx/shapes/connector.py
new file mode 100644
index 00000000..070b080d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/connector.py
@@ -0,0 +1,297 @@
+"""Connector (line) shape and related objects.
+
+A connector is a line shape having end-points that can be connected to other
+objects (but not to other connectors). A connector can be straight, have
+elbows, or can be curved.
+"""
+
+from __future__ import annotations
+
+from pptx.dml.line import LineFormat
+from pptx.enum.shapes import MSO_SHAPE_TYPE
+from pptx.shapes.base import BaseShape
+from pptx.util import Emu, lazyproperty
+
+
+class Connector(BaseShape):
+ """Connector (line) shape.
+
+ A connector is a linear shape having end-points that can be connected to
+ other objects (but not to other connectors). A connector can be straight,
+ have elbows, or can be curved.
+ """
+
+ def begin_connect(self, shape, cxn_pt_idx):
+ """
+ **EXPERIMENTAL** - *The current implementation only works properly
+ with rectangular shapes, such as pictures and rectangles. Use with
+ other shape types may cause unexpected visual alignment of the
+ connected end-point and could lead to a load error if cxn_pt_idx
+ exceeds the connection point count available on the connected shape.
+ That said, a quick test should reveal what to expect when using this
+ method with other shape types.*
+
+ Connect the beginning of this connector to *shape* at the connection
+ point specified by *cxn_pt_idx*. Each shape has zero or more
+ connection points and they are identified by index, starting with 0.
+ Generally, the first connection point of a shape is at the top center
+ of its bounding box and numbering proceeds counter-clockwise from
+ there. However this is only a convention and may vary, especially
+ with non built-in shapes.
+ """
+ self._connect_begin_to(shape, cxn_pt_idx)
+ self._move_begin_to_cxn(shape, cxn_pt_idx)
+
+ @property
+ def begin_x(self):
+ """
+ Return the X-position of the begin point of this connector, in
+ English Metric Units (as a |Length| object).
+ """
+ cxnSp = self._element
+ x, cx, flipH = cxnSp.x, cxnSp.cx, cxnSp.flipH
+ begin_x = x + cx if flipH else x
+ return Emu(begin_x)
+
+ @begin_x.setter
+ def begin_x(self, value):
+ cxnSp = self._element
+ x, cx, flipH, new_x = cxnSp.x, cxnSp.cx, cxnSp.flipH, int(value)
+
+ if flipH:
+ old_x = x + cx
+ dx = abs(new_x - old_x)
+ if new_x >= old_x:
+ cxnSp.cx = cx + dx
+ elif dx <= cx:
+ cxnSp.cx = cx - dx
+ else:
+ cxnSp.flipH = False
+ cxnSp.x = new_x
+ cxnSp.cx = dx - cx
+ else:
+ dx = abs(new_x - x)
+ if new_x <= x:
+ cxnSp.x = new_x
+ cxnSp.cx = cx + dx
+ elif dx <= cx:
+ cxnSp.x = new_x
+ cxnSp.cx = cx - dx
+ else:
+ cxnSp.flipH = True
+ cxnSp.x = x + cx
+ cxnSp.cx = dx - cx
+
+ @property
+ def begin_y(self):
+ """
+ Return the Y-position of the begin point of this connector, in
+ English Metric Units (as a |Length| object).
+ """
+ cxnSp = self._element
+ y, cy, flipV = cxnSp.y, cxnSp.cy, cxnSp.flipV
+ begin_y = y + cy if flipV else y
+ return Emu(begin_y)
+
+ @begin_y.setter
+ def begin_y(self, value):
+ cxnSp = self._element
+ y, cy, flipV, new_y = cxnSp.y, cxnSp.cy, cxnSp.flipV, int(value)
+
+ if flipV:
+ old_y = y + cy
+ dy = abs(new_y - old_y)
+ if new_y >= old_y:
+ cxnSp.cy = cy + dy
+ elif dy <= cy:
+ cxnSp.cy = cy - dy
+ else:
+ cxnSp.flipV = False
+ cxnSp.y = new_y
+ cxnSp.cy = dy - cy
+ else:
+ dy = abs(new_y - y)
+ if new_y <= y:
+ cxnSp.y = new_y
+ cxnSp.cy = cy + dy
+ elif dy <= cy:
+ cxnSp.y = new_y
+ cxnSp.cy = cy - dy
+ else:
+ cxnSp.flipV = True
+ cxnSp.y = y + cy
+ cxnSp.cy = dy - cy
+
+ def end_connect(self, shape, cxn_pt_idx):
+ """
+ **EXPERIMENTAL** - *The current implementation only works properly
+ with rectangular shapes, such as pictures and rectangles. Use with
+ other shape types may cause unexpected visual alignment of the
+ connected end-point and could lead to a load error if cxn_pt_idx
+ exceeds the connection point count available on the connected shape.
+ That said, a quick test should reveal what to expect when using this
+ method with other shape types.*
+
+ Connect the ending of this connector to *shape* at the connection
+ point specified by *cxn_pt_idx*.
+ """
+ self._connect_end_to(shape, cxn_pt_idx)
+ self._move_end_to_cxn(shape, cxn_pt_idx)
+
+ @property
+ def end_x(self):
+ """
+ Return the X-position of the end point of this connector, in English
+ Metric Units (as a |Length| object).
+ """
+ cxnSp = self._element
+ x, cx, flipH = cxnSp.x, cxnSp.cx, cxnSp.flipH
+ end_x = x if flipH else x + cx
+ return Emu(end_x)
+
+ @end_x.setter
+ def end_x(self, value):
+ cxnSp = self._element
+ x, cx, flipH, new_x = cxnSp.x, cxnSp.cx, cxnSp.flipH, int(value)
+
+ if flipH:
+ dx = abs(new_x - x)
+ if new_x <= x:
+ cxnSp.x = new_x
+ cxnSp.cx = cx + dx
+ elif dx <= cx:
+ cxnSp.x = new_x
+ cxnSp.cx = cx - dx
+ else:
+ cxnSp.flipH = False
+ cxnSp.x = x + cx
+ cxnSp.cx = dx - cx
+ else:
+ old_x = x + cx
+ dx = abs(new_x - old_x)
+ if new_x >= old_x:
+ cxnSp.cx = cx + dx
+ elif dx <= cx:
+ cxnSp.cx = cx - dx
+ else:
+ cxnSp.flipH = True
+ cxnSp.x = new_x
+ cxnSp.cx = dx - cx
+
+ @property
+ def end_y(self):
+ """
+ Return the Y-position of the end point of this connector, in English
+ Metric Units (as a |Length| object).
+ """
+ cxnSp = self._element
+ y, cy, flipV = cxnSp.y, cxnSp.cy, cxnSp.flipV
+ end_y = y if flipV else y + cy
+ return Emu(end_y)
+
+ @end_y.setter
+ def end_y(self, value):
+ cxnSp = self._element
+ y, cy, flipV, new_y = cxnSp.y, cxnSp.cy, cxnSp.flipV, int(value)
+
+ if flipV:
+ dy = abs(new_y - y)
+ if new_y <= y:
+ cxnSp.y = new_y
+ cxnSp.cy = cy + dy
+ elif dy <= cy:
+ cxnSp.y = new_y
+ cxnSp.cy = cy - dy
+ else:
+ cxnSp.flipV = False
+ cxnSp.y = y + cy
+ cxnSp.cy = dy - cy
+ else:
+ old_y = y + cy
+ dy = abs(new_y - old_y)
+ if new_y >= old_y:
+ cxnSp.cy = cy + dy
+ elif dy <= cy:
+ cxnSp.cy = cy - dy
+ else:
+ cxnSp.flipV = True
+ cxnSp.y = new_y
+ cxnSp.cy = dy - cy
+
+ def get_or_add_ln(self):
+ """Helper method required by |LineFormat|."""
+ return self._element.spPr.get_or_add_ln()
+
+ @lazyproperty
+ def line(self):
+ """|LineFormat| instance for this connector.
+
+ Provides access to line properties such as line color, width, and
+ line style.
+ """
+ return LineFormat(self)
+
+ @property
+ def ln(self):
+ """Helper method required by |LineFormat|.
+
+ The ``<a:ln>`` element containing the line format properties such as
+ line color and width. |None| if no `<a:ln>` element is present.
+ """
+ return self._element.spPr.ln
+
+ @property
+ def shape_type(self):
+ """Member of `MSO_SHAPE_TYPE` identifying the type of this shape.
+
+ Unconditionally `MSO_SHAPE_TYPE.LINE` for a `Connector` object.
+ """
+ return MSO_SHAPE_TYPE.LINE
+
+ def _connect_begin_to(self, shape, cxn_pt_idx):
+ """
+ Add or update a stCxn element for this connector that connects its
+ begin point to the connection point of *shape* specified by
+ *cxn_pt_idx*.
+ """
+ cNvCxnSpPr = self._element.nvCxnSpPr.cNvCxnSpPr
+ stCxn = cNvCxnSpPr.get_or_add_stCxn()
+ stCxn.id = shape.shape_id
+ stCxn.idx = cxn_pt_idx
+
+ def _connect_end_to(self, shape, cxn_pt_idx):
+ """
+ Add or update an endCxn element for this connector that connects its
+ end point to the connection point of *shape* specified by
+ *cxn_pt_idx*.
+ """
+ cNvCxnSpPr = self._element.nvCxnSpPr.cNvCxnSpPr
+ endCxn = cNvCxnSpPr.get_or_add_endCxn()
+ endCxn.id = shape.shape_id
+ endCxn.idx = cxn_pt_idx
+
+ def _move_begin_to_cxn(self, shape, cxn_pt_idx):
+ """
+ Move the begin point of this connector to coordinates of the
+ connection point of *shape* specified by *cxn_pt_idx*.
+ """
+ x, y, cx, cy = shape.left, shape.top, shape.width, shape.height
+ self.begin_x, self.begin_y = {
+ 0: (int(x + cx / 2), y),
+ 1: (x, int(y + cy / 2)),
+ 2: (int(x + cx / 2), y + cy),
+ 3: (x + cx, int(y + cy / 2)),
+ }[cxn_pt_idx]
+
+ def _move_end_to_cxn(self, shape, cxn_pt_idx):
+ """
+ Move the end point of this connector to the coordinates of the
+ connection point of *shape* specified by *cxn_pt_idx*.
+ """
+ x, y, cx, cy = shape.left, shape.top, shape.width, shape.height
+ self.end_x, self.end_y = {
+ 0: (int(x + cx / 2), y),
+ 1: (x, int(y + cy / 2)),
+ 2: (int(x + cx / 2), y + cy),
+ 3: (x + cx, int(y + cy / 2)),
+ }[cxn_pt_idx]
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/freeform.py b/.venv/lib/python3.12/site-packages/pptx/shapes/freeform.py
new file mode 100644
index 00000000..afe87385
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/freeform.py
@@ -0,0 +1,337 @@
+"""Objects related to construction of freeform shapes."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Iterable, Iterator, Sequence
+
+from pptx.util import Emu, lazyproperty
+
+if TYPE_CHECKING:
+ from typing_extensions import TypeAlias
+
+ from pptx.oxml.shapes.autoshape import (
+ CT_Path2D,
+ CT_Path2DClose,
+ CT_Path2DLineTo,
+ CT_Path2DMoveTo,
+ CT_Shape,
+ )
+ from pptx.shapes.shapetree import _BaseGroupShapes # pyright: ignore[reportPrivateUsage]
+ from pptx.util import Length
+
+CT_DrawingOperation: TypeAlias = "CT_Path2DClose | CT_Path2DLineTo | CT_Path2DMoveTo"
+DrawingOperation: TypeAlias = "_LineSegment | _MoveTo | _Close"
+
+
+class FreeformBuilder(Sequence[DrawingOperation]):
+ """Allows a freeform shape to be specified and created.
+
+ The initial pen position is provided on construction. From there, drawing proceeds using
+ successive calls to draw line segments. The freeform shape may be closed by calling the
+ :meth:`close` method.
+
+ A shape may have more than one contour, in which case overlapping areas are "subtracted". A
+ contour is a sequence of line segments beginning with a "move-to" operation. A move-to
+ operation is automatically inserted in each new freeform; additional move-to ops can be
+ inserted with the `.move_to()` method.
+ """
+
+ def __init__(
+ self,
+ shapes: _BaseGroupShapes,
+ start_x: Length,
+ start_y: Length,
+ x_scale: float,
+ y_scale: float,
+ ):
+ super(FreeformBuilder, self).__init__()
+ self._shapes = shapes
+ self._start_x = start_x
+ self._start_y = start_y
+ self._x_scale = x_scale
+ self._y_scale = y_scale
+
+ def __getitem__( # pyright: ignore[reportIncompatibleMethodOverride]
+ self, idx: int
+ ) -> DrawingOperation:
+ return self._drawing_operations.__getitem__(idx)
+
+ def __iter__(self) -> Iterator[DrawingOperation]:
+ return self._drawing_operations.__iter__()
+
+ def __len__(self):
+ return self._drawing_operations.__len__()
+
+ @classmethod
+ def new(
+ cls,
+ shapes: _BaseGroupShapes,
+ start_x: float,
+ start_y: float,
+ x_scale: float,
+ y_scale: float,
+ ):
+ """Return a new |FreeformBuilder| object.
+
+ The initial pen location is specified (in local coordinates) by
+ (`start_x`, `start_y`).
+ """
+ return cls(shapes, Emu(int(round(start_x))), Emu(int(round(start_y))), x_scale, y_scale)
+
+ def add_line_segments(self, vertices: Iterable[tuple[float, float]], close: bool = True):
+ """Add a straight line segment to each point in `vertices`.
+
+ `vertices` must be an iterable of (x, y) pairs (2-tuples). Each x and y value is rounded
+ to the nearest integer before use. The optional `close` parameter determines whether the
+ resulting contour is `closed` or left `open`.
+
+ Returns this |FreeformBuilder| object so it can be used in chained calls.
+ """
+ for x, y in vertices:
+ self._add_line_segment(x, y)
+ if close:
+ self._add_close()
+ return self
+
+ def convert_to_shape(self, origin_x: Length = Emu(0), origin_y: Length = Emu(0)):
+ """Return new freeform shape positioned relative to specified offset.
+
+ `origin_x` and `origin_y` locate the origin of the local coordinate system in slide
+ coordinates (EMU), perhaps most conveniently by use of a |Length| object.
+
+ Note that this method may be called more than once to add multiple shapes of the same
+ geometry in different locations on the slide.
+ """
+ sp = self._add_freeform_sp(origin_x, origin_y)
+ path = self._start_path(sp)
+ for drawing_operation in self:
+ drawing_operation.apply_operation_to(path)
+ return self._shapes._shape_factory(sp) # pyright: ignore[reportPrivateUsage]
+
+ def move_to(self, x: float, y: float):
+ """Move pen to (x, y) (local coordinates) without drawing line.
+
+ Returns this |FreeformBuilder| object so it can be used in chained calls.
+ """
+ self._drawing_operations.append(_MoveTo.new(self, x, y))
+ return self
+
+ @property
+ def shape_offset_x(self) -> Length:
+ """Return x distance of shape origin from local coordinate origin.
+
+ The returned integer represents the leftmost extent of the freeform shape, in local
+ coordinates. Note that the bounding box of the shape need not start at the local origin.
+ """
+ min_x = self._start_x
+ for drawing_operation in self:
+ if isinstance(drawing_operation, _Close):
+ continue
+ min_x = min(min_x, drawing_operation.x)
+ return Emu(min_x)
+
+ @property
+ def shape_offset_y(self) -> Length:
+ """Return y distance of shape origin from local coordinate origin.
+
+ The returned integer represents the topmost extent of the freeform shape, in local
+ coordinates. Note that the bounding box of the shape need not start at the local origin.
+ """
+ min_y = self._start_y
+ for drawing_operation in self:
+ if isinstance(drawing_operation, _Close):
+ continue
+ min_y = min(min_y, drawing_operation.y)
+ return Emu(min_y)
+
+ def _add_close(self):
+ """Add a close |_Close| operation to the drawing sequence."""
+ self._drawing_operations.append(_Close.new())
+
+ def _add_freeform_sp(self, origin_x: Length, origin_y: Length):
+ """Add a freeform `p:sp` element having no drawing elements.
+
+ `origin_x` and `origin_y` are specified in slide coordinates, and represent the location
+ of the local coordinates origin on the slide.
+ """
+ spTree = self._shapes._spTree # pyright: ignore[reportPrivateUsage]
+ return spTree.add_freeform_sp(
+ origin_x + self._left, origin_y + self._top, self._width, self._height
+ )
+
+ def _add_line_segment(self, x: float, y: float) -> None:
+ """Add a |_LineSegment| operation to the drawing sequence."""
+ self._drawing_operations.append(_LineSegment.new(self, x, y))
+
+ @lazyproperty
+ def _drawing_operations(self) -> list[DrawingOperation]:
+ """Return the sequence of drawing operation objects for freeform."""
+ return []
+
+ @property
+ def _dx(self) -> Length:
+ """Return width of this shape's path in local units."""
+ min_x = max_x = self._start_x
+ for drawing_operation in self:
+ if isinstance(drawing_operation, _Close):
+ continue
+ min_x = min(min_x, drawing_operation.x)
+ max_x = max(max_x, drawing_operation.x)
+ return Emu(max_x - min_x)
+
+ @property
+ def _dy(self) -> Length:
+ """Return integer height of this shape's path in local units."""
+ min_y = max_y = self._start_y
+ for drawing_operation in self:
+ if isinstance(drawing_operation, _Close):
+ continue
+ min_y = min(min_y, drawing_operation.y)
+ max_y = max(max_y, drawing_operation.y)
+ return Emu(max_y - min_y)
+
+ @property
+ def _height(self):
+ """Return vertical size of this shape's path in slide coordinates.
+
+ This value is based on the actual extents of the shape and does not include any
+ positioning offset.
+ """
+ return int(round(self._dy * self._y_scale))
+
+ @property
+ def _left(self):
+ """Return leftmost extent of this shape's path in slide coordinates.
+
+ Note that this value does not include any positioning offset; it assumes the drawing
+ (local) coordinate origin is at (0, 0) on the slide.
+ """
+ return int(round(self.shape_offset_x * self._x_scale))
+
+ def _local_to_shape(self, local_x: Length, local_y: Length) -> tuple[Length, Length]:
+ """Translate local coordinates point to shape coordinates.
+
+ Shape coordinates have the same unit as local coordinates, but are offset such that the
+ origin of the shape coordinate system (0, 0) is located at the top-left corner of the
+ shape bounding box.
+ """
+ return Emu(local_x - self.shape_offset_x), Emu(local_y - self.shape_offset_y)
+
+ def _start_path(self, sp: CT_Shape) -> CT_Path2D:
+ """Return a newly created `a:path` element added to `sp`.
+
+ The returned `a:path` element has an `a:moveTo` element representing the shape starting
+ point as its only child.
+ """
+ path = sp.add_path(w=self._dx, h=self._dy)
+ path.add_moveTo(*self._local_to_shape(self._start_x, self._start_y))
+ return path
+
+ @property
+ def _top(self):
+ """Return topmost extent of this shape's path in slide coordinates.
+
+ Note that this value does not include any positioning offset; it assumes the drawing
+ (local) coordinate origin is located at slide coordinates (0, 0) (top-left corner of
+ slide).
+ """
+ return int(round(self.shape_offset_y * self._y_scale))
+
+ @property
+ def _width(self):
+ """Return width of this shape's path in slide coordinates.
+
+ This value is based on the actual extents of the shape path and does not include any
+ positioning offset.
+ """
+ return int(round(self._dx * self._x_scale))
+
+
+class _BaseDrawingOperation(object):
+ """Base class for freeform drawing operations.
+
+ A drawing operation has at least one location (x, y) in local coordinates.
+ """
+
+ def __init__(self, freeform_builder: FreeformBuilder, x: Length, y: Length):
+ super(_BaseDrawingOperation, self).__init__()
+ self._freeform_builder = freeform_builder
+ self._x = x
+ self._y = y
+
+ def apply_operation_to(self, path: CT_Path2D) -> CT_DrawingOperation:
+ """Add the XML element(s) implementing this operation to `path`.
+
+ Must be implemented by each subclass.
+ """
+ raise NotImplementedError("must be implemented by each subclass")
+
+ @property
+ def x(self) -> Length:
+ """Return the horizontal (x) target location of this operation.
+
+ The returned value is an integer in local coordinates.
+ """
+ return self._x
+
+ @property
+ def y(self) -> Length:
+ """Return the vertical (y) target location of this operation.
+
+ The returned value is an integer in local coordinates.
+ """
+ return self._y
+
+
+class _Close(object):
+ """Specifies adding a `<a:close/>` element to the current contour."""
+
+ @classmethod
+ def new(cls) -> _Close:
+ """Return a new _Close object."""
+ return cls()
+
+ def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DClose:
+ """Add `a:close` element to `path`."""
+ return path.add_close()
+
+
+class _LineSegment(_BaseDrawingOperation):
+ """Specifies a straight line segment ending at the specified point."""
+
+ @classmethod
+ def new(cls, freeform_builder: FreeformBuilder, x: float, y: float) -> _LineSegment:
+ """Return a new _LineSegment object ending at point *(x, y)*.
+
+ Both `x` and `y` are rounded to the nearest integer before use.
+ """
+ return cls(freeform_builder, Emu(int(round(x))), Emu(int(round(y))))
+
+ def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DLineTo:
+ """Add `a:lnTo` element to `path` for this line segment.
+
+ Returns the `a:lnTo` element newly added to the path.
+ """
+ return path.add_lnTo(
+ Emu(self._x - self._freeform_builder.shape_offset_x),
+ Emu(self._y - self._freeform_builder.shape_offset_y),
+ )
+
+
+class _MoveTo(_BaseDrawingOperation):
+ """Specifies a new pen position."""
+
+ @classmethod
+ def new(cls, freeform_builder: FreeformBuilder, x: float, y: float) -> _MoveTo:
+ """Return a new _MoveTo object for move to point `(x, y)`.
+
+ Both `x` and `y` are rounded to the nearest integer before use.
+ """
+ return cls(freeform_builder, Emu(int(round(x))), Emu(int(round(y))))
+
+ def apply_operation_to(self, path: CT_Path2D) -> CT_Path2DMoveTo:
+ """Add `a:moveTo` element to `path` for this line segment."""
+ return path.add_moveTo(
+ Emu(self._x - self._freeform_builder.shape_offset_x),
+ Emu(self._y - self._freeform_builder.shape_offset_y),
+ )
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/graphfrm.py b/.venv/lib/python3.12/site-packages/pptx/shapes/graphfrm.py
new file mode 100644
index 00000000..c0ed2bba
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/graphfrm.py
@@ -0,0 +1,166 @@
+"""Graphic Frame shape and related objects.
+
+A graphic frame is a common container for table, chart, smart art, and media
+objects.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+
+from pptx.enum.shapes import MSO_SHAPE_TYPE
+from pptx.shapes.base import BaseShape
+from pptx.shared import ParentedElementProxy
+from pptx.spec import (
+ GRAPHIC_DATA_URI_CHART,
+ GRAPHIC_DATA_URI_OLEOBJ,
+ GRAPHIC_DATA_URI_TABLE,
+)
+from pptx.table import Table
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.chart.chart import Chart
+ from pptx.dml.effect import ShadowFormat
+ from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectData, CT_GraphicalObjectFrame
+ from pptx.parts.chart import ChartPart
+ from pptx.parts.slide import BaseSlidePart
+ from pptx.types import ProvidesPart
+
+
+class GraphicFrame(BaseShape):
+ """Container shape for table, chart, smart art, and media objects.
+
+ Corresponds to a `p:graphicFrame` element in the shape tree.
+ """
+
+ def __init__(self, graphicFrame: CT_GraphicalObjectFrame, parent: ProvidesPart):
+ super().__init__(graphicFrame, parent)
+ self._graphicFrame = graphicFrame
+
+ @property
+ def chart(self) -> Chart:
+ """The |Chart| object containing the chart in this graphic frame.
+
+ Raises |ValueError| if this graphic frame does not contain a chart.
+ """
+ if not self.has_chart:
+ raise ValueError("shape does not contain a chart")
+ return self.chart_part.chart
+
+ @property
+ def chart_part(self) -> ChartPart:
+ """The |ChartPart| object containing the chart in this graphic frame."""
+ chart_rId = self._graphicFrame.chart_rId
+ if chart_rId is None:
+ raise ValueError("this graphic frame does not contain a chart")
+ return cast("ChartPart", self.part.related_part(chart_rId))
+
+ @property
+ def has_chart(self) -> bool:
+ """|True| if this graphic frame contains a chart object. |False| otherwise.
+
+ When |True|, the chart object can be accessed using the `.chart` property.
+ """
+ return self._graphicFrame.graphicData_uri == GRAPHIC_DATA_URI_CHART
+
+ @property
+ def has_table(self) -> bool:
+ """|True| if this graphic frame contains a table object, |False| otherwise.
+
+ When |True|, the table object can be accessed using the `.table` property.
+ """
+ return self._graphicFrame.graphicData_uri == GRAPHIC_DATA_URI_TABLE
+
+ @property
+ def ole_format(self) -> _OleFormat:
+ """_OleFormat object for this graphic-frame shape.
+
+ Raises `ValueError` on a GraphicFrame instance that does not contain an OLE object.
+
+ An shape that contains an OLE object will have `.shape_type` of either
+ `EMBEDDED_OLE_OBJECT` or `LINKED_OLE_OBJECT`.
+ """
+ if not self._graphicFrame.has_oleobj:
+ raise ValueError("not an OLE-object shape")
+ return _OleFormat(self._graphicFrame.graphicData, self._parent)
+
+ @lazyproperty
+ def shadow(self) -> ShadowFormat:
+ """Unconditionally raises |NotImplementedError|.
+
+ Access to the shadow effect for graphic-frame objects is content-specific (i.e. different
+ for charts, tables, etc.) and has not yet been implemented.
+ """
+ raise NotImplementedError("shadow property on GraphicFrame not yet supported")
+
+ @property
+ def shape_type(self) -> MSO_SHAPE_TYPE:
+ """Optional member of `MSO_SHAPE_TYPE` identifying the type of this shape.
+
+ Possible values are `MSO_SHAPE_TYPE.CHART`, `MSO_SHAPE_TYPE.TABLE`,
+ `MSO_SHAPE_TYPE.EMBEDDED_OLE_OBJECT`, `MSO_SHAPE_TYPE.LINKED_OLE_OBJECT`.
+
+ This value is `None` when none of these four types apply, for example when the shape
+ contains SmartArt.
+ """
+ graphicData_uri = self._graphicFrame.graphicData_uri
+ if graphicData_uri == GRAPHIC_DATA_URI_CHART:
+ return MSO_SHAPE_TYPE.CHART
+ elif graphicData_uri == GRAPHIC_DATA_URI_TABLE:
+ return MSO_SHAPE_TYPE.TABLE
+ elif graphicData_uri == GRAPHIC_DATA_URI_OLEOBJ:
+ return (
+ MSO_SHAPE_TYPE.EMBEDDED_OLE_OBJECT
+ if self._graphicFrame.is_embedded_ole_obj
+ else MSO_SHAPE_TYPE.LINKED_OLE_OBJECT
+ )
+ else:
+ return None # pyright: ignore[reportReturnType]
+
+ @property
+ def table(self) -> Table:
+ """The |Table| object contained in this graphic frame.
+
+ Raises |ValueError| if this graphic frame does not contain a table.
+ """
+ if not self.has_table:
+ raise ValueError("shape does not contain a table")
+ tbl = self._graphicFrame.graphic.graphicData.tbl
+ return Table(tbl, self)
+
+
+class _OleFormat(ParentedElementProxy):
+ """Provides attributes on an embedded OLE object."""
+
+ part: BaseSlidePart # pyright: ignore[reportIncompatibleMethodOverride]
+
+ def __init__(self, graphicData: CT_GraphicalObjectData, parent: ProvidesPart):
+ super().__init__(graphicData, parent)
+ self._graphicData = graphicData
+
+ @property
+ def blob(self) -> bytes | None:
+ """Optional bytes of OLE object, suitable for loading or saving as a file.
+
+ This value is `None` if the embedded object does not represent a "file".
+ """
+ blob_rId = self._graphicData.blob_rId
+ if blob_rId is None:
+ return None
+ return self.part.related_part(blob_rId).blob
+
+ @property
+ def prog_id(self) -> str | None:
+ """str "progId" attribute of this embedded OLE object.
+
+ The progId is a str like "Excel.Sheet.12" that identifies the "file-type" of the embedded
+ object, or perhaps more precisely, the application (aka. "server" in OLE parlance) to be
+ used to open this object.
+ """
+ return self._graphicData.progId
+
+ @property
+ def show_as_icon(self) -> bool | None:
+ """True when OLE object should appear as an icon (rather than preview)."""
+ return self._graphicData.showAsIcon
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/group.py b/.venv/lib/python3.12/site-packages/pptx/shapes/group.py
new file mode 100644
index 00000000..71737585
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/group.py
@@ -0,0 +1,69 @@
+"""GroupShape and related objects."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from pptx.dml.effect import ShadowFormat
+from pptx.enum.shapes import MSO_SHAPE_TYPE
+from pptx.shapes.base import BaseShape
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.action import ActionSetting
+ from pptx.oxml.shapes.groupshape import CT_GroupShape
+ from pptx.shapes.shapetree import GroupShapes
+ from pptx.types import ProvidesPart
+
+
+class GroupShape(BaseShape):
+ """A shape that acts as a container for other shapes."""
+
+ def __init__(self, grpSp: CT_GroupShape, parent: ProvidesPart):
+ super().__init__(grpSp, parent)
+ self._grpSp = grpSp
+
+ @lazyproperty
+ def click_action(self) -> ActionSetting:
+ """Unconditionally raises `TypeError`.
+
+ A group shape cannot have a click action or hover action.
+ """
+ raise TypeError("a group shape cannot have a click action")
+
+ @property
+ def has_text_frame(self) -> bool:
+ """Unconditionally |False|.
+
+ A group shape does not have a textframe and cannot itself contain text. This does not
+ impact the ability of shapes contained by the group to each have their own text.
+ """
+ return False
+
+ @lazyproperty
+ def shadow(self) -> ShadowFormat:
+ """|ShadowFormat| object representing shadow effect for this group.
+
+ A |ShadowFormat| object is always returned, even when no shadow is explicitly defined on
+ this group shape (i.e. when the group inherits its shadow behavior).
+ """
+ return ShadowFormat(self._grpSp.grpSpPr)
+
+ @property
+ def shape_type(self) -> MSO_SHAPE_TYPE:
+ """Member of :ref:`MsoShapeType` identifying the type of this shape.
+
+ Unconditionally `MSO_SHAPE_TYPE.GROUP` in this case
+ """
+ return MSO_SHAPE_TYPE.GROUP
+
+ @lazyproperty
+ def shapes(self) -> GroupShapes:
+ """|GroupShapes| object for this group.
+
+ The |GroupShapes| object provides access to the group's member shapes and provides methods
+ for adding new ones.
+ """
+ from pptx.shapes.shapetree import GroupShapes
+
+ return GroupShapes(self._element, self)
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/picture.py b/.venv/lib/python3.12/site-packages/pptx/shapes/picture.py
new file mode 100644
index 00000000..59182860
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/picture.py
@@ -0,0 +1,203 @@
+"""Shapes based on the `p:pic` element, including Picture and Movie."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from pptx.dml.line import LineFormat
+from pptx.enum.shapes import MSO_SHAPE, MSO_SHAPE_TYPE, PP_MEDIA_TYPE
+from pptx.shapes.base import BaseShape
+from pptx.shared import ParentedElementProxy
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.oxml.shapes.picture import CT_Picture
+ from pptx.oxml.shapes.shared import CT_LineProperties
+ from pptx.types import ProvidesPart
+
+
+class _BasePicture(BaseShape):
+ """Base class for shapes based on a `p:pic` element."""
+
+ def __init__(self, pic: CT_Picture, parent: ProvidesPart):
+ super(_BasePicture, self).__init__(pic, parent)
+ self._pic = pic
+
+ @property
+ def crop_bottom(self) -> float:
+ """|float| representing relative portion cropped from shape bottom.
+
+ Read/write. 1.0 represents 100%. For example, 25% is represented by 0.25. Negative values
+ are valid as are values greater than 1.0.
+ """
+ return self._pic.srcRect_b
+
+ @crop_bottom.setter
+ def crop_bottom(self, value: float):
+ self._pic.srcRect_b = value
+
+ @property
+ def crop_left(self) -> float:
+ """|float| representing relative portion cropped from left of shape.
+
+ Read/write. 1.0 represents 100%. A negative value extends the side beyond the image
+ boundary.
+ """
+ return self._pic.srcRect_l
+
+ @crop_left.setter
+ def crop_left(self, value: float):
+ self._pic.srcRect_l = value
+
+ @property
+ def crop_right(self) -> float:
+ """|float| representing relative portion cropped from right of shape.
+
+ Read/write. 1.0 represents 100%.
+ """
+ return self._pic.srcRect_r
+
+ @crop_right.setter
+ def crop_right(self, value: float):
+ self._pic.srcRect_r = value
+
+ @property
+ def crop_top(self) -> float:
+ """|float| representing relative portion cropped from shape top.
+
+ Read/write. 1.0 represents 100%.
+ """
+ return self._pic.srcRect_t
+
+ @crop_top.setter
+ def crop_top(self, value: float):
+ self._pic.srcRect_t = value
+
+ def get_or_add_ln(self):
+ """Return the `a:ln` element for this `p:pic`-based image.
+
+ The `a:ln` element contains the line format properties XML.
+ """
+ return self._pic.get_or_add_ln()
+
+ @lazyproperty
+ def line(self) -> LineFormat:
+ """Provides access to properties of the picture outline, such as its color and width."""
+ return LineFormat(self)
+
+ @property
+ def ln(self) -> CT_LineProperties | None:
+ """The `a:ln` element for this `p:pic`.
+
+ Contains the line format properties such as line color and width. |None| if no `a:ln`
+ element is present.
+ """
+ return self._pic.ln
+
+
+class Movie(_BasePicture):
+ """A movie shape, one that places a video on a slide.
+
+ Like |Picture|, a movie shape is based on the `p:pic` element. A movie is composed of a video
+ and a *poster frame*, the placeholder image that represents the video before it is played.
+ """
+
+ @lazyproperty
+ def media_format(self) -> _MediaFormat:
+ """The |_MediaFormat| object for this movie.
+
+ The |_MediaFormat| object provides access to formatting properties for the movie.
+ """
+ return _MediaFormat(self._pic, self)
+
+ @property
+ def media_type(self) -> PP_MEDIA_TYPE:
+ """Member of :ref:`PpMediaType` describing this shape.
+
+ The return value is unconditionally `PP_MEDIA_TYPE.MOVIE` in this case.
+ """
+ return PP_MEDIA_TYPE.MOVIE
+
+ @property
+ def poster_frame(self):
+ """Return |Image| object containing poster frame for this movie.
+
+ Returns |None| if this movie has no poster frame (uncommon).
+ """
+ slide_part, rId = self.part, self._pic.blip_rId
+ if rId is None:
+ return None
+ return slide_part.get_image(rId)
+
+ @property
+ def shape_type(self) -> MSO_SHAPE_TYPE:
+ """Return member of :ref:`MsoShapeType` describing this shape.
+
+ The return value is unconditionally `MSO_SHAPE_TYPE.MEDIA` in this
+ case.
+ """
+ return MSO_SHAPE_TYPE.MEDIA
+
+
+class Picture(_BasePicture):
+ """A picture shape, one that places an image on a slide.
+
+ Based on the `p:pic` element.
+ """
+
+ @property
+ def auto_shape_type(self) -> MSO_SHAPE | None:
+ """Member of MSO_SHAPE indicating masking shape.
+
+ A picture can be masked by any of the so-called "auto-shapes" available in PowerPoint,
+ such as an ellipse or triangle. When a picture is masked by a shape, the shape assumes the
+ same dimensions as the picture and the portion of the picture outside the shape boundaries
+ does not appear. Note the default value for a newly-inserted picture is
+ `MSO_AUTO_SHAPE_TYPE.RECTANGLE`, which performs no cropping because the extents of the
+ rectangle exactly correspond to the extents of the picture.
+
+ The available shapes correspond to the members of :ref:`MsoAutoShapeType`.
+
+ The return value can also be |None|, indicating the picture either has no geometry (not
+ expected) or has custom geometry, like a freeform shape. A picture with no geometry will
+ have no visible representation on the slide, although it can be selected. This is because
+ without geometry, there is no "inside-the-shape" for it to appear in.
+ """
+ prstGeom = self._pic.spPr.prstGeom
+ if prstGeom is None: # ---generally means cropped with freeform---
+ return None
+ return prstGeom.prst
+
+ @auto_shape_type.setter
+ def auto_shape_type(self, member: MSO_SHAPE):
+ MSO_SHAPE.validate(member)
+ spPr = self._pic.spPr
+ prstGeom = spPr.prstGeom
+ if prstGeom is None:
+ spPr._remove_custGeom() # pyright: ignore[reportPrivateUsage]
+ prstGeom = spPr._add_prstGeom() # pyright: ignore[reportPrivateUsage]
+ prstGeom.prst = member
+
+ @property
+ def image(self):
+ """The |Image| object for this picture.
+
+ Provides access to the properties and bytes of the image in this picture shape.
+ """
+ slide_part, rId = self.part, self._pic.blip_rId
+ if rId is None:
+ raise ValueError("no embedded image")
+ return slide_part.get_image(rId)
+
+ @property
+ def shape_type(self) -> MSO_SHAPE_TYPE:
+ """Unconditionally `MSO_SHAPE_TYPE.PICTURE` in this case."""
+ return MSO_SHAPE_TYPE.PICTURE
+
+
+class _MediaFormat(ParentedElementProxy):
+ """Provides access to formatting properties for a Media object.
+
+ Media format properties are things like start point, volume, and
+ compression type.
+ """
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/placeholder.py b/.venv/lib/python3.12/site-packages/pptx/shapes/placeholder.py
new file mode 100644
index 00000000..c44837be
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/placeholder.py
@@ -0,0 +1,407 @@
+"""Placeholder-related objects.
+
+Specific to shapes having a `p:ph` element. A placeholder has distinct behaviors
+depending on whether it appears on a slide, layout, or master. Hence there is a
+non-trivial class inheritance structure.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER
+from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame
+from pptx.oxml.shapes.picture import CT_Picture
+from pptx.shapes.autoshape import Shape
+from pptx.shapes.graphfrm import GraphicFrame
+from pptx.shapes.picture import Picture
+from pptx.util import Emu
+
+if TYPE_CHECKING:
+ from pptx.oxml.shapes.autoshape import CT_Shape
+
+
+class _InheritsDimensions(object):
+ """
+ Mixin class that provides inherited dimension behavior. Specifically,
+ left, top, width, and height report the value from the layout placeholder
+ where they would have otherwise reported |None|. This behavior is
+ distinctive to placeholders. :meth:`_base_placeholder` must be overridden
+ by all subclasses to provide lookup of the appropriate base placeholder
+ to inherit from.
+ """
+
+ @property
+ def height(self):
+ """
+ The effective height of this placeholder shape; its directly-applied
+ height if it has one, otherwise the height of its parent layout
+ placeholder.
+ """
+ return self._effective_value("height")
+
+ @height.setter
+ def height(self, value):
+ self._element.cy = value
+
+ @property
+ def left(self):
+ """
+ The effective left of this placeholder shape; its directly-applied
+ left if it has one, otherwise the left of its parent layout
+ placeholder.
+ """
+ return self._effective_value("left")
+
+ @left.setter
+ def left(self, value):
+ self._element.x = value
+
+ @property
+ def shape_type(self):
+ """
+ Member of :ref:`MsoShapeType` specifying the type of this shape.
+ Unconditionally ``MSO_SHAPE_TYPE.PLACEHOLDER`` in this case.
+ Read-only.
+ """
+ return MSO_SHAPE_TYPE.PLACEHOLDER
+
+ @property
+ def top(self):
+ """
+ The effective top of this placeholder shape; its directly-applied
+ top if it has one, otherwise the top of its parent layout
+ placeholder.
+ """
+ return self._effective_value("top")
+
+ @top.setter
+ def top(self, value):
+ self._element.y = value
+
+ @property
+ def width(self):
+ """
+ The effective width of this placeholder shape; its directly-applied
+ width if it has one, otherwise the width of its parent layout
+ placeholder.
+ """
+ return self._effective_value("width")
+
+ @width.setter
+ def width(self, value):
+ self._element.cx = value
+
+ @property
+ def _base_placeholder(self):
+ """
+ Return the layout or master placeholder shape this placeholder
+ inherits from. Not to be confused with an instance of
+ |BasePlaceholder| (necessarily).
+ """
+ raise NotImplementedError("Must be implemented by all subclasses.")
+
+ def _effective_value(self, attr_name):
+ """
+ The effective value of *attr_name* on this placeholder shape; its
+ directly-applied value if it has one, otherwise the value on the
+ layout placeholder it inherits from.
+ """
+ directly_applied_value = getattr(super(_InheritsDimensions, self), attr_name)
+ if directly_applied_value is not None:
+ return directly_applied_value
+ return self._inherited_value(attr_name)
+
+ def _inherited_value(self, attr_name):
+ """
+ Return the attribute value, e.g. 'width' of the base placeholder this
+ placeholder inherits from.
+ """
+ base_placeholder = self._base_placeholder
+ if base_placeholder is None:
+ return None
+ inherited_value = getattr(base_placeholder, attr_name)
+ return inherited_value
+
+
+class _BaseSlidePlaceholder(_InheritsDimensions, Shape):
+ """Base class for placeholders on slides.
+
+ Provides common behaviors such as inherited dimensions.
+ """
+
+ @property
+ def is_placeholder(self):
+ """
+ Boolean indicating whether this shape is a placeholder.
+ Unconditionally |True| in this case.
+ """
+ return True
+
+ @property
+ def shape_type(self):
+ """
+ Member of :ref:`MsoShapeType` specifying the type of this shape.
+ Unconditionally ``MSO_SHAPE_TYPE.PLACEHOLDER`` in this case.
+ Read-only.
+ """
+ return MSO_SHAPE_TYPE.PLACEHOLDER
+
+ @property
+ def _base_placeholder(self):
+ """
+ Return the layout placeholder this slide placeholder inherits from.
+ Not to be confused with an instance of |BasePlaceholder|
+ (necessarily).
+ """
+ layout, idx = self.part.slide_layout, self._element.ph_idx
+ return layout.placeholders.get(idx=idx)
+
+ def _replace_placeholder_with(self, element):
+ """
+ Substitute *element* for this placeholder element in the shapetree.
+ This placeholder's `._element` attribute is set to |None| and its
+ original element is free for garbage collection. Any attribute access
+ (including a method call) on this placeholder after this call raises
+ |AttributeError|.
+ """
+ element._nvXxPr.nvPr._insert_ph(self._element.ph)
+ self._element.addprevious(element)
+ self._element.getparent().remove(self._element)
+ self._element = None
+
+
+class BasePlaceholder(Shape):
+ """
+ NOTE: This class is deprecated and will be removed from a future release
+ along with the properties *idx*, *orient*, *ph_type*, and *sz*. The *idx*
+ property will be available via the .placeholder_format property. The
+ others will be accessed directly from the oxml layer as they are only
+ used for internal purposes.
+
+ Base class for placeholder subclasses that differentiate the varying
+ behaviors of placeholders on a master, layout, and slide.
+ """
+
+ @property
+ def idx(self):
+ """
+ Integer placeholder 'idx' attribute, e.g. 0
+ """
+ return self._sp.ph_idx
+
+ @property
+ def orient(self):
+ """
+ Placeholder orientation, e.g. ST_Direction.HORZ
+ """
+ return self._sp.ph_orient
+
+ @property
+ def ph_type(self):
+ """
+ Placeholder type, e.g. PP_PLACEHOLDER.CENTER_TITLE
+ """
+ return self._sp.ph_type
+
+ @property
+ def sz(self):
+ """
+ Placeholder 'sz' attribute, e.g. ST_PlaceholderSize.FULL
+ """
+ return self._sp.ph_sz
+
+
+class LayoutPlaceholder(_InheritsDimensions, Shape):
+ """Placeholder shape on a slide layout.
+
+ Provides differentiated behavior for slide layout placeholders, in particular, inheriting
+ shape properties from the master placeholder having the same type, when a matching one exists.
+ """
+
+ element: CT_Shape # pyright: ignore[reportIncompatibleMethodOverride]
+
+ @property
+ def _base_placeholder(self):
+ """
+ Return the master placeholder this layout placeholder inherits from.
+ """
+ base_ph_type = {
+ PP_PLACEHOLDER.BODY: PP_PLACEHOLDER.BODY,
+ PP_PLACEHOLDER.CHART: PP_PLACEHOLDER.BODY,
+ PP_PLACEHOLDER.BITMAP: PP_PLACEHOLDER.BODY,
+ PP_PLACEHOLDER.CENTER_TITLE: PP_PLACEHOLDER.TITLE,
+ PP_PLACEHOLDER.ORG_CHART: PP_PLACEHOLDER.BODY,
+ PP_PLACEHOLDER.DATE: PP_PLACEHOLDER.DATE,
+ PP_PLACEHOLDER.FOOTER: PP_PLACEHOLDER.FOOTER,
+ PP_PLACEHOLDER.MEDIA_CLIP: PP_PLACEHOLDER.BODY,
+ PP_PLACEHOLDER.OBJECT: PP_PLACEHOLDER.BODY,
+ PP_PLACEHOLDER.PICTURE: PP_PLACEHOLDER.BODY,
+ PP_PLACEHOLDER.SLIDE_NUMBER: PP_PLACEHOLDER.SLIDE_NUMBER,
+ PP_PLACEHOLDER.SUBTITLE: PP_PLACEHOLDER.BODY,
+ PP_PLACEHOLDER.TABLE: PP_PLACEHOLDER.BODY,
+ PP_PLACEHOLDER.TITLE: PP_PLACEHOLDER.TITLE,
+ }[self._element.ph_type]
+ slide_master = self.part.slide_master
+ return slide_master.placeholders.get(base_ph_type, None)
+
+
+class MasterPlaceholder(BasePlaceholder):
+ """Placeholder shape on a slide master."""
+
+ element: CT_Shape # pyright: ignore[reportIncompatibleMethodOverride]
+
+
+class NotesSlidePlaceholder(_InheritsDimensions, Shape):
+ """
+ Placeholder shape on a notes slide. Inherits shape properties from the
+ placeholder on the notes master that has the same type (e.g. 'body').
+ """
+
+ @property
+ def _base_placeholder(self):
+ """
+ Return the notes master placeholder this notes slide placeholder
+ inherits from, or |None| if no placeholder of the matching type is
+ present.
+ """
+ notes_master = self.part.notes_master
+ ph_type = self.element.ph_type
+ return notes_master.placeholders.get(ph_type=ph_type)
+
+
+class SlidePlaceholder(_BaseSlidePlaceholder):
+ """
+ Placeholder shape on a slide. Inherits shape properties from its
+ corresponding slide layout placeholder.
+ """
+
+
+class ChartPlaceholder(_BaseSlidePlaceholder):
+ """Placeholder shape that can only accept a chart."""
+
+ def insert_chart(self, chart_type, chart_data):
+ """
+ Return a |PlaceholderGraphicFrame| object containing a new chart of
+ *chart_type* depicting *chart_data* and having the same position and
+ size as this placeholder. *chart_type* is one of the
+ :ref:`XlChartType` enumeration values. *chart_data* is a |ChartData|
+ object populated with the categories and series values for the chart.
+ Note that the new |Chart| object is not returned directly. The chart
+ object may be accessed using the
+ :attr:`~.PlaceholderGraphicFrame.chart` property of the returned
+ |PlaceholderGraphicFrame| object.
+ """
+ rId = self.part.add_chart_part(chart_type, chart_data)
+ graphicFrame = self._new_chart_graphicFrame(
+ rId, self.left, self.top, self.width, self.height
+ )
+ self._replace_placeholder_with(graphicFrame)
+ return PlaceholderGraphicFrame(graphicFrame, self._parent)
+
+ def _new_chart_graphicFrame(self, rId, x, y, cx, cy):
+ """
+ Return a newly created `p:graphicFrame` element having the specified
+ position and size and containing the chart identified by *rId*.
+ """
+ id_, name = self.shape_id, self.name
+ return CT_GraphicalObjectFrame.new_chart_graphicFrame(id_, name, rId, x, y, cx, cy)
+
+
+class PicturePlaceholder(_BaseSlidePlaceholder):
+ """Placeholder shape that can only accept a picture."""
+
+ def insert_picture(self, image_file):
+ """Return a |PlaceholderPicture| object depicting the image in `image_file`.
+
+ `image_file` may be either a path (string) or a file-like object. The image is
+ cropped to fill the entire space of the placeholder. A |PlaceholderPicture|
+ object has all the properties and methods of a |Picture| shape except that the
+ value of its :attr:`~._BaseSlidePlaceholder.shape_type` property is
+ `MSO_SHAPE_TYPE.PLACEHOLDER` instead of `MSO_SHAPE_TYPE.PICTURE`.
+ """
+ pic = self._new_placeholder_pic(image_file)
+ self._replace_placeholder_with(pic)
+ return PlaceholderPicture(pic, self._parent)
+
+ def _new_placeholder_pic(self, image_file):
+ """
+ Return a new `p:pic` element depicting the image in *image_file*,
+ suitable for use as a placeholder. In particular this means not
+ having an `a:xfrm` element, allowing its extents to be inherited from
+ its layout placeholder.
+ """
+ rId, desc, image_size = self._get_or_add_image(image_file)
+ shape_id, name = self.shape_id, self.name
+ pic = CT_Picture.new_ph_pic(shape_id, name, desc, rId)
+ pic.crop_to_fit(image_size, (self.width, self.height))
+ return pic
+
+ def _get_or_add_image(self, image_file):
+ """
+ Return an (rId, description, image_size) 3-tuple identifying the
+ related image part containing *image_file* and describing the image.
+ """
+ image_part, rId = self.part.get_or_add_image_part(image_file)
+ desc, image_size = image_part.desc, image_part._px_size
+ return rId, desc, image_size
+
+
+class PlaceholderGraphicFrame(GraphicFrame):
+ """
+ Placeholder shape populated with a table, chart, or smart art.
+ """
+
+ @property
+ def is_placeholder(self):
+ """
+ Boolean indicating whether this shape is a placeholder.
+ Unconditionally |True| in this case.
+ """
+ return True
+
+
+class PlaceholderPicture(_InheritsDimensions, Picture):
+ """
+ Placeholder shape populated with a picture.
+ """
+
+ @property
+ def _base_placeholder(self):
+ """
+ Return the layout placeholder this picture placeholder inherits from.
+ """
+ layout, idx = self.part.slide_layout, self._element.ph_idx
+ return layout.placeholders.get(idx=idx)
+
+
+class TablePlaceholder(_BaseSlidePlaceholder):
+ """Placeholder shape that can only accept a table."""
+
+ def insert_table(self, rows, cols):
+ """Return |PlaceholderGraphicFrame| object containing a `rows` by `cols` table.
+
+ The position and width of the table are those of the placeholder and its height
+ is proportional to the number of rows. A |PlaceholderGraphicFrame| object has
+ all the properties and methods of a |GraphicFrame| shape except that the value
+ of its :attr:`~._BaseSlidePlaceholder.shape_type` property is unconditionally
+ `MSO_SHAPE_TYPE.PLACEHOLDER`. Note that the return value is not the new table
+ but rather *contains* the new table. The table can be accessed using the
+ :attr:`~.PlaceholderGraphicFrame.table` property of the returned
+ |PlaceholderGraphicFrame| object.
+ """
+ graphicFrame = self._new_placeholder_table(rows, cols)
+ self._replace_placeholder_with(graphicFrame)
+ return PlaceholderGraphicFrame(graphicFrame, self._parent)
+
+ def _new_placeholder_table(self, rows, cols):
+ """
+ Return a newly added `p:graphicFrame` element containing an empty
+ table with *rows* rows and *cols* columns, positioned at the location
+ of this placeholder and having its same width. The table's height is
+ determined by the number of rows.
+ """
+ shape_id, name, height = self.shape_id, self.name, Emu(rows * 370840)
+ return CT_GraphicalObjectFrame.new_table_graphicFrame(
+ shape_id, name, rows, cols, self.left, self.top, self.width, height
+ )
diff --git a/.venv/lib/python3.12/site-packages/pptx/shapes/shapetree.py b/.venv/lib/python3.12/site-packages/pptx/shapes/shapetree.py
new file mode 100644
index 00000000..29623f1f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shapes/shapetree.py
@@ -0,0 +1,1190 @@
+"""The shape tree, the structure that holds a slide's shapes."""
+
+from __future__ import annotations
+
+import io
+import os
+from typing import IO, TYPE_CHECKING, Callable, Iterable, Iterator, cast
+
+from pptx.enum.shapes import PP_PLACEHOLDER, PROG_ID
+from pptx.media import SPEAKER_IMAGE_BYTES, Video
+from pptx.opc.constants import CONTENT_TYPE as CT
+from pptx.oxml.ns import qn
+from pptx.oxml.shapes.autoshape import CT_Shape
+from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame
+from pptx.oxml.shapes.picture import CT_Picture
+from pptx.oxml.simpletypes import ST_Direction
+from pptx.shapes.autoshape import AutoShapeType, Shape
+from pptx.shapes.base import BaseShape
+from pptx.shapes.connector import Connector
+from pptx.shapes.freeform import FreeformBuilder
+from pptx.shapes.graphfrm import GraphicFrame
+from pptx.shapes.group import GroupShape
+from pptx.shapes.picture import Movie, Picture
+from pptx.shapes.placeholder import (
+ ChartPlaceholder,
+ LayoutPlaceholder,
+ MasterPlaceholder,
+ NotesSlidePlaceholder,
+ PicturePlaceholder,
+ PlaceholderGraphicFrame,
+ PlaceholderPicture,
+ SlidePlaceholder,
+ TablePlaceholder,
+)
+from pptx.shared import ParentedElementProxy
+from pptx.util import Emu, lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.chart.chart import Chart
+ from pptx.chart.data import ChartData
+ from pptx.enum.chart import XL_CHART_TYPE
+ from pptx.enum.shapes import MSO_CONNECTOR_TYPE, MSO_SHAPE
+ from pptx.oxml.shapes import ShapeElement
+ from pptx.oxml.shapes.connector import CT_Connector
+ from pptx.oxml.shapes.groupshape import CT_GroupShape
+ from pptx.parts.image import ImagePart
+ from pptx.parts.slide import SlidePart
+ from pptx.slide import Slide, SlideLayout
+ from pptx.types import ProvidesPart
+ from pptx.util import Length
+
+# +-- _BaseShapes
+# | |
+# | +-- _BaseGroupShapes
+# | | |
+# | | +-- GroupShapes
+# | | |
+# | | +-- SlideShapes
+# | |
+# | +-- LayoutShapes
+# | |
+# | +-- MasterShapes
+# | |
+# | +-- NotesSlideShapes
+# | |
+# | +-- BasePlaceholders
+# | |
+# | +-- LayoutPlaceholders
+# | |
+# | +-- MasterPlaceholders
+# | |
+# | +-- NotesSlidePlaceholders
+# |
+# +-- SlidePlaceholders
+
+
+class _BaseShapes(ParentedElementProxy):
+ """Base class for a shape collection appearing in a slide-type object.
+
+ Subclasses include Slide, SlideLayout, and SlideMaster. Provides common methods.
+ """
+
+ def __init__(self, spTree: CT_GroupShape, parent: ProvidesPart):
+ super(_BaseShapes, self).__init__(spTree, parent)
+ self._spTree = spTree
+ self._cached_max_shape_id = None
+
+ def __getitem__(self, idx: int) -> BaseShape:
+ """Return shape at `idx` in sequence, e.g. `shapes[2]`."""
+ shape_elms = list(self._iter_member_elms())
+ try:
+ shape_elm = shape_elms[idx]
+ except IndexError:
+ raise IndexError("shape index out of range")
+ return self._shape_factory(shape_elm)
+
+ def __iter__(self) -> Iterator[BaseShape]:
+ """Generate a reference to each shape in the collection, in sequence."""
+ for shape_elm in self._iter_member_elms():
+ yield self._shape_factory(shape_elm)
+
+ def __len__(self) -> int:
+ """Return count of shapes in this shape tree.
+
+ A group shape contributes 1 to the total, without regard to the number of shapes contained
+ in the group.
+ """
+ shape_elms = list(self._iter_member_elms())
+ return len(shape_elms)
+
+ def clone_placeholder(self, placeholder: LayoutPlaceholder) -> None:
+ """Add a new placeholder shape based on `placeholder`."""
+ sp = placeholder.element
+ ph_type, orient, sz, idx = (sp.ph_type, sp.ph_orient, sp.ph_sz, sp.ph_idx)
+ id_ = self._next_shape_id
+ name = self._next_ph_name(ph_type, id_, orient)
+ self._spTree.add_placeholder(id_, name, ph_type, orient, sz, idx)
+
+ def ph_basename(self, ph_type: PP_PLACEHOLDER) -> str:
+ """Return the base name for a placeholder of `ph_type` in this shape collection.
+
+ There is some variance between slide types, for example a notes slide uses a different
+ name for the body placeholder, so this method can be overriden by subclasses.
+ """
+ return {
+ PP_PLACEHOLDER.BITMAP: "ClipArt Placeholder",
+ PP_PLACEHOLDER.BODY: "Text Placeholder",
+ PP_PLACEHOLDER.CENTER_TITLE: "Title",
+ PP_PLACEHOLDER.CHART: "Chart Placeholder",
+ PP_PLACEHOLDER.DATE: "Date Placeholder",
+ PP_PLACEHOLDER.FOOTER: "Footer Placeholder",
+ PP_PLACEHOLDER.HEADER: "Header Placeholder",
+ PP_PLACEHOLDER.MEDIA_CLIP: "Media Placeholder",
+ PP_PLACEHOLDER.OBJECT: "Content Placeholder",
+ PP_PLACEHOLDER.ORG_CHART: "SmartArt Placeholder",
+ PP_PLACEHOLDER.PICTURE: "Picture Placeholder",
+ PP_PLACEHOLDER.SLIDE_NUMBER: "Slide Number Placeholder",
+ PP_PLACEHOLDER.SUBTITLE: "Subtitle",
+ PP_PLACEHOLDER.TABLE: "Table Placeholder",
+ PP_PLACEHOLDER.TITLE: "Title",
+ }[ph_type]
+
+ @property
+ def turbo_add_enabled(self) -> bool:
+ """True if "turbo-add" mode is enabled. Read/Write.
+
+ EXPERIMENTAL: This feature can radically improve performance when adding large numbers
+ (hundreds of shapes) to a slide. It works by caching the last shape ID used and
+ incrementing that value to assign the next shape id. This avoids repeatedly searching all
+ shape ids in the slide each time a new ID is required.
+
+ Performance is not noticeably improved for a slide with a relatively small number of
+ shapes, but because the search time rises with the square of the shape count, this option
+ can be useful for optimizing generation of a slide composed of many shapes.
+
+ Shape-id collisions can occur (causing a repair error on load) if more than one |Slide|
+ object is used to interact with the same slide in the presentation. Note that the |Slides|
+ collection creates a new |Slide| object each time a slide is accessed (e.g. `slide =
+ prs.slides[0]`, so you must be careful to limit use to a single |Slide| object.
+ """
+ return self._cached_max_shape_id is not None
+
+ @turbo_add_enabled.setter
+ def turbo_add_enabled(self, value: bool):
+ enable = bool(value)
+ self._cached_max_shape_id = self._spTree.max_shape_id if enable else None
+
+ @staticmethod
+ def _is_member_elm(shape_elm: ShapeElement) -> bool:
+ """Return true if `shape_elm` represents a member of this collection, False otherwise."""
+ return True
+
+ def _iter_member_elms(self) -> Iterator[ShapeElement]:
+ """Generate each child of the `p:spTree` element that corresponds to a shape.
+
+ Items appear in XML document order.
+ """
+ for shape_elm in self._spTree.iter_shape_elms():
+ if self._is_member_elm(shape_elm):
+ yield shape_elm
+
+ def _next_ph_name(self, ph_type: PP_PLACEHOLDER, id: int, orient: str) -> str:
+ """Next unique placeholder name for placeholder shape of type `ph_type`.
+
+ Usually will be standard placeholder root name suffixed with id-1, e.g.
+ _next_ph_name(ST_PlaceholderType.TBL, 4, 'horz') ==> 'Table Placeholder 3'. The number is
+ incremented as necessary to make the name unique within the collection. If `orient` is
+ `'vert'`, the placeholder name is prefixed with `'Vertical '`.
+ """
+ basename = self.ph_basename(ph_type)
+
+ # prefix rootname with 'Vertical ' if orient is 'vert'
+ if orient == ST_Direction.VERT:
+ basename = "Vertical %s" % basename
+
+ # increment numpart as necessary to make name unique
+ numpart = id - 1
+ names = self._spTree.xpath("//p:cNvPr/@name")
+ while True:
+ name = "%s %d" % (basename, numpart)
+ if name not in names:
+ break
+ numpart += 1
+
+ return name
+
+ @property
+ def _next_shape_id(self) -> int:
+ """Return a unique shape id suitable for use with a new shape.
+
+ The returned id is 1 greater than the maximum shape id used so far. In practice, the
+ minimum id is 2 because the spTree element is always assigned id="1".
+ """
+ # ---presence of cached-max-shape-id indicates turbo mode is on---
+ if self._cached_max_shape_id is not None:
+ self._cached_max_shape_id += 1
+ return self._cached_max_shape_id
+
+ return self._spTree.max_shape_id + 1
+
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+ return BaseShapeFactory(shape_elm, self)
+
+
+class _BaseGroupShapes(_BaseShapes):
+ """Base class for shape-trees that can add shapes."""
+
+ part: SlidePart # pyright: ignore[reportIncompatibleMethodOverride]
+ _element: CT_GroupShape
+
+ def __init__(self, grpSp: CT_GroupShape, parent: ProvidesPart):
+ super(_BaseGroupShapes, self).__init__(grpSp, parent)
+ self._grpSp = grpSp
+
+ def add_chart(
+ self,
+ chart_type: XL_CHART_TYPE,
+ x: Length,
+ y: Length,
+ cx: Length,
+ cy: Length,
+ chart_data: ChartData,
+ ) -> Chart:
+ """Add a new chart of `chart_type` to the slide.
+
+ The chart is positioned at (`x`, `y`), has size (`cx`, `cy`), and depicts `chart_data`.
+ `chart_type` is one of the :ref:`XlChartType` enumeration values. `chart_data` is a
+ |ChartData| object populated with the categories and series values for the chart.
+
+ Note that a |GraphicFrame| shape object is returned, not the |Chart| object contained in
+ that graphic frame shape. The chart object may be accessed using the :attr:`chart`
+ property of the returned |GraphicFrame| object.
+ """
+ rId = self.part.add_chart_part(chart_type, chart_data)
+ graphicFrame = self._add_chart_graphicFrame(rId, x, y, cx, cy)
+ self._recalculate_extents()
+ return cast("Chart", self._shape_factory(graphicFrame))
+
+ def add_connector(
+ self,
+ connector_type: MSO_CONNECTOR_TYPE,
+ begin_x: Length,
+ begin_y: Length,
+ end_x: Length,
+ end_y: Length,
+ ) -> Connector:
+ """Add a newly created connector shape to the end of this shape tree.
+
+ `connector_type` is a member of the :ref:`MsoConnectorType` enumeration and the end-point
+ values are specified as EMU values. The returned connector is of type `connector_type` and
+ has begin and end points as specified.
+ """
+ cxnSp = self._add_cxnSp(connector_type, begin_x, begin_y, end_x, end_y)
+ self._recalculate_extents()
+ return cast(Connector, self._shape_factory(cxnSp))
+
+ def add_group_shape(self, shapes: Iterable[BaseShape] = ()) -> GroupShape:
+ """Return a |GroupShape| object newly appended to this shape tree.
+
+ The group shape is empty and must be populated with shapes using methods on its shape
+ tree, available on its `.shapes` property. The position and extents of the group shape are
+ determined by the shapes it contains; its position and extents are recalculated each time
+ a shape is added to it.
+ """
+ shapes = tuple(shapes)
+ grpSp = self._element.add_grpSp()
+ for shape in shapes:
+ grpSp.insert_element_before(
+ shape._element, "p:extLst" # pyright: ignore[reportPrivateUsage]
+ )
+ if shapes:
+ grpSp.recalculate_extents()
+ return cast(GroupShape, self._shape_factory(grpSp))
+
+ def add_ole_object(
+ self,
+ object_file: str | IO[bytes],
+ prog_id: str,
+ left: Length,
+ top: Length,
+ width: Length | None = None,
+ height: Length | None = None,
+ icon_file: str | IO[bytes] | None = None,
+ icon_width: Length | None = None,
+ icon_height: Length | None = None,
+ ) -> GraphicFrame:
+ """Return newly-created GraphicFrame shape embedding `object_file`.
+
+ The returned graphic-frame shape contains `object_file` as an embedded OLE object. It is
+ displayed as an icon at `left`, `top` with size `width`, `height`. `width` and `height`
+ may be omitted when `prog_id` is a member of `PROG_ID`, in which case the default icon
+ size is used. This is advised for best appearance where applicable because it avoids an
+ icon with a "stretched" appearance.
+
+ `object_file` may either be a str path to a file or file-like object (such as
+ `io.BytesIO`) containing the bytes of the object to be embedded (such as an Excel file).
+
+ `prog_id` can be either a member of `pptx.enum.shapes.PROG_ID` or a str value like
+ `"Adobe.Exchange.7"` determined by inspecting the XML generated by PowerPoint for an
+ object of the desired type.
+
+ `icon_file` may either be a str path to an image file or a file-like object containing the
+ image. The image provided will be displayed in lieu of the OLE object; double-clicking on
+ the image opens the object (subject to operating-system limitations). The image file can
+ be any supported image file. Those produced by PowerPoint itself are generally EMF and can
+ be harvested from a PPTX package that embeds such an object. PNG and JPG also work fine.
+
+ `icon_width` and `icon_height` are `Length` values (e.g. Emu() or Inches()) that describe
+ the size of the icon image within the shape. These should be omitted unless a custom
+ `icon_file` is provided. The dimensions must be discovered by inspecting the XML.
+ Automatic resizing of the OLE-object shape can occur when the icon is double-clicked if
+ these values are not as set by PowerPoint. This behavior may only manifest in the Windows
+ version of PowerPoint.
+ """
+ graphicFrame = _OleObjectElementCreator.graphicFrame(
+ self,
+ self._next_shape_id,
+ object_file,
+ prog_id,
+ left,
+ top,
+ width,
+ height,
+ icon_file,
+ icon_width,
+ icon_height,
+ )
+ self._spTree.append(graphicFrame)
+ self._recalculate_extents()
+ return cast(GraphicFrame, self._shape_factory(graphicFrame))
+
+ def add_picture(
+ self,
+ image_file: str | IO[bytes],
+ left: Length,
+ top: Length,
+ width: Length | None = None,
+ height: Length | None = None,
+ ) -> Picture:
+ """Add picture shape displaying image in `image_file`.
+
+ `image_file` can be either a path to a file (a string) or a file-like object. The picture
+ is positioned with its top-left corner at (`top`, `left`). If `width` and `height` are
+ both |None|, the native size of the image is used. If only one of `width` or `height` is
+ used, the unspecified dimension is calculated to preserve the aspect ratio of the image.
+ If both are specified, the picture is stretched to fit, without regard to its native
+ aspect ratio.
+ """
+ image_part, rId = self.part.get_or_add_image_part(image_file)
+ pic = self._add_pic_from_image_part(image_part, rId, left, top, width, height)
+ self._recalculate_extents()
+ return cast(Picture, self._shape_factory(pic))
+
+ def add_shape(
+ self, autoshape_type_id: MSO_SHAPE, left: Length, top: Length, width: Length, height: Length
+ ) -> Shape:
+ """Return new |Shape| object appended to this shape tree.
+
+ `autoshape_type_id` is a member of :ref:`MsoAutoShapeType` e.g. `MSO_SHAPE.RECTANGLE`
+ specifying the type of shape to be added. The remaining arguments specify the new shape's
+ position and size.
+ """
+ autoshape_type = AutoShapeType(autoshape_type_id)
+ sp = self._add_sp(autoshape_type, left, top, width, height)
+ self._recalculate_extents()
+ return cast(Shape, self._shape_factory(sp))
+
+ def add_textbox(self, left: Length, top: Length, width: Length, height: Length) -> Shape:
+ """Return newly added text box shape appended to this shape tree.
+
+ The text box is of the specified size, located at the specified position on the slide.
+ """
+ sp = self._add_textbox_sp(left, top, width, height)
+ self._recalculate_extents()
+ return cast(Shape, self._shape_factory(sp))
+
+ def build_freeform(
+ self, start_x: float = 0, start_y: float = 0, scale: tuple[float, float] | float = 1.0
+ ) -> FreeformBuilder:
+ """Return |FreeformBuilder| object to specify a freeform shape.
+
+ The optional `start_x` and `start_y` arguments specify the starting pen position in local
+ coordinates. They will be rounded to the nearest integer before use and each default to
+ zero.
+
+ The optional `scale` argument specifies the size of local coordinates proportional to
+ slide coordinates (EMU). If the vertical scale is different than the horizontal scale
+ (local coordinate units are "rectangular"), a pair of numeric values can be provided as
+ the `scale` argument, e.g. `scale=(1.0, 2.0)`. In this case the first number is
+ interpreted as the horizontal (X) scale and the second as the vertical (Y) scale.
+
+ A convenient method for calculating scale is to divide a |Length| object by an equivalent
+ count of local coordinate units, e.g. `scale = Inches(1)/1000` for 1000 local units per
+ inch.
+ """
+ x_scale, y_scale = scale if isinstance(scale, tuple) else (scale, scale)
+
+ return FreeformBuilder.new(self, start_x, start_y, x_scale, y_scale)
+
+ def index(self, shape: BaseShape) -> int:
+ """Return the index of `shape` in this sequence.
+
+ Raises |ValueError| if `shape` is not in the collection.
+ """
+ shape_elms = list(self._element.iter_shape_elms())
+ return shape_elms.index(shape.element)
+
+ def _add_chart_graphicFrame(
+ self, rId: str, x: Length, y: Length, cx: Length, cy: Length
+ ) -> CT_GraphicalObjectFrame:
+ """Return new `p:graphicFrame` element appended to this shape tree.
+
+ The `p:graphicFrame` element has the specified position and size and refers to the chart
+ part identified by `rId`.
+ """
+ shape_id = self._next_shape_id
+ name = "Chart %d" % (shape_id - 1)
+ graphicFrame = CT_GraphicalObjectFrame.new_chart_graphicFrame(
+ shape_id, name, rId, x, y, cx, cy
+ )
+ self._spTree.append(graphicFrame)
+ return graphicFrame
+
+ def _add_cxnSp(
+ self,
+ connector_type: MSO_CONNECTOR_TYPE,
+ begin_x: Length,
+ begin_y: Length,
+ end_x: Length,
+ end_y: Length,
+ ) -> CT_Connector:
+ """Return a newly-added `p:cxnSp` element as specified.
+
+ The `p:cxnSp` element is for a connector of `connector_type` beginning at (`begin_x`,
+ `begin_y`) and extending to (`end_x`, `end_y`).
+ """
+ id_ = self._next_shape_id
+ name = "Connector %d" % (id_ - 1)
+
+ flipH, flipV = begin_x > end_x, begin_y > end_y
+ x, y = min(begin_x, end_x), min(begin_y, end_y)
+ cx, cy = abs(end_x - begin_x), abs(end_y - begin_y)
+
+ return self._element.add_cxnSp(id_, name, connector_type, x, y, cx, cy, flipH, flipV)
+
+ def _add_pic_from_image_part(
+ self,
+ image_part: ImagePart,
+ rId: str,
+ x: Length,
+ y: Length,
+ cx: Length | None,
+ cy: Length | None,
+ ) -> CT_Picture:
+ """Return a newly appended `p:pic` element as specified.
+
+ The `p:pic` element displays the image in `image_part` with size and position specified by
+ `x`, `y`, `cx`, and `cy`. The element is appended to the shape tree, causing it to be
+ displayed first in z-order on the slide.
+ """
+ id_ = self._next_shape_id
+ scaled_cx, scaled_cy = image_part.scale(cx, cy)
+ name = "Picture %d" % (id_ - 1)
+ desc = image_part.desc
+ pic = self._grpSp.add_pic(id_, name, desc, rId, x, y, scaled_cx, scaled_cy)
+ return pic
+
+ def _add_sp(
+ self, autoshape_type: AutoShapeType, x: Length, y: Length, cx: Length, cy: Length
+ ) -> CT_Shape:
+ """Return newly-added `p:sp` element as specified.
+
+ `p:sp` element is of `autoshape_type` at position (`x`, `y`) and of size (`cx`, `cy`).
+ """
+ id_ = self._next_shape_id
+ name = "%s %d" % (autoshape_type.basename, id_ - 1)
+ sp = self._grpSp.add_autoshape(id_, name, autoshape_type.prst, x, y, cx, cy)
+ return sp
+
+ def _add_textbox_sp(self, x: Length, y: Length, cx: Length, cy: Length) -> CT_Shape:
+ """Return newly-appended textbox `p:sp` element.
+
+ Element has position (`x`, `y`) and size (`cx`, `cy`).
+ """
+ id_ = self._next_shape_id
+ name = "TextBox %d" % (id_ - 1)
+ sp = self._spTree.add_textbox(id_, name, x, y, cx, cy)
+ return sp
+
+ def _recalculate_extents(self) -> None:
+ """Adjust position and size to incorporate all contained shapes.
+
+ This would typically be called when a contained shape is added, removed, or its position
+ or size updated.
+ """
+ # ---default behavior is to do nothing, GroupShapes overrides to
+ # produce the distinctive behavior of groups and subgroups.---
+ pass
+
+
+class GroupShapes(_BaseGroupShapes):
+ """The sequence of child shapes belonging to a group shape.
+
+ Note that this collection can itself contain a group shape, making this part of a recursive,
+ tree data structure (acyclic graph).
+ """
+
+ def _recalculate_extents(self) -> None:
+ """Adjust position and size to incorporate all contained shapes.
+
+ This would typically be called when a contained shape is added, removed, or its position
+ or size updated.
+ """
+ self._grpSp.recalculate_extents()
+
+
+class SlideShapes(_BaseGroupShapes):
+ """Sequence of shapes appearing on a slide.
+
+ The first shape in the sequence is the backmost in z-order and the last shape is topmost.
+ Supports indexed access, len(), index(), and iteration.
+ """
+
+ parent: Slide # pyright: ignore[reportIncompatibleMethodOverride]
+
+ def add_movie(
+ self,
+ movie_file: str | IO[bytes],
+ left: Length,
+ top: Length,
+ width: Length,
+ height: Length,
+ poster_frame_image: str | IO[bytes] | None = None,
+ mime_type: str = CT.VIDEO,
+ ) -> GraphicFrame:
+ """Return newly added movie shape displaying video in `movie_file`.
+
+ **EXPERIMENTAL.** This method has important limitations:
+
+ * The size must be specified; no auto-scaling such as that provided by :meth:`add_picture`
+ is performed.
+ * The MIME type of the video file should be specified, e.g. 'video/mp4'. The provided
+ video file is not interrogated for its type. The MIME type `video/unknown` is used by
+ default (and works fine in tests as of this writing).
+ * A poster frame image must be provided, it cannot be automatically extracted from the
+ video file. If no poster frame is provided, the default "media loudspeaker" image will
+ be used.
+
+ Return a newly added movie shape to the slide, positioned at (`left`, `top`), having size
+ (`width`, `height`), and containing `movie_file`. Before the video is started,
+ `poster_frame_image` is displayed as a placeholder for the video.
+ """
+ movie_pic = _MoviePicElementCreator.new_movie_pic(
+ self,
+ self._next_shape_id,
+ movie_file,
+ left,
+ top,
+ width,
+ height,
+ poster_frame_image,
+ mime_type,
+ )
+ self._spTree.append(movie_pic)
+ self._add_video_timing(movie_pic)
+ return cast(GraphicFrame, self._shape_factory(movie_pic))
+
+ def add_table(
+ self, rows: int, cols: int, left: Length, top: Length, width: Length, height: Length
+ ) -> GraphicFrame:
+ """Add a |GraphicFrame| object containing a table.
+
+ The table has the specified number of `rows` and `cols` and the specified position and
+ size. `width` is evenly distributed between the columns of the new table. Likewise,
+ `height` is evenly distributed between the rows. Note that the `.table` property on the
+ returned |GraphicFrame| shape must be used to access the enclosed |Table| object.
+ """
+ graphicFrame = self._add_graphicFrame_containing_table(rows, cols, left, top, width, height)
+ return cast(GraphicFrame, self._shape_factory(graphicFrame))
+
+ def clone_layout_placeholders(self, slide_layout: SlideLayout) -> None:
+ """Add placeholder shapes based on those in `slide_layout`.
+
+ Z-order of placeholders is preserved. Latent placeholders (date, slide number, and footer)
+ are not cloned.
+ """
+ for placeholder in slide_layout.iter_cloneable_placeholders():
+ self.clone_placeholder(placeholder)
+
+ @property
+ def placeholders(self) -> SlidePlaceholders:
+ """Sequence of placeholder shapes in this slide."""
+ return self.parent.placeholders
+
+ @property
+ def title(self) -> Shape | None:
+ """The title placeholder shape on the slide.
+
+ |None| if the slide has no title placeholder.
+ """
+ for elm in self._spTree.iter_ph_elms():
+ if elm.ph_idx == 0:
+ return cast(Shape, self._shape_factory(elm))
+ return None
+
+ def _add_graphicFrame_containing_table(
+ self, rows: int, cols: int, x: Length, y: Length, cx: Length, cy: Length
+ ) -> CT_GraphicalObjectFrame:
+ """Return a newly added `p:graphicFrame` element containing a table as specified."""
+ _id = self._next_shape_id
+ name = "Table %d" % (_id - 1)
+ graphicFrame = self._spTree.add_table(_id, name, rows, cols, x, y, cx, cy)
+ return graphicFrame
+
+ def _add_video_timing(self, pic: CT_Picture) -> None:
+ """Add a `p:video` element under `p:sld/p:timing`.
+
+ The element will refer to the specified `pic` element by its shape id, and cause the video
+ play controls to appear for that video.
+ """
+ sld = self._spTree.xpath("/p:sld")[0]
+ childTnLst = sld.get_or_add_childTnLst()
+ childTnLst.add_video(pic.shape_id)
+
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+ return SlideShapeFactory(shape_elm, self)
+
+
+class LayoutShapes(_BaseShapes):
+ """Sequence of shapes appearing on a slide layout.
+
+ The first shape in the sequence is the backmost in z-order and the last shape is topmost.
+ Supports indexed access, len(), index(), and iteration.
+ """
+
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+ return _LayoutShapeFactory(shape_elm, self)
+
+
+class MasterShapes(_BaseShapes):
+ """Sequence of shapes appearing on a slide master.
+
+ The first shape in the sequence is the backmost in z-order and the last shape is topmost.
+ Supports indexed access, len(), and iteration.
+ """
+
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+ return _MasterShapeFactory(shape_elm, self)
+
+
+class NotesSlideShapes(_BaseShapes):
+ """Sequence of shapes appearing on a notes slide.
+
+ The first shape in the sequence is the backmost in z-order and the last shape is topmost.
+ Supports indexed access, len(), index(), and iteration.
+ """
+
+ def ph_basename(self, ph_type: PP_PLACEHOLDER) -> str:
+ """Return the base name for a placeholder of `ph_type` in this shape collection.
+
+ A notes slide uses a different name for the body placeholder and has some unique
+ placeholder types, so this method overrides the default in the base class.
+ """
+ return {
+ PP_PLACEHOLDER.BODY: "Notes Placeholder",
+ PP_PLACEHOLDER.DATE: "Date Placeholder",
+ PP_PLACEHOLDER.FOOTER: "Footer Placeholder",
+ PP_PLACEHOLDER.HEADER: "Header Placeholder",
+ PP_PLACEHOLDER.SLIDE_IMAGE: "Slide Image Placeholder",
+ PP_PLACEHOLDER.SLIDE_NUMBER: "Slide Number Placeholder",
+ }[ph_type]
+
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+ """Return appropriate shape object for `shape_elm` appearing on a notes slide."""
+ return _NotesSlideShapeFactory(shape_elm, self)
+
+
+class BasePlaceholders(_BaseShapes):
+ """Base class for placeholder collections.
+
+ Subclasses differentiate behaviors for a master, layout, and slide. By default, placeholder
+ shapes are constructed using |BaseShapeFactory|. Subclasses should override
+ :method:`_shape_factory` to use custom placeholder classes.
+ """
+
+ @staticmethod
+ def _is_member_elm(shape_elm: ShapeElement) -> bool:
+ """True if `shape_elm` is a placeholder shape, False otherwise."""
+ return shape_elm.has_ph_elm
+
+
+class LayoutPlaceholders(BasePlaceholders):
+ """Sequence of |LayoutPlaceholder| instance for each placeholder shape on a slide layout."""
+
+ __iter__: Callable[ # pyright: ignore[reportIncompatibleMethodOverride]
+ [], Iterator[LayoutPlaceholder]
+ ]
+
+ def get(self, idx: int, default: LayoutPlaceholder | None = None) -> LayoutPlaceholder | None:
+ """The first placeholder shape with matching `idx` value, or `default` if not found."""
+ for placeholder in self:
+ if placeholder.element.ph_idx == idx:
+ return placeholder
+ return default
+
+ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+ return _LayoutShapeFactory(shape_elm, self)
+
+
+class MasterPlaceholders(BasePlaceholders):
+ """Sequence of MasterPlaceholder representing the placeholder shapes on a slide master."""
+
+ __iter__: Callable[ # pyright: ignore[reportIncompatibleMethodOverride]
+ [], Iterator[MasterPlaceholder]
+ ]
+
+ def get(self, ph_type: PP_PLACEHOLDER, default: MasterPlaceholder | None = None):
+ """Return the first placeholder shape with type `ph_type` (e.g. 'body').
+
+ Returns `default` if no such placeholder shape is present in the collection.
+ """
+ for placeholder in self:
+ if placeholder.ph_type == ph_type:
+ return placeholder
+ return default
+
+ def _shape_factory( # pyright: ignore[reportIncompatibleMethodOverride]
+ self, placeholder_elm: CT_Shape
+ ) -> MasterPlaceholder:
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+ return cast(MasterPlaceholder, _MasterShapeFactory(placeholder_elm, self))
+
+
+class NotesSlidePlaceholders(MasterPlaceholders):
+ """Sequence of placeholder shapes on a notes slide."""
+
+ __iter__: Callable[ # pyright: ignore[reportIncompatibleMethodOverride]
+ [], Iterator[NotesSlidePlaceholder]
+ ]
+
+ def _shape_factory( # pyright: ignore[reportIncompatibleMethodOverride]
+ self, placeholder_elm: CT_Shape
+ ) -> NotesSlidePlaceholder:
+ """Return an instance of the appropriate placeholder proxy class for `placeholder_elm`."""
+ return cast(NotesSlidePlaceholder, _NotesSlideShapeFactory(placeholder_elm, self))
+
+
+class SlidePlaceholders(ParentedElementProxy):
+ """Collection of placeholder shapes on a slide.
+
+ Supports iteration, :func:`len`, and dictionary-style lookup on the `idx` value of the
+ placeholders it contains.
+ """
+
+ _element: CT_GroupShape
+
+ def __getitem__(self, idx: int):
+ """Access placeholder shape having `idx`.
+
+ Note that while this looks like list access, idx is actually a dictionary key and will
+ raise |KeyError| if no placeholder with that idx value is in the collection.
+ """
+ for e in self._element.iter_ph_elms():
+ if e.ph_idx == idx:
+ return SlideShapeFactory(e, self)
+ raise KeyError("no placeholder on this slide with idx == %d" % idx)
+
+ def __iter__(self):
+ """Generate placeholder shapes in `idx` order."""
+ ph_elms = sorted([e for e in self._element.iter_ph_elms()], key=lambda e: e.ph_idx)
+ return (SlideShapeFactory(e, self) for e in ph_elms)
+
+ def __len__(self) -> int:
+ """Return count of placeholder shapes."""
+ return len(list(self._element.iter_ph_elms()))
+
+
+def BaseShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
+ """Return an instance of the appropriate shape proxy class for `shape_elm`."""
+ tag = shape_elm.tag
+
+ if isinstance(shape_elm, CT_Picture):
+ videoFiles = shape_elm.xpath("./p:nvPicPr/p:nvPr/a:videoFile")
+ if videoFiles:
+ return Movie(shape_elm, parent)
+ return Picture(shape_elm, parent)
+
+ shape_cls = {
+ qn("p:cxnSp"): Connector,
+ qn("p:grpSp"): GroupShape,
+ qn("p:sp"): Shape,
+ qn("p:graphicFrame"): GraphicFrame,
+ }.get(tag, BaseShape)
+
+ return shape_cls(shape_elm, parent) # pyright: ignore[reportArgumentType]
+
+
+def _LayoutShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
+ """Return appropriate shape object for `shape_elm` on a slide layout."""
+ if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm:
+ return LayoutPlaceholder(shape_elm, parent)
+ return BaseShapeFactory(shape_elm, parent)
+
+
+def _MasterShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
+ """Return appropriate shape object for `shape_elm` on a slide master."""
+ if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm:
+ return MasterPlaceholder(shape_elm, parent)
+ return BaseShapeFactory(shape_elm, parent)
+
+
+def _NotesSlideShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
+ """Return appropriate shape object for `shape_elm` on a notes slide."""
+ if isinstance(shape_elm, CT_Shape) and shape_elm.has_ph_elm:
+ return NotesSlidePlaceholder(shape_elm, parent)
+ return BaseShapeFactory(shape_elm, parent)
+
+
+def _SlidePlaceholderFactory(shape_elm: ShapeElement, parent: ProvidesPart):
+ """Return a placeholder shape of the appropriate type for `shape_elm`."""
+ tag = shape_elm.tag
+ if tag == qn("p:sp"):
+ Constructor = {
+ PP_PLACEHOLDER.BITMAP: PicturePlaceholder,
+ PP_PLACEHOLDER.CHART: ChartPlaceholder,
+ PP_PLACEHOLDER.PICTURE: PicturePlaceholder,
+ PP_PLACEHOLDER.TABLE: TablePlaceholder,
+ }.get(shape_elm.ph_type, SlidePlaceholder)
+ elif tag == qn("p:graphicFrame"):
+ Constructor = PlaceholderGraphicFrame
+ elif tag == qn("p:pic"):
+ Constructor = PlaceholderPicture
+ else:
+ Constructor = BaseShapeFactory
+ return Constructor(shape_elm, parent) # pyright: ignore[reportArgumentType]
+
+
+def SlideShapeFactory(shape_elm: ShapeElement, parent: ProvidesPart) -> BaseShape:
+ """Return appropriate shape object for `shape_elm` on a slide."""
+ if shape_elm.has_ph_elm:
+ return _SlidePlaceholderFactory(shape_elm, parent)
+ return BaseShapeFactory(shape_elm, parent)
+
+
+class _MoviePicElementCreator(object):
+ """Functional service object for creating a new movie p:pic element.
+
+ It's entire external interface is its :meth:`new_movie_pic` class method that returns a new
+ `p:pic` element containing the specified video. This class is not intended to be constructed
+ or an instance of it retained by the caller; it is a "one-shot" object, really a function
+ wrapped in a object such that its helper methods can be organized here.
+ """
+
+ def __init__(
+ self,
+ shapes: SlideShapes,
+ shape_id: int,
+ movie_file: str | IO[bytes],
+ x: Length,
+ y: Length,
+ cx: Length,
+ cy: Length,
+ poster_frame_file: str | IO[bytes] | None,
+ mime_type: str | None,
+ ):
+ super(_MoviePicElementCreator, self).__init__()
+ self._shapes = shapes
+ self._shape_id = shape_id
+ self._movie_file = movie_file
+ self._x, self._y, self._cx, self._cy = x, y, cx, cy
+ self._poster_frame_file = poster_frame_file
+ self._mime_type = mime_type
+
+ @classmethod
+ def new_movie_pic(
+ cls,
+ shapes: SlideShapes,
+ shape_id: int,
+ movie_file: str | IO[bytes],
+ x: Length,
+ y: Length,
+ cx: Length,
+ cy: Length,
+ poster_frame_image: str | IO[bytes] | None,
+ mime_type: str | None,
+ ) -> CT_Picture:
+ """Return a new `p:pic` element containing video in `movie_file`.
+
+ If `mime_type` is None, 'video/unknown' is used. If `poster_frame_file` is None, the
+ default "media loudspeaker" image is used.
+ """
+ return cls(shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_image, mime_type)._pic
+
+ @property
+ def _media_rId(self) -> str:
+ """Return the rId of RT.MEDIA relationship to video part.
+
+ For historical reasons, there are two relationships to the same part; one is the video rId
+ and the other is the media rId.
+ """
+ return self._video_part_rIds[0]
+
+ @lazyproperty
+ def _pic(self) -> CT_Picture:
+ """Return the new `p:pic` element referencing the video."""
+ return CT_Picture.new_video_pic(
+ self._shape_id,
+ self._shape_name,
+ self._video_rId,
+ self._media_rId,
+ self._poster_frame_rId,
+ self._x,
+ self._y,
+ self._cx,
+ self._cy,
+ )
+
+ @lazyproperty
+ def _poster_frame_image_file(self) -> str | IO[bytes]:
+ """Return the image file for video placeholder image.
+
+ If no poster frame file is provided, the default "media loudspeaker" image is used.
+ """
+ poster_frame_file = self._poster_frame_file
+ if poster_frame_file is None:
+ return io.BytesIO(SPEAKER_IMAGE_BYTES)
+ return poster_frame_file
+
+ @lazyproperty
+ def _poster_frame_rId(self) -> str:
+ """Return the rId of relationship to poster frame image.
+
+ The poster frame is the image used to represent the video before it's played.
+ """
+ _, poster_frame_rId = self._slide_part.get_or_add_image_part(self._poster_frame_image_file)
+ return poster_frame_rId
+
+ @property
+ def _shape_name(self) -> str:
+ """Return the appropriate shape name for the p:pic shape.
+
+ A movie shape is named with the base filename of the video.
+ """
+ return self._video.filename
+
+ @property
+ def _slide_part(self) -> SlidePart:
+ """Return SlidePart object for slide containing this movie."""
+ return self._shapes.part
+
+ @lazyproperty
+ def _video(self) -> Video:
+ """Return a |Video| object containing the movie file."""
+ return Video.from_path_or_file_like(self._movie_file, self._mime_type)
+
+ @lazyproperty
+ def _video_part_rIds(self) -> tuple[str, str]:
+ """Return the rIds for relationships to media part for video.
+
+ This is where the media part and its relationships to the slide are actually created.
+ """
+ media_rId, video_rId = self._slide_part.get_or_add_video_media_part(self._video)
+ return media_rId, video_rId
+
+ @property
+ def _video_rId(self) -> str:
+ """Return the rId of RT.VIDEO relationship to video part.
+
+ For historical reasons, there are two relationships to the same part; one is the video rId
+ and the other is the media rId.
+ """
+ return self._video_part_rIds[1]
+
+
+class _OleObjectElementCreator(object):
+ """Functional service object for creating a new OLE-object p:graphicFrame element.
+
+ It's entire external interface is its :meth:`graphicFrame` class method that returns a new
+ `p:graphicFrame` element containing the specified embedded OLE-object shape. This class is not
+ intended to be constructed or an instance of it retained by the caller; it is a "one-shot"
+ object, really a function wrapped in a object such that its helper methods can be organized
+ here.
+ """
+
+ def __init__(
+ self,
+ shapes: _BaseGroupShapes,
+ shape_id: int,
+ ole_object_file: str | IO[bytes],
+ prog_id: PROG_ID | str,
+ x: Length,
+ y: Length,
+ cx: Length | None,
+ cy: Length | None,
+ icon_file: str | IO[bytes] | None,
+ icon_width: Length | None,
+ icon_height: Length | None,
+ ):
+ self._shapes = shapes
+ self._shape_id = shape_id
+ self._ole_object_file = ole_object_file
+ self._prog_id_arg = prog_id
+ self._x = x
+ self._y = y
+ self._cx_arg = cx
+ self._cy_arg = cy
+ self._icon_file_arg = icon_file
+ self._icon_width_arg = icon_width
+ self._icon_height_arg = icon_height
+
+ @classmethod
+ def graphicFrame(
+ cls,
+ shapes: _BaseGroupShapes,
+ shape_id: int,
+ ole_object_file: str | IO[bytes],
+ prog_id: PROG_ID | str,
+ x: Length,
+ y: Length,
+ cx: Length | None,
+ cy: Length | None,
+ icon_file: str | IO[bytes] | None,
+ icon_width: Length | None,
+ icon_height: Length | None,
+ ) -> CT_GraphicalObjectFrame:
+ """Return new `p:graphicFrame` element containing embedded `ole_object_file`."""
+ return cls(
+ shapes,
+ shape_id,
+ ole_object_file,
+ prog_id,
+ x,
+ y,
+ cx,
+ cy,
+ icon_file,
+ icon_width,
+ icon_height,
+ )._graphicFrame
+
+ @lazyproperty
+ def _graphicFrame(self) -> CT_GraphicalObjectFrame:
+ """Newly-created `p:graphicFrame` element referencing embedded OLE-object."""
+ return CT_GraphicalObjectFrame.new_ole_object_graphicFrame(
+ self._shape_id,
+ self._shape_name,
+ self._ole_object_rId,
+ self._progId,
+ self._icon_rId,
+ self._x,
+ self._y,
+ self._cx,
+ self._cy,
+ self._icon_width,
+ self._icon_height,
+ )
+
+ @lazyproperty
+ def _cx(self) -> Length:
+ """Emu object specifying width of "show-as-icon" image for OLE shape."""
+ # --- a user-specified width overrides any default ---
+ if self._cx_arg is not None:
+ return self._cx_arg
+
+ # --- the default width is specified by the PROG_ID member if prog_id is one,
+ # --- otherwise it gets the default icon width.
+ return (
+ Emu(self._prog_id_arg.width) if isinstance(self._prog_id_arg, PROG_ID) else Emu(965200)
+ )
+
+ @lazyproperty
+ def _cy(self) -> Length:
+ """Emu object specifying height of "show-as-icon" image for OLE shape."""
+ # --- a user-specified width overrides any default ---
+ if self._cy_arg is not None:
+ return self._cy_arg
+
+ # --- the default height is specified by the PROG_ID member if prog_id is one,
+ # --- otherwise it gets the default icon height.
+ return (
+ Emu(self._prog_id_arg.height) if isinstance(self._prog_id_arg, PROG_ID) else Emu(609600)
+ )
+
+ @lazyproperty
+ def _icon_height(self) -> Length:
+ """Vertical size of enclosed EMF icon within the OLE graphic-frame.
+
+ This must be specified when a custom icon is used, to avoid stretching of the image and
+ possible undesired resizing by PowerPoint when the OLE shape is double-clicked to open it.
+
+ The correct size can be determined by creating an example PPTX using PowerPoint and then
+ inspecting the XML of the OLE graphics-frame (p:oleObj.imgH).
+ """
+ return self._icon_height_arg if self._icon_height_arg is not None else Emu(609600)
+
+ @lazyproperty
+ def _icon_image_file(self) -> str | IO[bytes]:
+ """Reference to image file containing icon to show in lieu of this object.
+
+ This can be either a str path or a file-like object (io.BytesIO typically).
+ """
+ # --- a user-specified icon overrides any default ---
+ if self._icon_file_arg is not None:
+ return self._icon_file_arg
+
+ # --- A prog_id belonging to PROG_ID gets its icon filename from there. A
+ # --- user-specified (str) prog_id gets the default icon.
+ icon_filename = (
+ self._prog_id_arg.icon_filename
+ if isinstance(self._prog_id_arg, PROG_ID)
+ else "generic-icon.emf"
+ )
+
+ _thisdir = os.path.split(__file__)[0]
+ return os.path.abspath(os.path.join(_thisdir, "..", "templates", icon_filename))
+
+ @lazyproperty
+ def _icon_rId(self) -> str:
+ """str rId like "rId7" of rel to icon (image) representing OLE-object part."""
+ _, rId = self._slide_part.get_or_add_image_part(self._icon_image_file)
+ return rId
+
+ @lazyproperty
+ def _icon_width(self) -> Length:
+ """Width of enclosed EMF icon within the OLE graphic-frame.
+
+ This must be specified when a custom icon is used, to avoid stretching of the image and
+ possible undesired resizing by PowerPoint when the OLE shape is double-clicked to open it.
+ """
+ return self._icon_width_arg if self._icon_width_arg is not None else Emu(965200)
+
+ @lazyproperty
+ def _ole_object_rId(self) -> str:
+ """str rId like "rId6" of relationship to embedded ole_object part.
+
+ This is where the ole_object part and its relationship to the slide are actually created.
+ """
+ return self._slide_part.add_embedded_ole_object_part(
+ self._prog_id_arg, self._ole_object_file
+ )
+
+ @lazyproperty
+ def _progId(self) -> str:
+ """str like "Excel.Sheet.12" identifying program used to open object.
+
+ This value appears in the `progId` attribute of the `p:oleObj` element for the object.
+ """
+ prog_id_arg = self._prog_id_arg
+
+ # --- member of PROG_ID enumeration knows its progId keyphrase, otherwise caller
+ # --- has specified it explicitly (as str)
+ return prog_id_arg.progId if isinstance(prog_id_arg, PROG_ID) else prog_id_arg
+
+ @lazyproperty
+ def _shape_name(self) -> str:
+ """str name like "Object 1" for the embedded ole_object shape.
+
+ The name is formed from the prefix "Object " and the shape-id decremented by 1.
+ """
+ return "Object %d" % (self._shape_id - 1)
+
+ @lazyproperty
+ def _slide_part(self) -> SlidePart:
+ """SlidePart object for this slide."""
+ return self._shapes.part
diff --git a/.venv/lib/python3.12/site-packages/pptx/shared.py b/.venv/lib/python3.12/site-packages/pptx/shared.py
new file mode 100644
index 00000000..da2a1718
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/shared.py
@@ -0,0 +1,82 @@
+"""Objects shared by pptx modules."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from pptx.opc.package import XmlPart
+ from pptx.oxml.xmlchemy import BaseOxmlElement
+ from pptx.types import ProvidesPart
+
+
+class ElementProxy(object):
+ """Base class for lxml element proxy classes.
+
+ An element proxy class is one whose primary responsibilities are fulfilled by manipulating the
+ attributes and child elements of an XML element. They are the most common type of class in
+ python-pptx other than custom element (oxml) classes.
+ """
+
+ def __init__(self, element: BaseOxmlElement):
+ self._element = element
+
+ def __eq__(self, other: object) -> bool:
+ """Return |True| if this proxy object refers to the same oxml element as does *other*.
+
+ ElementProxy objects are value objects and should maintain no mutable local state.
+ Equality for proxy objects is defined as referring to the same XML element, whether or not
+ they are the same proxy object instance.
+ """
+ if not isinstance(other, ElementProxy):
+ return False
+ return self._element is other._element
+
+ def __ne__(self, other: object) -> bool:
+ if not isinstance(other, ElementProxy):
+ return True
+ return self._element is not other._element
+
+ @property
+ def element(self):
+ """The lxml element proxied by this object."""
+ return self._element
+
+
+class ParentedElementProxy(ElementProxy):
+ """Provides access to ancestor objects and part.
+
+ An ancestor may occasionally be required to provide a service, such as add or drop a
+ relationship. Provides the :attr:`_parent` attribute to subclasses and the public
+ :attr:`parent` read-only property.
+ """
+
+ def __init__(self, element: BaseOxmlElement, parent: ProvidesPart):
+ super(ParentedElementProxy, self).__init__(element)
+ self._parent = parent
+
+ @property
+ def parent(self):
+ """The ancestor proxy object to this one.
+
+ For example, the parent of a shape is generally the |SlideShapes| object that contains it.
+ """
+ return self._parent
+
+ @property
+ def part(self) -> XmlPart:
+ """The package part containing this object."""
+ return self._parent.part
+
+
+class PartElementProxy(ElementProxy):
+ """Provides common members for proxy-objects that wrap a part's root element, e.g. `p:sld`."""
+
+ def __init__(self, element: BaseOxmlElement, part: XmlPart):
+ super(PartElementProxy, self).__init__(element)
+ self._part = part
+
+ @property
+ def part(self) -> XmlPart:
+ """The package part containing this object."""
+ return self._part
diff --git a/.venv/lib/python3.12/site-packages/pptx/slide.py b/.venv/lib/python3.12/site-packages/pptx/slide.py
new file mode 100644
index 00000000..3b1b65d8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/slide.py
@@ -0,0 +1,498 @@
+"""Slide-related objects, including masters, layouts, and notes."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Iterator, cast
+
+from pptx.dml.fill import FillFormat
+from pptx.enum.shapes import PP_PLACEHOLDER
+from pptx.shapes.shapetree import (
+ LayoutPlaceholders,
+ LayoutShapes,
+ MasterPlaceholders,
+ MasterShapes,
+ NotesSlidePlaceholders,
+ NotesSlideShapes,
+ SlidePlaceholders,
+ SlideShapes,
+)
+from pptx.shared import ElementProxy, ParentedElementProxy, PartElementProxy
+from pptx.util import lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.oxml.presentation import CT_SlideIdList, CT_SlideMasterIdList
+ from pptx.oxml.slide import (
+ CT_CommonSlideData,
+ CT_NotesSlide,
+ CT_Slide,
+ CT_SlideLayoutIdList,
+ CT_SlideMaster,
+ )
+ from pptx.parts.presentation import PresentationPart
+ from pptx.parts.slide import SlideLayoutPart, SlideMasterPart, SlidePart
+ from pptx.presentation import Presentation
+ from pptx.shapes.placeholder import LayoutPlaceholder, MasterPlaceholder
+ from pptx.shapes.shapetree import NotesSlidePlaceholder
+ from pptx.text.text import TextFrame
+
+
+class _BaseSlide(PartElementProxy):
+ """Base class for slide objects, including masters, layouts and notes."""
+
+ _element: CT_Slide
+
+ @lazyproperty
+ def background(self) -> _Background:
+ """|_Background| object providing slide background properties.
+
+ This property returns a |_Background| object whether or not the
+ slide, master, or layout has an explicitly defined background.
+
+ The same |_Background| object is returned on every call for the same
+ slide object.
+ """
+ return _Background(self._element.cSld)
+
+ @property
+ def name(self) -> str:
+ """String representing the internal name of this slide.
+
+ Returns an empty string (`''`) if no name is assigned. Assigning an empty string or |None|
+ to this property causes any name to be removed.
+ """
+ return self._element.cSld.name
+
+ @name.setter
+ def name(self, value: str | None):
+ new_value = "" if value is None else value
+ self._element.cSld.name = new_value
+
+
+class _BaseMaster(_BaseSlide):
+ """Base class for master objects such as |SlideMaster| and |NotesMaster|.
+
+ Provides access to placeholders and regular shapes.
+ """
+
+ @lazyproperty
+ def placeholders(self) -> MasterPlaceholders:
+ """|MasterPlaceholders| collection of placeholder shapes in this master.
+
+ Sequence sorted in `idx` order.
+ """
+ return MasterPlaceholders(self._element.spTree, self)
+
+ @lazyproperty
+ def shapes(self):
+ """
+ Instance of |MasterShapes| containing sequence of shape objects
+ appearing on this slide.
+ """
+ return MasterShapes(self._element.spTree, self)
+
+
+class NotesMaster(_BaseMaster):
+ """Proxy for the notes master XML document.
+
+ Provides access to shapes, the most commonly used of which are placeholders.
+ """
+
+
+class NotesSlide(_BaseSlide):
+ """Notes slide object.
+
+ Provides access to slide notes placeholder and other shapes on the notes handout
+ page.
+ """
+
+ element: CT_NotesSlide # pyright: ignore[reportIncompatibleMethodOverride]
+
+ def clone_master_placeholders(self, notes_master: NotesMaster) -> None:
+ """Selectively add placeholder shape elements from `notes_master`.
+
+ Selected placeholder shape elements from `notes_master` are added to the shapes
+ collection of this notes slide. Z-order of placeholders is preserved. Certain
+ placeholders (header, date, footer) are not cloned.
+ """
+
+ def iter_cloneable_placeholders() -> Iterator[MasterPlaceholder]:
+ """Generate a reference to each cloneable placeholder in `notes_master`.
+
+ These are the placeholders that should be cloned to a notes slide when the a new notes
+ slide is created.
+ """
+ cloneable = (
+ PP_PLACEHOLDER.SLIDE_IMAGE,
+ PP_PLACEHOLDER.BODY,
+ PP_PLACEHOLDER.SLIDE_NUMBER,
+ )
+ for placeholder in notes_master.placeholders:
+ if placeholder.element.ph_type in cloneable:
+ yield placeholder
+
+ shapes = self.shapes
+ for placeholder in iter_cloneable_placeholders():
+ shapes.clone_placeholder(cast("LayoutPlaceholder", placeholder))
+
+ @property
+ def notes_placeholder(self) -> NotesSlidePlaceholder | None:
+ """the notes placeholder on this notes slide, the shape that contains the actual notes text.
+
+ Return |None| if no notes placeholder is present; while this is probably uncommon, it can
+ happen if the notes master does not have a body placeholder, or if the notes placeholder
+ has been deleted from the notes slide.
+ """
+ for placeholder in self.placeholders:
+ if placeholder.placeholder_format.type == PP_PLACEHOLDER.BODY:
+ return placeholder
+ return None
+
+ @property
+ def notes_text_frame(self) -> TextFrame | None:
+ """The text frame of the notes placeholder on this notes slide.
+
+ |None| if there is no notes placeholder. This is a shortcut to accommodate the common case
+ of simply adding "notes" text to the notes "page".
+ """
+ notes_placeholder = self.notes_placeholder
+ if notes_placeholder is None:
+ return None
+ return notes_placeholder.text_frame
+
+ @lazyproperty
+ def placeholders(self) -> NotesSlidePlaceholders:
+ """Instance of |NotesSlidePlaceholders| for this notes-slide.
+
+ Contains the sequence of placeholder shapes in this notes slide.
+ """
+ return NotesSlidePlaceholders(self.element.spTree, self)
+
+ @lazyproperty
+ def shapes(self) -> NotesSlideShapes:
+ """Sequence of shape objects appearing on this notes slide."""
+ return NotesSlideShapes(self._element.spTree, self)
+
+
+class Slide(_BaseSlide):
+ """Slide object. Provides access to shapes and slide-level properties."""
+
+ part: SlidePart # pyright: ignore[reportIncompatibleMethodOverride]
+
+ @property
+ def follow_master_background(self):
+ """|True| if this slide inherits the slide master background.
+
+ Assigning |False| causes background inheritance from the master to be
+ interrupted; if there is no custom background for this slide,
+ a default background is added. If a custom background already exists
+ for this slide, assigning |False| has no effect.
+
+ Assigning |True| causes any custom background for this slide to be
+ deleted and inheritance from the master restored.
+ """
+ return self._element.bg is None
+
+ @property
+ def has_notes_slide(self) -> bool:
+ """`True` if this slide has a notes slide, `False` otherwise.
+
+ A notes slide is created by :attr:`.notes_slide` when one doesn't exist; use this property
+ to test for a notes slide without the possible side effect of creating one.
+ """
+ return self.part.has_notes_slide
+
+ @property
+ def notes_slide(self) -> NotesSlide:
+ """The |NotesSlide| instance for this slide.
+
+ If the slide does not have a notes slide, one is created. The same single instance is
+ returned on each call.
+ """
+ return self.part.notes_slide
+
+ @lazyproperty
+ def placeholders(self) -> SlidePlaceholders:
+ """Sequence of placeholder shapes in this slide."""
+ return SlidePlaceholders(self._element.spTree, self)
+
+ @lazyproperty
+ def shapes(self) -> SlideShapes:
+ """Sequence of shape objects appearing on this slide."""
+ return SlideShapes(self._element.spTree, self)
+
+ @property
+ def slide_id(self) -> int:
+ """Integer value that uniquely identifies this slide within this presentation.
+
+ The slide id does not change if the position of this slide in the slide sequence is changed
+ by adding, rearranging, or deleting slides.
+ """
+ return self.part.slide_id
+
+ @property
+ def slide_layout(self) -> SlideLayout:
+ """|SlideLayout| object this slide inherits appearance from."""
+ return self.part.slide_layout
+
+
+class Slides(ParentedElementProxy):
+ """Sequence of slides belonging to an instance of |Presentation|.
+
+ Has list semantics for access to individual slides. Supports indexed access, len(), and
+ iteration.
+ """
+
+ part: PresentationPart # pyright: ignore[reportIncompatibleMethodOverride]
+
+ def __init__(self, sldIdLst: CT_SlideIdList, prs: Presentation):
+ super(Slides, self).__init__(sldIdLst, prs)
+ self._sldIdLst = sldIdLst
+
+ def __getitem__(self, idx: int) -> Slide:
+ """Provide indexed access, (e.g. 'slides[0]')."""
+ try:
+ sldId = self._sldIdLst.sldId_lst[idx]
+ except IndexError:
+ raise IndexError("slide index out of range")
+ return self.part.related_slide(sldId.rId)
+
+ def __iter__(self) -> Iterator[Slide]:
+ """Support iteration, e.g. `for slide in slides:`."""
+ for sldId in self._sldIdLst.sldId_lst:
+ yield self.part.related_slide(sldId.rId)
+
+ def __len__(self) -> int:
+ """Support len() built-in function, e.g. `len(slides) == 4`."""
+ return len(self._sldIdLst)
+
+ def add_slide(self, slide_layout: SlideLayout) -> Slide:
+ """Return a newly added slide that inherits layout from `slide_layout`."""
+ rId, slide = self.part.add_slide(slide_layout)
+ slide.shapes.clone_layout_placeholders(slide_layout)
+ self._sldIdLst.add_sldId(rId)
+ return slide
+
+ def get(self, slide_id: int, default: Slide | None = None) -> Slide | None:
+ """Return the slide identified by int `slide_id` in this presentation.
+
+ Returns `default` if not found.
+ """
+ slide = self.part.get_slide(slide_id)
+ if slide is None:
+ return default
+ return slide
+
+ def index(self, slide: Slide) -> int:
+ """Map `slide` to its zero-based position in this slide sequence.
+
+ Raises |ValueError| on *slide* not present.
+ """
+ for idx, this_slide in enumerate(self):
+ if this_slide == slide:
+ return idx
+ raise ValueError("%s is not in slide collection" % slide)
+
+
+class SlideLayout(_BaseSlide):
+ """Slide layout object.
+
+ Provides access to placeholders, regular shapes, and slide layout-level properties.
+ """
+
+ part: SlideLayoutPart # pyright: ignore[reportIncompatibleMethodOverride]
+
+ def iter_cloneable_placeholders(self) -> Iterator[LayoutPlaceholder]:
+ """Generate layout-placeholders on this slide-layout that should be cloned to a new slide.
+
+ Used when creating a new slide from this slide-layout.
+ """
+ latent_ph_types = (
+ PP_PLACEHOLDER.DATE,
+ PP_PLACEHOLDER.FOOTER,
+ PP_PLACEHOLDER.SLIDE_NUMBER,
+ )
+ for ph in self.placeholders:
+ if ph.element.ph_type not in latent_ph_types:
+ yield ph
+
+ @lazyproperty
+ def placeholders(self) -> LayoutPlaceholders:
+ """Sequence of placeholder shapes in this slide layout.
+
+ Placeholders appear in `idx` order.
+ """
+ return LayoutPlaceholders(self._element.spTree, self)
+
+ @lazyproperty
+ def shapes(self) -> LayoutShapes:
+ """Sequence of shapes appearing on this slide layout."""
+ return LayoutShapes(self._element.spTree, self)
+
+ @property
+ def slide_master(self) -> SlideMaster:
+ """Slide master from which this slide-layout inherits properties."""
+ return self.part.slide_master
+
+ @property
+ def used_by_slides(self):
+ """Tuple of slide objects based on this slide layout."""
+ # ---getting Slides collection requires going around the horn a bit---
+ slides = self.part.package.presentation_part.presentation.slides
+ return tuple(s for s in slides if s.slide_layout == self)
+
+
+class SlideLayouts(ParentedElementProxy):
+ """Sequence of slide layouts belonging to a slide-master.
+
+ Supports indexed access, len(), iteration, index() and remove().
+ """
+
+ part: SlideMasterPart # pyright: ignore[reportIncompatibleMethodOverride]
+
+ def __init__(self, sldLayoutIdLst: CT_SlideLayoutIdList, parent: SlideMaster):
+ super(SlideLayouts, self).__init__(sldLayoutIdLst, parent)
+ self._sldLayoutIdLst = sldLayoutIdLst
+
+ def __getitem__(self, idx: int) -> SlideLayout:
+ """Provides indexed access, e.g. `slide_layouts[2]`."""
+ try:
+ sldLayoutId = self._sldLayoutIdLst.sldLayoutId_lst[idx]
+ except IndexError:
+ raise IndexError("slide layout index out of range")
+ return self.part.related_slide_layout(sldLayoutId.rId)
+
+ def __iter__(self) -> Iterator[SlideLayout]:
+ """Generate each |SlideLayout| in the collection, in sequence."""
+ for sldLayoutId in self._sldLayoutIdLst.sldLayoutId_lst:
+ yield self.part.related_slide_layout(sldLayoutId.rId)
+
+ def __len__(self) -> int:
+ """Support len() built-in function, e.g. `len(slides) == 4`."""
+ return len(self._sldLayoutIdLst)
+
+ def get_by_name(self, name: str, default: SlideLayout | None = None) -> SlideLayout | None:
+ """Return SlideLayout object having `name`, or `default` if not found."""
+ for slide_layout in self:
+ if slide_layout.name == name:
+ return slide_layout
+ return default
+
+ def index(self, slide_layout: SlideLayout) -> int:
+ """Return zero-based index of `slide_layout` in this collection.
+
+ Raises `ValueError` if `slide_layout` is not present in this collection.
+ """
+ for idx, this_layout in enumerate(self):
+ if slide_layout == this_layout:
+ return idx
+ raise ValueError("layout not in this SlideLayouts collection")
+
+ def remove(self, slide_layout: SlideLayout) -> None:
+ """Remove `slide_layout` from the collection.
+
+ Raises ValueError when `slide_layout` is in use; a slide layout which is the basis for one
+ or more slides cannot be removed.
+ """
+ # ---raise if layout is in use---
+ if slide_layout.used_by_slides:
+ raise ValueError("cannot remove slide-layout in use by one or more slides")
+
+ # ---target layout is identified by its index in this collection---
+ target_idx = self.index(slide_layout)
+
+ # --remove layout from p:sldLayoutIds of its master
+ # --this stops layout from showing up, but doesn't remove it from package
+ target_sldLayoutId = self._sldLayoutIdLst.sldLayoutId_lst[target_idx]
+ self._sldLayoutIdLst.remove(target_sldLayoutId)
+
+ # --drop relationship from master to layout
+ # --this removes layout from package, along with everything (only) it refers to,
+ # --including images (not used elsewhere) and hyperlinks
+ slide_layout.slide_master.part.drop_rel(target_sldLayoutId.rId)
+
+
+class SlideMaster(_BaseMaster):
+ """Slide master object.
+
+ Provides access to slide layouts. Access to placeholders, regular shapes, and slide master-level
+ properties is inherited from |_BaseMaster|.
+ """
+
+ _element: CT_SlideMaster # pyright: ignore[reportIncompatibleVariableOverride]
+
+ @lazyproperty
+ def slide_layouts(self) -> SlideLayouts:
+ """|SlideLayouts| object providing access to this slide-master's layouts."""
+ return SlideLayouts(self._element.get_or_add_sldLayoutIdLst(), self)
+
+
+class SlideMasters(ParentedElementProxy):
+ """Sequence of |SlideMaster| objects belonging to a presentation.
+
+ Has list access semantics, supporting indexed access, len(), and iteration.
+ """
+
+ part: PresentationPart # pyright: ignore[reportIncompatibleMethodOverride]
+
+ def __init__(self, sldMasterIdLst: CT_SlideMasterIdList, parent: Presentation):
+ super(SlideMasters, self).__init__(sldMasterIdLst, parent)
+ self._sldMasterIdLst = sldMasterIdLst
+
+ def __getitem__(self, idx: int) -> SlideMaster:
+ """Provides indexed access, e.g. `slide_masters[2]`."""
+ try:
+ sldMasterId = self._sldMasterIdLst.sldMasterId_lst[idx]
+ except IndexError:
+ raise IndexError("slide master index out of range")
+ return self.part.related_slide_master(sldMasterId.rId)
+
+ def __iter__(self):
+ """Generate each |SlideMaster| instance in the collection, in sequence."""
+ for smi in self._sldMasterIdLst.sldMasterId_lst:
+ yield self.part.related_slide_master(smi.rId)
+
+ def __len__(self):
+ """Support len() built-in function, e.g. `len(slide_masters) == 4`."""
+ return len(self._sldMasterIdLst)
+
+
+class _Background(ElementProxy):
+ """Provides access to slide background properties.
+
+ Note that the presence of this object does not by itself imply an
+ explicitly-defined background; a slide with an inherited background still
+ has a |_Background| object.
+ """
+
+ def __init__(self, cSld: CT_CommonSlideData):
+ super(_Background, self).__init__(cSld)
+ self._cSld = cSld
+
+ @lazyproperty
+ def fill(self):
+ """|FillFormat| instance for this background.
+
+ This |FillFormat| object is used to interrogate or specify the fill
+ of the slide background.
+
+ Note that accessing this property is potentially destructive. A slide
+ background can also be specified by a background style reference and
+ accessing this property will remove that reference, if present, and
+ replace it with NoFill. This is frequently the case for a slide
+ master background.
+
+ This is also the case when there is no explicitly defined background
+ (background is inherited); merely accessing this property will cause
+ the background to be set to NoFill and the inheritance link will be
+ interrupted. This is frequently the case for a slide background.
+
+ Of course, if you are accessing this property in order to set the
+ fill, then these changes are of no consequence, but the existing
+ background cannot be reliably interrogated using this property unless
+ you have already established it is an explicit fill.
+
+ If the background is already a fill, then accessing this property
+ makes no changes to the current background.
+ """
+ bgPr = self._cSld.get_or_add_bgPr()
+ return FillFormat.from_fill_parent(bgPr)
diff --git a/.venv/lib/python3.12/site-packages/pptx/spec.py b/.venv/lib/python3.12/site-packages/pptx/spec.py
new file mode 100644
index 00000000..e9d3b7d5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/spec.py
@@ -0,0 +1,632 @@
+"""Mappings from the ISO/IEC 29500 spec.
+
+Some of these are inferred from PowerPoint application behavior
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, TypedDict
+
+from pptx.enum.shapes import MSO_SHAPE
+
+GRAPHIC_DATA_URI_CHART = "http://schemas.openxmlformats.org/drawingml/2006/chart"
+GRAPHIC_DATA_URI_OLEOBJ = "http://schemas.openxmlformats.org/presentationml/2006/ole"
+GRAPHIC_DATA_URI_TABLE = "http://schemas.openxmlformats.org/drawingml/2006/table"
+
+if TYPE_CHECKING:
+ from typing_extensions import TypeAlias
+
+AdjustmentValue: TypeAlias = "tuple[str, int]"
+
+
+class ShapeSpec(TypedDict):
+ basename: str
+ avLst: tuple[AdjustmentValue, ...]
+
+
+# ============================================================================
+# AutoShape type specs
+# ============================================================================
+
+autoshape_types: dict[MSO_SHAPE, ShapeSpec] = {
+ MSO_SHAPE.ACTION_BUTTON_BACK_OR_PREVIOUS: {
+ "basename": "Action Button: Back or Previous",
+ "avLst": (),
+ },
+ MSO_SHAPE.ACTION_BUTTON_BEGINNING: {
+ "basename": "Action Button: Beginning",
+ "avLst": (),
+ },
+ MSO_SHAPE.ACTION_BUTTON_CUSTOM: {"basename": "Action Button: Custom", "avLst": ()},
+ MSO_SHAPE.ACTION_BUTTON_DOCUMENT: {
+ "basename": "Action Button: Document",
+ "avLst": (),
+ },
+ MSO_SHAPE.ACTION_BUTTON_END: {"basename": "Action Button: End", "avLst": ()},
+ MSO_SHAPE.ACTION_BUTTON_FORWARD_OR_NEXT: {
+ "basename": "Action Button: Forward or Next",
+ "avLst": (),
+ },
+ MSO_SHAPE.ACTION_BUTTON_HELP: {"basename": "Action Button: Help", "avLst": ()},
+ MSO_SHAPE.ACTION_BUTTON_HOME: {"basename": "Action Button: Home", "avLst": ()},
+ MSO_SHAPE.ACTION_BUTTON_INFORMATION: {
+ "basename": "Action Button: Information",
+ "avLst": (),
+ },
+ MSO_SHAPE.ACTION_BUTTON_MOVIE: {"basename": "Action Button: Movie", "avLst": ()},
+ MSO_SHAPE.ACTION_BUTTON_RETURN: {"basename": "Action Button: Return", "avLst": ()},
+ MSO_SHAPE.ACTION_BUTTON_SOUND: {"basename": "Action Button: Sound", "avLst": ()},
+ MSO_SHAPE.ARC: {"basename": "Arc", "avLst": (("adj1", 16200000), ("adj2", 0))},
+ MSO_SHAPE.BALLOON: {
+ "basename": "Rounded Rectangular Callout",
+ "avLst": (("adj1", -20833), ("adj2", 62500), ("adj3", 16667)),
+ },
+ MSO_SHAPE.BENT_ARROW: {
+ "basename": "Bent Arrow",
+ "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000), ("adj4", 43750)),
+ },
+ MSO_SHAPE.BENT_UP_ARROW: {
+ "basename": "Bent-Up Arrow",
+ "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000)),
+ },
+ MSO_SHAPE.BEVEL: {"basename": "Bevel", "avLst": (("adj", 12500),)},
+ MSO_SHAPE.BLOCK_ARC: {
+ "basename": "Block Arc",
+ "avLst": (("adj1", 10800000), ("adj2", 0), ("adj3", 25000)),
+ },
+ MSO_SHAPE.CAN: {"basename": "Can", "avLst": (("adj", 25000),)},
+ MSO_SHAPE.CHART_PLUS: {"basename": "Chart Plus", "avLst": ()},
+ MSO_SHAPE.CHART_STAR: {"basename": "Chart Star", "avLst": ()},
+ MSO_SHAPE.CHART_X: {"basename": "Chart X", "avLst": ()},
+ MSO_SHAPE.CHEVRON: {"basename": "Chevron", "avLst": (("adj", 50000),)},
+ MSO_SHAPE.CHORD: {
+ "basename": "Chord",
+ "avLst": (("adj1", 2700000), ("adj2", 16200000)),
+ },
+ MSO_SHAPE.CIRCULAR_ARROW: {
+ "basename": "Circular Arrow",
+ "avLst": (
+ ("adj1", 12500),
+ ("adj2", 1142319),
+ ("adj3", 20457681),
+ ("adj4", 10800000),
+ ("adj5", 12500),
+ ),
+ },
+ MSO_SHAPE.CLOUD: {"basename": "Cloud", "avLst": ()},
+ MSO_SHAPE.CLOUD_CALLOUT: {
+ "basename": "Cloud Callout",
+ "avLst": (("adj1", -20833), ("adj2", 62500)),
+ },
+ MSO_SHAPE.CORNER: {
+ "basename": "Corner",
+ "avLst": (("adj1", 50000), ("adj2", 50000)),
+ },
+ MSO_SHAPE.CORNER_TABS: {"basename": "Corner Tabs", "avLst": ()},
+ MSO_SHAPE.CROSS: {"basename": "Cross", "avLst": (("adj", 25000),)},
+ MSO_SHAPE.CUBE: {"basename": "Cube", "avLst": (("adj", 25000),)},
+ MSO_SHAPE.CURVED_DOWN_ARROW: {
+ "basename": "Curved Down Arrow",
+ "avLst": (("adj1", 25000), ("adj2", 50000), ("adj3", 25000)),
+ },
+ MSO_SHAPE.CURVED_DOWN_RIBBON: {
+ "basename": "Curved Down Ribbon",
+ "avLst": (("adj1", 25000), ("adj2", 50000), ("adj3", 12500)),
+ },
+ MSO_SHAPE.CURVED_LEFT_ARROW: {
+ "basename": "Curved Left Arrow",
+ "avLst": (("adj1", 25000), ("adj2", 50000), ("adj3", 25000)),
+ },
+ MSO_SHAPE.CURVED_RIGHT_ARROW: {
+ "basename": "Curved Right Arrow",
+ "avLst": (("adj1", 25000), ("adj2", 50000), ("adj3", 25000)),
+ },
+ MSO_SHAPE.CURVED_UP_ARROW: {
+ "basename": "Curved Up Arrow",
+ "avLst": (("adj1", 25000), ("adj2", 50000), ("adj3", 25000)),
+ },
+ MSO_SHAPE.CURVED_UP_RIBBON: {
+ "basename": "Curved Up Ribbon",
+ "avLst": (("adj1", 25000), ("adj2", 50000), ("adj3", 12500)),
+ },
+ MSO_SHAPE.DECAGON: {"basename": "Decagon", "avLst": (("vf", 105146),)},
+ MSO_SHAPE.DIAGONAL_STRIPE: {
+ "basename": "Diagonal Stripe",
+ "avLst": (("adj", 50000),),
+ },
+ MSO_SHAPE.DIAMOND: {"basename": "Diamond", "avLst": ()},
+ MSO_SHAPE.DODECAGON: {"basename": "Dodecagon", "avLst": ()},
+ MSO_SHAPE.DONUT: {"basename": "Donut", "avLst": (("adj", 25000),)},
+ MSO_SHAPE.DOUBLE_BRACE: {"basename": "Double Brace", "avLst": (("adj", 8333),)},
+ MSO_SHAPE.DOUBLE_BRACKET: {
+ "basename": "Double Bracket",
+ "avLst": (("adj", 16667),),
+ },
+ MSO_SHAPE.DOUBLE_WAVE: {
+ "basename": "Double Wave",
+ "avLst": (("adj1", 6250), ("adj2", 0)),
+ },
+ MSO_SHAPE.DOWN_ARROW: {
+ "basename": "Down Arrow",
+ "avLst": (("adj1", 50000), ("adj2", 50000)),
+ },
+ MSO_SHAPE.DOWN_ARROW_CALLOUT: {
+ "basename": "Down Arrow Callout",
+ "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000), ("adj4", 64977)),
+ },
+ MSO_SHAPE.DOWN_RIBBON: {
+ "basename": "Down Ribbon",
+ "avLst": (("adj1", 16667), ("adj2", 50000)),
+ },
+ MSO_SHAPE.EXPLOSION1: {"basename": "Explosion", "avLst": ()},
+ MSO_SHAPE.EXPLOSION2: {"basename": "Explosion", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_ALTERNATE_PROCESS: {
+ "basename": "Alternate process",
+ "avLst": (),
+ },
+ MSO_SHAPE.FLOWCHART_CARD: {"basename": "Card", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_COLLATE: {"basename": "Collate", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_CONNECTOR: {"basename": "Connector", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_DATA: {"basename": "Data", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_DECISION: {"basename": "Decision", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_DELAY: {"basename": "Delay", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_DIRECT_ACCESS_STORAGE: {
+ "basename": "Direct Access Storage",
+ "avLst": (),
+ },
+ MSO_SHAPE.FLOWCHART_DISPLAY: {"basename": "Display", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_DOCUMENT: {"basename": "Document", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_EXTRACT: {"basename": "Extract", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_INTERNAL_STORAGE: {"basename": "Internal Storage", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_MAGNETIC_DISK: {"basename": "Magnetic Disk", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_MANUAL_INPUT: {"basename": "Manual Input", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_MANUAL_OPERATION: {"basename": "Manual Operation", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_MERGE: {"basename": "Merge", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_MULTIDOCUMENT: {"basename": "Multidocument", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_OFFLINE_STORAGE: {"basename": "Offline Storage", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_OFFPAGE_CONNECTOR: {
+ "basename": "Off-page Connector",
+ "avLst": (),
+ },
+ MSO_SHAPE.FLOWCHART_OR: {"basename": "Or", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_PREDEFINED_PROCESS: {
+ "basename": "Predefined Process",
+ "avLst": (),
+ },
+ MSO_SHAPE.FLOWCHART_PREPARATION: {"basename": "Preparation", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_PROCESS: {"basename": "Process", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_PUNCHED_TAPE: {"basename": "Punched Tape", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_SEQUENTIAL_ACCESS_STORAGE: {
+ "basename": "Sequential Access Storage",
+ "avLst": (),
+ },
+ MSO_SHAPE.FLOWCHART_SORT: {"basename": "Sort", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_STORED_DATA: {"basename": "Stored Data", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_SUMMING_JUNCTION: {"basename": "Summing Junction", "avLst": ()},
+ MSO_SHAPE.FLOWCHART_TERMINATOR: {"basename": "Terminator", "avLst": ()},
+ MSO_SHAPE.FOLDED_CORNER: {"basename": "Folded Corner", "avLst": ()},
+ MSO_SHAPE.FRAME: {"basename": "Frame", "avLst": (("adj1", 12500),)},
+ MSO_SHAPE.FUNNEL: {"basename": "Funnel", "avLst": ()},
+ MSO_SHAPE.GEAR_6: {
+ "basename": "Gear 6",
+ "avLst": (("adj1", 15000), ("adj2", 3526)),
+ },
+ MSO_SHAPE.GEAR_9: {
+ "basename": "Gear 9",
+ "avLst": (("adj1", 10000), ("adj2", 1763)),
+ },
+ MSO_SHAPE.HALF_FRAME: {
+ "basename": "Half Frame",
+ "avLst": (("adj1", 33333), ("adj2", 33333)),
+ },
+ MSO_SHAPE.HEART: {"basename": "Heart", "avLst": ()},
+ MSO_SHAPE.HEPTAGON: {
+ "basename": "Heptagon",
+ "avLst": (("hf", 102572), ("vf", 105210)),
+ },
+ MSO_SHAPE.HEXAGON: {
+ "basename": "Hexagon",
+ "avLst": (("adj", 25000), ("vf", 115470)),
+ },
+ MSO_SHAPE.HORIZONTAL_SCROLL: {
+ "basename": "Horizontal Scroll",
+ "avLst": (("adj", 12500),),
+ },
+ MSO_SHAPE.ISOSCELES_TRIANGLE: {
+ "basename": "Isosceles Triangle",
+ "avLst": (("adj", 50000),),
+ },
+ MSO_SHAPE.LEFT_ARROW: {
+ "basename": "Left Arrow",
+ "avLst": (("adj1", 50000), ("adj2", 50000)),
+ },
+ MSO_SHAPE.LEFT_ARROW_CALLOUT: {
+ "basename": "Left Arrow Callout",
+ "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000), ("adj4", 64977)),
+ },
+ MSO_SHAPE.LEFT_BRACE: {
+ "basename": "Left Brace",
+ "avLst": (("adj1", 8333), ("adj2", 50000)),
+ },
+ MSO_SHAPE.LEFT_BRACKET: {"basename": "Left Bracket", "avLst": (("adj", 8333),)},
+ MSO_SHAPE.LEFT_CIRCULAR_ARROW: {
+ "basename": "Left Circular Arrow",
+ "avLst": (
+ ("adj1", 12500),
+ ("adj2", -1142319),
+ ("adj3", 1142319),
+ ("adj4", 10800000),
+ ("adj5", 12500),
+ ),
+ },
+ MSO_SHAPE.LEFT_RIGHT_ARROW: {
+ "basename": "Left-Right Arrow",
+ "avLst": (("adj1", 50000), ("adj2", 50000)),
+ },
+ MSO_SHAPE.LEFT_RIGHT_ARROW_CALLOUT: {
+ "basename": "Left-Right Arrow Callout",
+ "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000), ("adj4", 48123)),
+ },
+ MSO_SHAPE.LEFT_RIGHT_CIRCULAR_ARROW: {
+ "basename": "Left Right Circular Arrow",
+ "avLst": (
+ ("adj1", 12500),
+ ("adj2", 1142319),
+ ("adj3", 20457681),
+ ("adj4", 11942319),
+ ("adj5", 12500),
+ ),
+ },
+ MSO_SHAPE.LEFT_RIGHT_RIBBON: {
+ "basename": "Left Right Ribbon",
+ "avLst": (("adj1", 50000), ("adj2", 50000), ("adj3", 16667)),
+ },
+ MSO_SHAPE.LEFT_RIGHT_UP_ARROW: {
+ "basename": "Left-Right-Up Arrow",
+ "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000)),
+ },
+ MSO_SHAPE.LEFT_UP_ARROW: {
+ "basename": "Left-Up Arrow",
+ "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000)),
+ },
+ MSO_SHAPE.LIGHTNING_BOLT: {"basename": "Lightning Bolt", "avLst": ()},
+ MSO_SHAPE.LINE_CALLOUT_1: {
+ "basename": "Line Callout 1",
+ "avLst": (("adj1", 18750), ("adj2", -8333), ("adj3", 112500), ("adj4", -38333)),
+ },
+ MSO_SHAPE.LINE_CALLOUT_1_ACCENT_BAR: {
+ "basename": "Line Callout 1 (Accent Bar)",
+ "avLst": (("adj1", 18750), ("adj2", -8333), ("adj3", 112500), ("adj4", -38333)),
+ },
+ MSO_SHAPE.LINE_CALLOUT_1_BORDER_AND_ACCENT_BAR: {
+ "basename": "Line Callout 1 (Border and Accent Bar)",
+ "avLst": (("adj1", 18750), ("adj2", -8333), ("adj3", 112500), ("adj4", -38333)),
+ },
+ MSO_SHAPE.LINE_CALLOUT_1_NO_BORDER: {
+ "basename": "Line Callout 1 (No Border)",
+ "avLst": (("adj1", 18750), ("adj2", -8333), ("adj3", 112500), ("adj4", -38333)),
+ },
+ MSO_SHAPE.LINE_CALLOUT_2: {
+ "basename": "Line Callout 2",
+ "avLst": (
+ ("adj1", 18750),
+ ("adj2", -8333),
+ ("adj3", 18750),
+ ("adj4", -16667),
+ ("adj5", 112500),
+ ("adj6", -46667),
+ ),
+ },
+ MSO_SHAPE.LINE_CALLOUT_2_ACCENT_BAR: {
+ "basename": "Line Callout 2 (Accent Bar)",
+ "avLst": (
+ ("adj1", 18750),
+ ("adj2", -8333),
+ ("adj3", 18750),
+ ("adj4", -16667),
+ ("adj5", 112500),
+ ("adj6", -46667),
+ ),
+ },
+ MSO_SHAPE.LINE_CALLOUT_2_BORDER_AND_ACCENT_BAR: {
+ "basename": "Line Callout 2 (Border and Accent Bar)",
+ "avLst": (
+ ("adj1", 18750),
+ ("adj2", -8333),
+ ("adj3", 18750),
+ ("adj4", -16667),
+ ("adj5", 112500),
+ ("adj6", -46667),
+ ),
+ },
+ MSO_SHAPE.LINE_CALLOUT_2_NO_BORDER: {
+ "basename": "Line Callout 2 (No Border)",
+ "avLst": (
+ ("adj1", 18750),
+ ("adj2", -8333),
+ ("adj3", 18750),
+ ("adj4", -16667),
+ ("adj5", 112500),
+ ("adj6", -46667),
+ ),
+ },
+ MSO_SHAPE.LINE_CALLOUT_3: {
+ "basename": "Line Callout 3",
+ "avLst": (
+ ("adj1", 18750),
+ ("adj2", -8333),
+ ("adj3", 18750),
+ ("adj4", -16667),
+ ("adj5", 100000),
+ ("adj6", -16667),
+ ("adj7", 112963),
+ ("adj8", -8333),
+ ),
+ },
+ MSO_SHAPE.LINE_CALLOUT_3_ACCENT_BAR: {
+ "basename": "Line Callout 3 (Accent Bar)",
+ "avLst": (
+ ("adj1", 18750),
+ ("adj2", -8333),
+ ("adj3", 18750),
+ ("adj4", -16667),
+ ("adj5", 100000),
+ ("adj6", -16667),
+ ("adj7", 112963),
+ ("adj8", -8333),
+ ),
+ },
+ MSO_SHAPE.LINE_CALLOUT_3_BORDER_AND_ACCENT_BAR: {
+ "basename": "Line Callout 3 (Border and Accent Bar)",
+ "avLst": (
+ ("adj1", 18750),
+ ("adj2", -8333),
+ ("adj3", 18750),
+ ("adj4", -16667),
+ ("adj5", 100000),
+ ("adj6", -16667),
+ ("adj7", 112963),
+ ("adj8", -8333),
+ ),
+ },
+ MSO_SHAPE.LINE_CALLOUT_3_NO_BORDER: {
+ "basename": "Line Callout 3 (No Border)",
+ "avLst": (
+ ("adj1", 18750),
+ ("adj2", -8333),
+ ("adj3", 18750),
+ ("adj4", -16667),
+ ("adj5", 100000),
+ ("adj6", -16667),
+ ("adj7", 112963),
+ ("adj8", -8333),
+ ),
+ },
+ MSO_SHAPE.LINE_CALLOUT_4: {
+ "basename": "Line Callout 3",
+ "avLst": (
+ ("adj1", 18750),
+ ("adj2", -8333),
+ ("adj3", 18750),
+ ("adj4", -16667),
+ ("adj5", 100000),
+ ("adj6", -16667),
+ ("adj7", 112963),
+ ("adj8", -8333),
+ ),
+ },
+ MSO_SHAPE.LINE_CALLOUT_4_ACCENT_BAR: {
+ "basename": "Line Callout 3 (Accent Bar)",
+ "avLst": (
+ ("adj1", 18750),
+ ("adj2", -8333),
+ ("adj3", 18750),
+ ("adj4", -16667),
+ ("adj5", 100000),
+ ("adj6", -16667),
+ ("adj7", 112963),
+ ("adj8", -8333),
+ ),
+ },
+ MSO_SHAPE.LINE_CALLOUT_4_BORDER_AND_ACCENT_BAR: {
+ "basename": "Line Callout 3 (Border and Accent Bar)",
+ "avLst": (
+ ("adj1", 18750),
+ ("adj2", -8333),
+ ("adj3", 18750),
+ ("adj4", -16667),
+ ("adj5", 100000),
+ ("adj6", -16667),
+ ("adj7", 112963),
+ ("adj8", -8333),
+ ),
+ },
+ MSO_SHAPE.LINE_CALLOUT_4_NO_BORDER: {
+ "basename": "Line Callout 3 (No Border)",
+ "avLst": (
+ ("adj1", 18750),
+ ("adj2", -8333),
+ ("adj3", 18750),
+ ("adj4", -16667),
+ ("adj5", 100000),
+ ("adj6", -16667),
+ ("adj7", 112963),
+ ("adj8", -8333),
+ ),
+ },
+ MSO_SHAPE.LINE_INVERSE: {"basename": "Straight Connector", "avLst": ()},
+ MSO_SHAPE.MATH_DIVIDE: {
+ "basename": "Division",
+ "avLst": (("adj1", 23520), ("adj2", 5880), ("adj3", 11760)),
+ },
+ MSO_SHAPE.MATH_EQUAL: {
+ "basename": "Equal",
+ "avLst": (("adj1", 23520), ("adj2", 11760)),
+ },
+ MSO_SHAPE.MATH_MINUS: {"basename": "Minus", "avLst": (("adj1", 23520),)},
+ MSO_SHAPE.MATH_MULTIPLY: {"basename": "Multiply", "avLst": (("adj1", 23520),)},
+ MSO_SHAPE.MATH_NOT_EQUAL: {
+ "basename": "Not Equal",
+ "avLst": (("adj1", 23520), ("adj2", 6600000), ("adj3", 11760)),
+ },
+ MSO_SHAPE.MATH_PLUS: {"basename": "Plus", "avLst": (("adj1", 23520),)},
+ MSO_SHAPE.MOON: {"basename": "Moon", "avLst": (("adj", 50000),)},
+ MSO_SHAPE.NON_ISOSCELES_TRAPEZOID: {
+ "basename": "Non-isosceles Trapezoid",
+ "avLst": (("adj1", 25000), ("adj2", 25000)),
+ },
+ MSO_SHAPE.NOTCHED_RIGHT_ARROW: {
+ "basename": "Notched Right Arrow",
+ "avLst": (("adj1", 50000), ("adj2", 50000)),
+ },
+ MSO_SHAPE.NO_SYMBOL: {"basename": '"No" Symbol', "avLst": (("adj", 18750),)},
+ MSO_SHAPE.OCTAGON: {"basename": "Octagon", "avLst": (("adj", 29289),)},
+ MSO_SHAPE.OVAL: {"basename": "Oval", "avLst": ()},
+ MSO_SHAPE.OVAL_CALLOUT: {
+ "basename": "Oval Callout",
+ "avLst": (("adj1", -20833), ("adj2", 62500)),
+ },
+ MSO_SHAPE.PARALLELOGRAM: {"basename": "Parallelogram", "avLst": (("adj", 25000),)},
+ MSO_SHAPE.PENTAGON: {"basename": "Pentagon", "avLst": (("adj", 50000),)},
+ MSO_SHAPE.PIE: {"basename": "Pie", "avLst": (("adj1", 0), ("adj2", 16200000))},
+ MSO_SHAPE.PIE_WEDGE: {"basename": "Pie", "avLst": ()},
+ MSO_SHAPE.PLAQUE: {"basename": "Plaque", "avLst": (("adj", 16667),)},
+ MSO_SHAPE.PLAQUE_TABS: {"basename": "Plaque Tabs", "avLst": ()},
+ MSO_SHAPE.QUAD_ARROW: {
+ "basename": "Quad Arrow",
+ "avLst": (("adj1", 22500), ("adj2", 22500), ("adj3", 22500)),
+ },
+ MSO_SHAPE.QUAD_ARROW_CALLOUT: {
+ "basename": "Quad Arrow Callout",
+ "avLst": (("adj1", 18515), ("adj2", 18515), ("adj3", 18515), ("adj4", 48123)),
+ },
+ MSO_SHAPE.RECTANGLE: {"basename": "Rectangle", "avLst": ()},
+ MSO_SHAPE.RECTANGULAR_CALLOUT: {
+ "basename": "Rectangular Callout",
+ "avLst": (("adj1", -20833), ("adj2", 62500)),
+ },
+ MSO_SHAPE.REGULAR_PENTAGON: {
+ "basename": "Regular Pentagon",
+ "avLst": (("hf", 105146), ("vf", 110557)),
+ },
+ MSO_SHAPE.RIGHT_ARROW: {
+ "basename": "Right Arrow",
+ "avLst": (("adj1", 50000), ("adj2", 50000)),
+ },
+ MSO_SHAPE.RIGHT_ARROW_CALLOUT: {
+ "basename": "Right Arrow Callout",
+ "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000), ("adj4", 64977)),
+ },
+ MSO_SHAPE.RIGHT_BRACE: {
+ "basename": "Right Brace",
+ "avLst": (("adj1", 8333), ("adj2", 50000)),
+ },
+ MSO_SHAPE.RIGHT_BRACKET: {"basename": "Right Bracket", "avLst": (("adj", 8333),)},
+ MSO_SHAPE.RIGHT_TRIANGLE: {"basename": "Right Triangle", "avLst": ()},
+ MSO_SHAPE.ROUNDED_RECTANGLE: {
+ "basename": "Rounded Rectangle",
+ "avLst": (("adj", 16667),),
+ },
+ MSO_SHAPE.ROUNDED_RECTANGULAR_CALLOUT: {
+ "basename": "Rounded Rectangular Callout",
+ "avLst": (("adj1", -20833), ("adj2", 62500), ("adj3", 16667)),
+ },
+ MSO_SHAPE.ROUND_1_RECTANGLE: {
+ "basename": "Round Single Corner Rectangle",
+ "avLst": (("adj", 16667),),
+ },
+ MSO_SHAPE.ROUND_2_DIAG_RECTANGLE: {
+ "basename": "Round Diagonal Corner Rectangle",
+ "avLst": (("adj1", 16667), ("adj2", 0)),
+ },
+ MSO_SHAPE.ROUND_2_SAME_RECTANGLE: {
+ "basename": "Round Same Side Corner Rectangle",
+ "avLst": (("adj1", 16667), ("adj2", 0)),
+ },
+ MSO_SHAPE.SMILEY_FACE: {"basename": "Smiley Face", "avLst": (("adj", 4653),)},
+ MSO_SHAPE.SNIP_1_RECTANGLE: {
+ "basename": "Snip Single Corner Rectangle",
+ "avLst": (("adj", 16667),),
+ },
+ MSO_SHAPE.SNIP_2_DIAG_RECTANGLE: {
+ "basename": "Snip Diagonal Corner Rectangle",
+ "avLst": (("adj1", 0), ("adj2", 16667)),
+ },
+ MSO_SHAPE.SNIP_2_SAME_RECTANGLE: {
+ "basename": "Snip Same Side Corner Rectangle",
+ "avLst": (("adj1", 16667), ("adj2", 0)),
+ },
+ MSO_SHAPE.SNIP_ROUND_RECTANGLE: {
+ "basename": "Snip and Round Single Corner Rectangle",
+ "avLst": (("adj1", 16667), ("adj2", 16667)),
+ },
+ MSO_SHAPE.SQUARE_TABS: {"basename": "Square Tabs", "avLst": ()},
+ MSO_SHAPE.STAR_10_POINT: {
+ "basename": "10-Point Star",
+ "avLst": (("adj", 42533), ("hf", 105146)),
+ },
+ MSO_SHAPE.STAR_12_POINT: {"basename": "12-Point Star", "avLst": (("adj", 37500),)},
+ MSO_SHAPE.STAR_16_POINT: {"basename": "16-Point Star", "avLst": (("adj", 37500),)},
+ MSO_SHAPE.STAR_24_POINT: {"basename": "24-Point Star", "avLst": (("adj", 37500),)},
+ MSO_SHAPE.STAR_32_POINT: {"basename": "32-Point Star", "avLst": (("adj", 37500),)},
+ MSO_SHAPE.STAR_4_POINT: {"basename": "4-Point Star", "avLst": (("adj", 12500),)},
+ MSO_SHAPE.STAR_5_POINT: {
+ "basename": "5-Point Star",
+ "avLst": (("adj", 19098), ("hf", 105146), ("vf", 110557)),
+ },
+ MSO_SHAPE.STAR_6_POINT: {
+ "basename": "6-Point Star",
+ "avLst": (("adj", 28868), ("hf", 115470)),
+ },
+ MSO_SHAPE.STAR_7_POINT: {
+ "basename": "7-Point Star",
+ "avLst": (("adj", 34601), ("hf", 102572), ("vf", 105210)),
+ },
+ MSO_SHAPE.STAR_8_POINT: {"basename": "8-Point Star", "avLst": (("adj", 37500),)},
+ MSO_SHAPE.STRIPED_RIGHT_ARROW: {
+ "basename": "Striped Right Arrow",
+ "avLst": (("adj1", 50000), ("adj2", 50000)),
+ },
+ MSO_SHAPE.SUN: {"basename": "Sun", "avLst": (("adj", 25000),)},
+ MSO_SHAPE.SWOOSH_ARROW: {
+ "basename": "Swoosh Arrow",
+ "avLst": (("adj1", 25000), ("adj2", 16667)),
+ },
+ MSO_SHAPE.TEAR: {"basename": "Teardrop", "avLst": (("adj", 100000),)},
+ MSO_SHAPE.TRAPEZOID: {"basename": "Trapezoid", "avLst": (("adj", 25000),)},
+ MSO_SHAPE.UP_ARROW: {
+ "basename": "Up Arrow",
+ "avLst": (("adj1", 50000), ("adj2", 50000)),
+ },
+ MSO_SHAPE.UP_ARROW_CALLOUT: {
+ "basename": "Up Arrow Callout",
+ "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000), ("adj4", 64977)),
+ },
+ MSO_SHAPE.UP_DOWN_ARROW: {
+ "basename": "Up-Down Arrow",
+ "avLst": (("adj1", 50000), ("adj1", 50000), ("adj2", 50000), ("adj2", 50000)),
+ },
+ MSO_SHAPE.UP_DOWN_ARROW_CALLOUT: {
+ "basename": "Up-Down Arrow Callout",
+ "avLst": (("adj1", 25000), ("adj2", 25000), ("adj3", 25000), ("adj4", 48123)),
+ },
+ MSO_SHAPE.UP_RIBBON: {
+ "basename": "Up Ribbon",
+ "avLst": (("adj1", 16667), ("adj2", 50000)),
+ },
+ MSO_SHAPE.U_TURN_ARROW: {
+ "basename": "U-Turn Arrow",
+ "avLst": (
+ ("adj1", 25000),
+ ("adj2", 25000),
+ ("adj3", 25000),
+ ("adj4", 43750),
+ ("adj5", 75000),
+ ),
+ },
+ MSO_SHAPE.VERTICAL_SCROLL: {
+ "basename": "Vertical Scroll",
+ "avLst": (("adj", 12500),),
+ },
+ MSO_SHAPE.WAVE: {"basename": "Wave", "avLst": (("adj1", 12500), ("adj2", 0))},
+}
diff --git a/.venv/lib/python3.12/site-packages/pptx/table.py b/.venv/lib/python3.12/site-packages/pptx/table.py
new file mode 100644
index 00000000..3bdf54ba
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/table.py
@@ -0,0 +1,496 @@
+"""Table-related objects such as Table and Cell."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Iterator
+
+from pptx.dml.fill import FillFormat
+from pptx.oxml.table import TcRange
+from pptx.shapes import Subshape
+from pptx.text.text import TextFrame
+from pptx.util import Emu, lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.enum.text import MSO_VERTICAL_ANCHOR
+ from pptx.oxml.table import CT_Table, CT_TableCell, CT_TableCol, CT_TableRow
+ from pptx.parts.slide import BaseSlidePart
+ from pptx.shapes.graphfrm import GraphicFrame
+ from pptx.types import ProvidesPart
+ from pptx.util import Length
+
+
+class Table(object):
+ """A DrawingML table object.
+
+ Not intended to be constructed directly, use
+ :meth:`.Slide.shapes.add_table` to add a table to a slide.
+ """
+
+ def __init__(self, tbl: CT_Table, graphic_frame: GraphicFrame):
+ super(Table, self).__init__()
+ self._tbl = tbl
+ self._graphic_frame = graphic_frame
+
+ def cell(self, row_idx: int, col_idx: int) -> _Cell:
+ """Return cell at `row_idx`, `col_idx`.
+
+ Return value is an instance of |_Cell|. `row_idx` and `col_idx` are zero-based, e.g.
+ cell(0, 0) is the top, left cell in the table.
+ """
+ return _Cell(self._tbl.tc(row_idx, col_idx), self)
+
+ @lazyproperty
+ def columns(self) -> _ColumnCollection:
+ """|_ColumnCollection| instance for this table.
+
+ Provides access to |_Column| objects representing the table's columns. |_Column| objects
+ are accessed using list notation, e.g. `col = tbl.columns[0]`.
+ """
+ return _ColumnCollection(self._tbl, self)
+
+ @property
+ def first_col(self) -> bool:
+ """When `True`, indicates first column should have distinct formatting.
+
+ Read/write. Distinct formatting is used, for example, when the first column contains row
+ headings (is a side-heading column).
+ """
+ return self._tbl.firstCol
+
+ @first_col.setter
+ def first_col(self, value: bool):
+ self._tbl.firstCol = value
+
+ @property
+ def first_row(self) -> bool:
+ """When `True`, indicates first row should have distinct formatting.
+
+ Read/write. Distinct formatting is used, for example, when the first row contains column
+ headings.
+ """
+ return self._tbl.firstRow
+
+ @first_row.setter
+ def first_row(self, value: bool):
+ self._tbl.firstRow = value
+
+ @property
+ def horz_banding(self) -> bool:
+ """When `True`, indicates rows should have alternating shading.
+
+ Read/write. Used to allow rows to be traversed more easily without losing track of which
+ row is being read.
+ """
+ return self._tbl.bandRow
+
+ @horz_banding.setter
+ def horz_banding(self, value: bool):
+ self._tbl.bandRow = value
+
+ def iter_cells(self) -> Iterator[_Cell]:
+ """Generate _Cell object for each cell in this table.
+
+ Each grid cell is generated in left-to-right, top-to-bottom order.
+ """
+ return (_Cell(tc, self) for tc in self._tbl.iter_tcs())
+
+ @property
+ def last_col(self) -> bool:
+ """When `True`, indicates the rightmost column should have distinct formatting.
+
+ Read/write. Used, for example, when a row totals column appears at the far right of the
+ table.
+ """
+ return self._tbl.lastCol
+
+ @last_col.setter
+ def last_col(self, value: bool):
+ self._tbl.lastCol = value
+
+ @property
+ def last_row(self) -> bool:
+ """When `True`, indicates the bottom row should have distinct formatting.
+
+ Read/write. Used, for example, when a totals row appears as the bottom row.
+ """
+ return self._tbl.lastRow
+
+ @last_row.setter
+ def last_row(self, value: bool):
+ self._tbl.lastRow = value
+
+ def notify_height_changed(self) -> None:
+ """Called by a row when its height changes.
+
+ Triggers the graphic frame to recalculate its total height (as the sum of the row
+ heights).
+ """
+ new_table_height = Emu(sum([row.height for row in self.rows]))
+ self._graphic_frame.height = new_table_height
+
+ def notify_width_changed(self) -> None:
+ """Called by a column when its width changes.
+
+ Triggers the graphic frame to recalculate its total width (as the sum of the column
+ widths).
+ """
+ new_table_width = Emu(sum([col.width for col in self.columns]))
+ self._graphic_frame.width = new_table_width
+
+ @property
+ def part(self) -> BaseSlidePart:
+ """The package part containing this table."""
+ return self._graphic_frame.part
+
+ @lazyproperty
+ def rows(self):
+ """|_RowCollection| instance for this table.
+
+ Provides access to |_Row| objects representing the table's rows. |_Row| objects are
+ accessed using list notation, e.g. `col = tbl.rows[0]`.
+ """
+ return _RowCollection(self._tbl, self)
+
+ @property
+ def vert_banding(self) -> bool:
+ """When `True`, indicates columns should have alternating shading.
+
+ Read/write. Used to allow columns to be traversed more easily without losing track of
+ which column is being read.
+ """
+ return self._tbl.bandCol
+
+ @vert_banding.setter
+ def vert_banding(self, value: bool):
+ self._tbl.bandCol = value
+
+
+class _Cell(Subshape):
+ """Table cell"""
+
+ def __init__(self, tc: CT_TableCell, parent: ProvidesPart):
+ super(_Cell, self).__init__(parent)
+ self._tc = tc
+
+ def __eq__(self, other: object) -> bool:
+ """|True| if this object proxies the same element as `other`.
+
+ Equality for proxy objects is defined as referring to the same XML element, whether or not
+ they are the same proxy object instance.
+ """
+ if not isinstance(other, type(self)):
+ return False
+ return self._tc is other._tc
+
+ def __ne__(self, other: object) -> bool:
+ if not isinstance(other, type(self)):
+ return True
+ return self._tc is not other._tc
+
+ @lazyproperty
+ def fill(self) -> FillFormat:
+ """|FillFormat| instance for this cell.
+
+ Provides access to fill properties such as foreground color.
+ """
+ tcPr = self._tc.get_or_add_tcPr()
+ return FillFormat.from_fill_parent(tcPr)
+
+ @property
+ def is_merge_origin(self) -> bool:
+ """True if this cell is the top-left grid cell in a merged cell."""
+ return self._tc.is_merge_origin
+
+ @property
+ def is_spanned(self) -> bool:
+ """True if this cell is spanned by a merge-origin cell.
+
+ A merge-origin cell "spans" the other grid cells in its merge range, consuming their area
+ and "shadowing" the spanned grid cells.
+
+ Note this value is |False| for a merge-origin cell. A merge-origin cell spans other grid
+ cells, but is not itself a spanned cell.
+ """
+ return self._tc.is_spanned
+
+ @property
+ def margin_left(self) -> Length:
+ """Left margin of cells.
+
+ Read/write. If assigned |None|, the default value is used, 0.1 inches for left and right
+ margins and 0.05 inches for top and bottom.
+ """
+ return self._tc.marL
+
+ @margin_left.setter
+ def margin_left(self, margin_left: Length | None):
+ self._validate_margin_value(margin_left)
+ self._tc.marL = margin_left
+
+ @property
+ def margin_right(self) -> Length:
+ """Right margin of cell."""
+ return self._tc.marR
+
+ @margin_right.setter
+ def margin_right(self, margin_right: Length | None):
+ self._validate_margin_value(margin_right)
+ self._tc.marR = margin_right
+
+ @property
+ def margin_top(self) -> Length:
+ """Top margin of cell."""
+ return self._tc.marT
+
+ @margin_top.setter
+ def margin_top(self, margin_top: Length | None):
+ self._validate_margin_value(margin_top)
+ self._tc.marT = margin_top
+
+ @property
+ def margin_bottom(self) -> Length:
+ """Bottom margin of cell."""
+ return self._tc.marB
+
+ @margin_bottom.setter
+ def margin_bottom(self, margin_bottom: Length | None):
+ self._validate_margin_value(margin_bottom)
+ self._tc.marB = margin_bottom
+
+ def merge(self, other_cell: _Cell) -> None:
+ """Create merged cell from this cell to `other_cell`.
+
+ This cell and `other_cell` specify opposite corners of the merged cell range. Either
+ diagonal of the cell region may be specified in either order, e.g. self=bottom-right,
+ other_cell=top-left, etc.
+
+ Raises |ValueError| if the specified range already contains merged cells anywhere within
+ its extents or if `other_cell` is not in the same table as `self`.
+ """
+ tc_range = TcRange(self._tc, other_cell._tc)
+
+ if not tc_range.in_same_table:
+ raise ValueError("other_cell from different table")
+ if tc_range.contains_merged_cell:
+ raise ValueError("range contains one or more merged cells")
+
+ tc_range.move_content_to_origin()
+
+ row_count, col_count = tc_range.dimensions
+
+ for tc in tc_range.iter_top_row_tcs():
+ tc.rowSpan = row_count
+ for tc in tc_range.iter_left_col_tcs():
+ tc.gridSpan = col_count
+ for tc in tc_range.iter_except_left_col_tcs():
+ tc.hMerge = True
+ for tc in tc_range.iter_except_top_row_tcs():
+ tc.vMerge = True
+
+ @property
+ def span_height(self) -> int:
+ """int count of rows spanned by this cell.
+
+ The value of this property may be misleading (often 1) on cells where `.is_merge_origin`
+ is not |True|, since only a merge-origin cell contains complete span information. This
+ property is only intended for use on cells known to be a merge origin by testing
+ `.is_merge_origin`.
+ """
+ return self._tc.rowSpan
+
+ @property
+ def span_width(self) -> int:
+ """int count of columns spanned by this cell.
+
+ The value of this property may be misleading (often 1) on cells where `.is_merge_origin`
+ is not |True|, since only a merge-origin cell contains complete span information. This
+ property is only intended for use on cells known to be a merge origin by testing
+ `.is_merge_origin`.
+ """
+ return self._tc.gridSpan
+
+ def split(self) -> None:
+ """Remove merge from this (merge-origin) cell.
+
+ The merged cell represented by this object will be "unmerged", yielding a separate
+ unmerged cell for each grid cell previously spanned by this merge.
+
+ Raises |ValueError| when this cell is not a merge-origin cell. Test with
+ `.is_merge_origin` before calling.
+ """
+ if not self.is_merge_origin:
+ raise ValueError("not a merge-origin cell; only a merge-origin cell can be sp" "lit")
+
+ tc_range = TcRange.from_merge_origin(self._tc)
+
+ for tc in tc_range.iter_tcs():
+ tc.rowSpan = tc.gridSpan = 1
+ tc.hMerge = tc.vMerge = False
+
+ @property
+ def text(self) -> str:
+ """Textual content of cell as a single string.
+
+ The returned string will contain a newline character (`"\\n"`) separating each paragraph
+ and a vertical-tab (`"\\v"`) character for each line break (soft carriage return) in the
+ cell's text.
+
+ Assignment to `text` replaces all text currently contained in the cell. A newline
+ character (`"\\n"`) in the assigned text causes a new paragraph to be started. A
+ vertical-tab (`"\\v"`) character in the assigned text causes a line-break (soft
+ carriage-return) to be inserted. (The vertical-tab character appears in clipboard text
+ copied from PowerPoint as its encoding of line-breaks.)
+ """
+ return self.text_frame.text
+
+ @text.setter
+ def text(self, text: str):
+ self.text_frame.text = text
+
+ @property
+ def text_frame(self) -> TextFrame:
+ """|TextFrame| containing the text that appears in the cell."""
+ txBody = self._tc.get_or_add_txBody()
+ return TextFrame(txBody, self)
+
+ @property
+ def vertical_anchor(self) -> MSO_VERTICAL_ANCHOR | None:
+ """Vertical alignment of this cell.
+
+ This value is a member of the :ref:`MsoVerticalAnchor` enumeration or |None|. A value of
+ |None| indicates the cell has no explicitly applied vertical anchor setting and its
+ effective value is inherited from its style-hierarchy ancestors.
+
+ Assigning |None| to this property causes any explicitly applied vertical anchor setting to
+ be cleared and inheritance of its effective value to be restored.
+ """
+ return self._tc.anchor
+
+ @vertical_anchor.setter
+ def vertical_anchor(self, mso_anchor_idx: MSO_VERTICAL_ANCHOR | None):
+ self._tc.anchor = mso_anchor_idx
+
+ @staticmethod
+ def _validate_margin_value(margin_value: Length | None) -> None:
+ """Raise ValueError if `margin_value` is not a positive integer value or |None|."""
+ if not isinstance(margin_value, int) and margin_value is not None:
+ tmpl = "margin value must be integer or None, got '%s'"
+ raise TypeError(tmpl % margin_value)
+
+
+class _Column(Subshape):
+ """Table column"""
+
+ def __init__(self, gridCol: CT_TableCol, parent: _ColumnCollection):
+ super(_Column, self).__init__(parent)
+ self._parent = parent
+ self._gridCol = gridCol
+
+ @property
+ def width(self) -> Length:
+ """Width of column in EMU."""
+ return self._gridCol.w
+
+ @width.setter
+ def width(self, width: Length):
+ self._gridCol.w = width
+ self._parent.notify_width_changed()
+
+
+class _Row(Subshape):
+ """Table row"""
+
+ def __init__(self, tr: CT_TableRow, parent: _RowCollection):
+ super(_Row, self).__init__(parent)
+ self._parent = parent
+ self._tr = tr
+
+ @property
+ def cells(self):
+ """Read-only reference to collection of cells in row.
+
+ An individual cell is referenced using list notation, e.g. `cell = row.cells[0]`.
+ """
+ return _CellCollection(self._tr, self)
+
+ @property
+ def height(self) -> Length:
+ """Height of row in EMU."""
+ return self._tr.h
+
+ @height.setter
+ def height(self, height: Length):
+ self._tr.h = height
+ self._parent.notify_height_changed()
+
+
+class _CellCollection(Subshape):
+ """Horizontal sequence of row cells"""
+
+ def __init__(self, tr: CT_TableRow, parent: _Row):
+ super(_CellCollection, self).__init__(parent)
+ self._parent = parent
+ self._tr = tr
+
+ def __getitem__(self, idx: int) -> _Cell:
+ """Provides indexed access, (e.g. 'cells[0]')."""
+ if idx < 0 or idx >= len(self._tr.tc_lst):
+ msg = "cell index [%d] out of range" % idx
+ raise IndexError(msg)
+ return _Cell(self._tr.tc_lst[idx], self)
+
+ def __iter__(self) -> Iterator[_Cell]:
+ """Provides iterability."""
+ return (_Cell(tc, self) for tc in self._tr.tc_lst)
+
+ def __len__(self) -> int:
+ """Supports len() function (e.g. 'len(cells) == 1')."""
+ return len(self._tr.tc_lst)
+
+
+class _ColumnCollection(Subshape):
+ """Sequence of table columns."""
+
+ def __init__(self, tbl: CT_Table, parent: Table):
+ super(_ColumnCollection, self).__init__(parent)
+ self._parent = parent
+ self._tbl = tbl
+
+ def __getitem__(self, idx: int):
+ """Provides indexed access, (e.g. 'columns[0]')."""
+ if idx < 0 or idx >= len(self._tbl.tblGrid.gridCol_lst):
+ msg = "column index [%d] out of range" % idx
+ raise IndexError(msg)
+ return _Column(self._tbl.tblGrid.gridCol_lst[idx], self)
+
+ def __len__(self):
+ """Supports len() function (e.g. 'len(columns) == 1')."""
+ return len(self._tbl.tblGrid.gridCol_lst)
+
+ def notify_width_changed(self):
+ """Called by a column when its width changes. Pass along to parent."""
+ self._parent.notify_width_changed()
+
+
+class _RowCollection(Subshape):
+ """Sequence of table rows"""
+
+ def __init__(self, tbl: CT_Table, parent: Table):
+ super(_RowCollection, self).__init__(parent)
+ self._parent = parent
+ self._tbl = tbl
+
+ def __getitem__(self, idx: int) -> _Row:
+ """Provides indexed access, (e.g. 'rows[0]')."""
+ if idx < 0 or idx >= len(self):
+ msg = "row index [%d] out of range" % idx
+ raise IndexError(msg)
+ return _Row(self._tbl.tr_lst[idx], self)
+
+ def __len__(self):
+ """Supports len() function (e.g. 'len(rows) == 1')."""
+ return len(self._tbl.tr_lst)
+
+ def notify_height_changed(self):
+ """Called by a row when its height changes. Pass along to parent."""
+ self._parent.notify_height_changed()
diff --git a/.venv/lib/python3.12/site-packages/pptx/templates/default.pptx b/.venv/lib/python3.12/site-packages/pptx/templates/default.pptx
new file mode 100644
index 00000000..e7fd6565
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/templates/default.pptx
Binary files differ
diff --git a/.venv/lib/python3.12/site-packages/pptx/templates/docx-icon.emf b/.venv/lib/python3.12/site-packages/pptx/templates/docx-icon.emf
new file mode 100644
index 00000000..b8660118
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/templates/docx-icon.emf
Binary files differ
diff --git a/.venv/lib/python3.12/site-packages/pptx/templates/generic-icon.emf b/.venv/lib/python3.12/site-packages/pptx/templates/generic-icon.emf
new file mode 100644
index 00000000..d0914e00
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/templates/generic-icon.emf
Binary files differ
diff --git a/.venv/lib/python3.12/site-packages/pptx/templates/notes.xml b/.venv/lib/python3.12/site-packages/pptx/templates/notes.xml
new file mode 100644
index 00000000..654effbb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/templates/notes.xml
@@ -0,0 +1,23 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
+<p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
+ <p:cSld>
+ <p:spTree>
+ <p:nvGrpSpPr>
+ <p:cNvPr id="1" name=""/>
+ <p:cNvGrpSpPr/>
+ <p:nvPr/>
+ </p:nvGrpSpPr>
+ <p:grpSpPr>
+ <a:xfrm>
+ <a:off x="0" y="0"/>
+ <a:ext cx="0" cy="0"/>
+ <a:chOff x="0" y="0"/>
+ <a:chExt cx="0" cy="0"/>
+ </a:xfrm>
+ </p:grpSpPr>
+ </p:spTree>
+ </p:cSld>
+ <p:clrMapOvr>
+ <a:masterClrMapping/>
+ </p:clrMapOvr>
+</p:notes>
diff --git a/.venv/lib/python3.12/site-packages/pptx/templates/notesMaster.xml b/.venv/lib/python3.12/site-packages/pptx/templates/notesMaster.xml
new file mode 100644
index 00000000..80008e07
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/templates/notesMaster.xml
@@ -0,0 +1,352 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
+<p:notesMaster
+ xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
+ xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
+ xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
+ >
+ <p:cSld>
+ <p:bg>
+ <p:bgRef idx="1001">
+ <a:schemeClr val="bg1"/>
+ </p:bgRef>
+ </p:bg>
+ <p:spTree>
+ <p:nvGrpSpPr>
+ <p:cNvPr id="1" name=""/>
+ <p:cNvGrpSpPr/>
+ <p:nvPr/>
+ </p:nvGrpSpPr>
+ <p:grpSpPr>
+ <a:xfrm>
+ <a:off x="0" y="0"/>
+ <a:ext cx="0" cy="0"/>
+ <a:chOff x="0" y="0"/>
+ <a:chExt cx="0" cy="0"/>
+ </a:xfrm>
+ </p:grpSpPr>
+ <p:sp>
+ <p:nvSpPr>
+ <p:cNvPr id="2" name="Header Placeholder 1"/>
+ <p:cNvSpPr>
+ <a:spLocks noGrp="1"/>
+ </p:cNvSpPr>
+ <p:nvPr>
+ <p:ph type="hdr" sz="quarter"/>
+ </p:nvPr>
+ </p:nvSpPr>
+ <p:spPr>
+ <a:xfrm>
+ <a:off x="0" y="0"/>
+ <a:ext cx="2971800" cy="457200"/>
+ </a:xfrm>
+ <a:prstGeom prst="rect">
+ <a:avLst/>
+ </a:prstGeom>
+ </p:spPr>
+ <p:txBody>
+ <a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0"/>
+ <a:lstStyle>
+ <a:lvl1pPr algn="l">
+ <a:defRPr sz="1200"/>
+ </a:lvl1pPr>
+ </a:lstStyle>
+ <a:p>
+ <a:endParaRPr lang="en-US"/>
+ </a:p>
+ </p:txBody>
+ </p:sp>
+ <p:sp>
+ <p:nvSpPr>
+ <p:cNvPr id="3" name="Date Placeholder 2"/>
+ <p:cNvSpPr>
+ <a:spLocks noGrp="1"/>
+ </p:cNvSpPr>
+ <p:nvPr>
+ <p:ph type="dt" idx="1"/>
+ </p:nvPr>
+ </p:nvSpPr>
+ <p:spPr>
+ <a:xfrm>
+ <a:off x="3884613" y="0"/>
+ <a:ext cx="2971800" cy="457200"/>
+ </a:xfrm>
+ <a:prstGeom prst="rect">
+ <a:avLst/>
+ </a:prstGeom>
+ </p:spPr>
+ <p:txBody>
+ <a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0"/>
+ <a:lstStyle>
+ <a:lvl1pPr algn="r">
+ <a:defRPr sz="1200"/>
+ </a:lvl1pPr>
+ </a:lstStyle>
+ <a:p>
+ <a:fld id="{0F89C1C7-3DCD-1040-A9CF-14679D8B5DDD}" type="datetimeFigureOut">
+ <a:rPr lang="en-US" smtClean="0"/>
+ <a:t>10/17/16</a:t>
+ </a:fld>
+ <a:endParaRPr lang="en-US"/>
+ </a:p>
+ </p:txBody>
+ </p:sp>
+ <p:sp>
+ <p:nvSpPr>
+ <p:cNvPr id="4" name="Slide Image Placeholder 3"/>
+ <p:cNvSpPr>
+ <a:spLocks noGrp="1" noRot="1" noChangeAspect="1"/>
+ </p:cNvSpPr>
+ <p:nvPr>
+ <p:ph type="sldImg" idx="2"/>
+ </p:nvPr>
+ </p:nvSpPr>
+ <p:spPr>
+ <a:xfrm>
+ <a:off x="1143000" y="685800"/>
+ <a:ext cx="4572000" cy="3429000"/>
+ </a:xfrm>
+ <a:prstGeom prst="rect">
+ <a:avLst/>
+ </a:prstGeom>
+ <a:noFill/>
+ <a:ln w="12700">
+ <a:solidFill>
+ <a:prstClr val="black"/>
+ </a:solidFill>
+ </a:ln>
+ </p:spPr>
+ <p:txBody>
+ <a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0" anchor="ctr"/>
+ <a:lstStyle/>
+ <a:p>
+ <a:endParaRPr lang="en-US"/>
+ </a:p>
+ </p:txBody>
+ </p:sp>
+ <p:sp>
+ <p:nvSpPr>
+ <p:cNvPr id="5" name="Notes Placeholder 4"/>
+ <p:cNvSpPr>
+ <a:spLocks noGrp="1"/>
+ </p:cNvSpPr>
+ <p:nvPr>
+ <p:ph type="body" sz="quarter" idx="3"/>
+ </p:nvPr>
+ </p:nvSpPr>
+ <p:spPr>
+ <a:xfrm>
+ <a:off x="685800" y="4343400"/>
+ <a:ext cx="5486400" cy="4114800"/>
+ </a:xfrm>
+ <a:prstGeom prst="rect">
+ <a:avLst/>
+ </a:prstGeom>
+ </p:spPr>
+ <p:txBody>
+ <a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0"/>
+ <a:lstStyle/>
+ <a:p>
+ <a:pPr lvl="0"/>
+ <a:r>
+ <a:rPr lang="en-US" smtClean="0"/>
+ <a:t>Click to edit Master text styles</a:t>
+ </a:r>
+ </a:p>
+ <a:p>
+ <a:pPr lvl="1"/>
+ <a:r>
+ <a:rPr lang="en-US" smtClean="0"/>
+ <a:t>Second level</a:t>
+ </a:r>
+ </a:p>
+ <a:p>
+ <a:pPr lvl="2"/>
+ <a:r>
+ <a:rPr lang="en-US" smtClean="0"/>
+ <a:t>Third level</a:t>
+ </a:r>
+ </a:p>
+ <a:p>
+ <a:pPr lvl="3"/>
+ <a:r>
+ <a:rPr lang="en-US" smtClean="0"/>
+ <a:t>Fourth level</a:t>
+ </a:r>
+ </a:p>
+ <a:p>
+ <a:pPr lvl="4"/>
+ <a:r>
+ <a:rPr lang="en-US" smtClean="0"/>
+ <a:t>Fifth level</a:t>
+ </a:r>
+ <a:endParaRPr lang="en-US"/>
+ </a:p>
+ </p:txBody>
+ </p:sp>
+ <p:sp>
+ <p:nvSpPr>
+ <p:cNvPr id="6" name="Footer Placeholder 5"/>
+ <p:cNvSpPr>
+ <a:spLocks noGrp="1"/>
+ </p:cNvSpPr>
+ <p:nvPr>
+ <p:ph type="ftr" sz="quarter" idx="4"/>
+ </p:nvPr>
+ </p:nvSpPr>
+ <p:spPr>
+ <a:xfrm>
+ <a:off x="0" y="8685213"/>
+ <a:ext cx="2971800" cy="457200"/>
+ </a:xfrm>
+ <a:prstGeom prst="rect">
+ <a:avLst/>
+ </a:prstGeom>
+ </p:spPr>
+ <p:txBody>
+ <a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0" anchor="b"/>
+ <a:lstStyle>
+ <a:lvl1pPr algn="l">
+ <a:defRPr sz="1200"/>
+ </a:lvl1pPr>
+ </a:lstStyle>
+ <a:p>
+ <a:endParaRPr lang="en-US"/>
+ </a:p>
+ </p:txBody>
+ </p:sp>
+ <p:sp>
+ <p:nvSpPr>
+ <p:cNvPr id="7" name="Slide Number Placeholder 6"/>
+ <p:cNvSpPr>
+ <a:spLocks noGrp="1"/>
+ </p:cNvSpPr>
+ <p:nvPr>
+ <p:ph type="sldNum" sz="quarter" idx="5"/>
+ </p:nvPr>
+ </p:nvSpPr>
+ <p:spPr>
+ <a:xfrm>
+ <a:off x="3884613" y="8685213"/>
+ <a:ext cx="2971800" cy="457200"/>
+ </a:xfrm>
+ <a:prstGeom prst="rect">
+ <a:avLst/>
+ </a:prstGeom>
+ </p:spPr>
+ <p:txBody>
+ <a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0" anchor="b"/>
+ <a:lstStyle>
+ <a:lvl1pPr algn="r">
+ <a:defRPr sz="1200"/>
+ </a:lvl1pPr>
+ </a:lstStyle>
+ <a:p>
+ <a:fld id="{BB5E49A5-4136-284D-997B-48E1D791AD67}" type="slidenum">
+ <a:rPr lang="en-US" smtClean="0"/>
+ <a:t>‹#›</a:t>
+ </a:fld>
+ <a:endParaRPr lang="en-US"/>
+ </a:p>
+ </p:txBody>
+ </p:sp>
+ </p:spTree>
+ <p:extLst>
+ <p:ext uri="{BB962C8B-B14F-4D97-AF65-F5344CB8AC3E}">
+ <p14:creationId xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" val="2623252185"/>
+ </p:ext>
+ </p:extLst>
+ </p:cSld>
+ <p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>
+ <p:notesStyle>
+ <a:lvl1pPr marL="0" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+ <a:defRPr sz="1200" kern="1200">
+ <a:solidFill>
+ <a:schemeClr val="tx1"/>
+ </a:solidFill>
+ <a:latin typeface="+mn-lt"/>
+ <a:ea typeface="+mn-ea"/>
+ <a:cs typeface="+mn-cs"/>
+ </a:defRPr>
+ </a:lvl1pPr>
+ <a:lvl2pPr marL="457200" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+ <a:defRPr sz="1200" kern="1200">
+ <a:solidFill>
+ <a:schemeClr val="tx1"/>
+ </a:solidFill>
+ <a:latin typeface="+mn-lt"/>
+ <a:ea typeface="+mn-ea"/>
+ <a:cs typeface="+mn-cs"/>
+ </a:defRPr>
+ </a:lvl2pPr>
+ <a:lvl3pPr marL="914400" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+ <a:defRPr sz="1200" kern="1200">
+ <a:solidFill>
+ <a:schemeClr val="tx1"/>
+ </a:solidFill>
+ <a:latin typeface="+mn-lt"/>
+ <a:ea typeface="+mn-ea"/>
+ <a:cs typeface="+mn-cs"/>
+ </a:defRPr>
+ </a:lvl3pPr>
+ <a:lvl4pPr marL="1371600" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+ <a:defRPr sz="1200" kern="1200">
+ <a:solidFill>
+ <a:schemeClr val="tx1"/>
+ </a:solidFill>
+ <a:latin typeface="+mn-lt"/>
+ <a:ea typeface="+mn-ea"/>
+ <a:cs typeface="+mn-cs"/>
+ </a:defRPr>
+ </a:lvl4pPr>
+ <a:lvl5pPr marL="1828800" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+ <a:defRPr sz="1200" kern="1200">
+ <a:solidFill>
+ <a:schemeClr val="tx1"/>
+ </a:solidFill>
+ <a:latin typeface="+mn-lt"/>
+ <a:ea typeface="+mn-ea"/>
+ <a:cs typeface="+mn-cs"/>
+ </a:defRPr>
+ </a:lvl5pPr>
+ <a:lvl6pPr marL="2286000" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+ <a:defRPr sz="1200" kern="1200">
+ <a:solidFill>
+ <a:schemeClr val="tx1"/>
+ </a:solidFill>
+ <a:latin typeface="+mn-lt"/>
+ <a:ea typeface="+mn-ea"/>
+ <a:cs typeface="+mn-cs"/>
+ </a:defRPr>
+ </a:lvl6pPr>
+ <a:lvl7pPr marL="2743200" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+ <a:defRPr sz="1200" kern="1200">
+ <a:solidFill>
+ <a:schemeClr val="tx1"/>
+ </a:solidFill>
+ <a:latin typeface="+mn-lt"/>
+ <a:ea typeface="+mn-ea"/>
+ <a:cs typeface="+mn-cs"/>
+ </a:defRPr>
+ </a:lvl7pPr>
+ <a:lvl8pPr marL="3200400" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+ <a:defRPr sz="1200" kern="1200">
+ <a:solidFill>
+ <a:schemeClr val="tx1"/>
+ </a:solidFill>
+ <a:latin typeface="+mn-lt"/>
+ <a:ea typeface="+mn-ea"/>
+ <a:cs typeface="+mn-cs"/>
+ </a:defRPr>
+ </a:lvl8pPr>
+ <a:lvl9pPr marL="3657600" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
+ <a:defRPr sz="1200" kern="1200">
+ <a:solidFill>
+ <a:schemeClr val="tx1"/>
+ </a:solidFill>
+ <a:latin typeface="+mn-lt"/>
+ <a:ea typeface="+mn-ea"/>
+ <a:cs typeface="+mn-cs"/>
+ </a:defRPr>
+ </a:lvl9pPr>
+ </p:notesStyle>
+</p:notesMaster>
diff --git a/.venv/lib/python3.12/site-packages/pptx/templates/pptx-icon.emf b/.venv/lib/python3.12/site-packages/pptx/templates/pptx-icon.emf
new file mode 100644
index 00000000..e9b1ce88
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/templates/pptx-icon.emf
Binary files differ
diff --git a/.venv/lib/python3.12/site-packages/pptx/templates/theme.xml b/.venv/lib/python3.12/site-packages/pptx/templates/theme.xml
new file mode 100644
index 00000000..bf57418d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/templates/theme.xml
@@ -0,0 +1,321 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
+<a:theme
+ xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
+ name="Office Theme"
+ >
+ <a:themeElements>
+ <a:clrScheme name="Office">
+ <a:dk1>
+ <a:sysClr val="windowText" lastClr="000000"/>
+ </a:dk1>
+ <a:lt1>
+ <a:sysClr val="window" lastClr="FFFFFF"/>
+ </a:lt1>
+ <a:dk2>
+ <a:srgbClr val="1F497D"/>
+ </a:dk2>
+ <a:lt2>
+ <a:srgbClr val="EEECE1"/>
+ </a:lt2>
+ <a:accent1>
+ <a:srgbClr val="4F81BD"/>
+ </a:accent1>
+ <a:accent2>
+ <a:srgbClr val="C0504D"/>
+ </a:accent2>
+ <a:accent3>
+ <a:srgbClr val="9BBB59"/>
+ </a:accent3>
+ <a:accent4>
+ <a:srgbClr val="8064A2"/>
+ </a:accent4>
+ <a:accent5>
+ <a:srgbClr val="4BACC6"/>
+ </a:accent5>
+ <a:accent6>
+ <a:srgbClr val="F79646"/>
+ </a:accent6>
+ <a:hlink>
+ <a:srgbClr val="0000FF"/>
+ </a:hlink>
+ <a:folHlink>
+ <a:srgbClr val="800080"/>
+ </a:folHlink>
+ </a:clrScheme>
+ <a:fontScheme name="Office">
+ <a:majorFont>
+ <a:latin typeface="Calibri"/>
+ <a:ea typeface=""/>
+ <a:cs typeface=""/>
+ <a:font script="Jpan" typeface="MS Pゴシック"/>
+ <a:font script="Hang" typeface="맑은 고딕"/>
+ <a:font script="Hans" typeface="宋体"/>
+ <a:font script="Hant" typeface="新細明體"/>
+ <a:font script="Arab" typeface="Times New Roman"/>
+ <a:font script="Hebr" typeface="Times New Roman"/>
+ <a:font script="Thai" typeface="Angsana New"/>
+ <a:font script="Ethi" typeface="Nyala"/>
+ <a:font script="Beng" typeface="Vrinda"/>
+ <a:font script="Gujr" typeface="Shruti"/>
+ <a:font script="Khmr" typeface="MoolBoran"/>
+ <a:font script="Knda" typeface="Tunga"/>
+ <a:font script="Guru" typeface="Raavi"/>
+ <a:font script="Cans" typeface="Euphemia"/>
+ <a:font script="Cher" typeface="Plantagenet Cherokee"/>
+ <a:font script="Yiii" typeface="Microsoft Yi Baiti"/>
+ <a:font script="Tibt" typeface="Microsoft Himalaya"/>
+ <a:font script="Thaa" typeface="MV Boli"/>
+ <a:font script="Deva" typeface="Mangal"/>
+ <a:font script="Telu" typeface="Gautami"/>
+ <a:font script="Taml" typeface="Latha"/>
+ <a:font script="Syrc" typeface="Estrangelo Edessa"/>
+ <a:font script="Orya" typeface="Kalinga"/>
+ <a:font script="Mlym" typeface="Kartika"/>
+ <a:font script="Laoo" typeface="DokChampa"/>
+ <a:font script="Sinh" typeface="Iskoola Pota"/>
+ <a:font script="Mong" typeface="Mongolian Baiti"/>
+ <a:font script="Viet" typeface="Times New Roman"/>
+ <a:font script="Uigh" typeface="Microsoft Uighur"/>
+ <a:font script="Geor" typeface="Sylfaen"/>
+ </a:majorFont>
+ <a:minorFont>
+ <a:latin typeface="Calibri"/>
+ <a:ea typeface=""/>
+ <a:cs typeface=""/>
+ <a:font script="Jpan" typeface="MS Pゴシック"/>
+ <a:font script="Hang" typeface="맑은 고딕"/>
+ <a:font script="Hans" typeface="宋体"/>
+ <a:font script="Hant" typeface="新細明體"/>
+ <a:font script="Arab" typeface="Arial"/>
+ <a:font script="Hebr" typeface="Arial"/>
+ <a:font script="Thai" typeface="Cordia New"/>
+ <a:font script="Ethi" typeface="Nyala"/>
+ <a:font script="Beng" typeface="Vrinda"/>
+ <a:font script="Gujr" typeface="Shruti"/>
+ <a:font script="Khmr" typeface="DaunPenh"/>
+ <a:font script="Knda" typeface="Tunga"/>
+ <a:font script="Guru" typeface="Raavi"/>
+ <a:font script="Cans" typeface="Euphemia"/>
+ <a:font script="Cher" typeface="Plantagenet Cherokee"/>
+ <a:font script="Yiii" typeface="Microsoft Yi Baiti"/>
+ <a:font script="Tibt" typeface="Microsoft Himalaya"/>
+ <a:font script="Thaa" typeface="MV Boli"/>
+ <a:font script="Deva" typeface="Mangal"/>
+ <a:font script="Telu" typeface="Gautami"/>
+ <a:font script="Taml" typeface="Latha"/>
+ <a:font script="Syrc" typeface="Estrangelo Edessa"/>
+ <a:font script="Orya" typeface="Kalinga"/>
+ <a:font script="Mlym" typeface="Kartika"/>
+ <a:font script="Laoo" typeface="DokChampa"/>
+ <a:font script="Sinh" typeface="Iskoola Pota"/>
+ <a:font script="Mong" typeface="Mongolian Baiti"/>
+ <a:font script="Viet" typeface="Arial"/>
+ <a:font script="Uigh" typeface="Microsoft Uighur"/>
+ <a:font script="Geor" typeface="Sylfaen"/>
+ </a:minorFont>
+ </a:fontScheme>
+ <a:fmtScheme name="Office">
+ <a:fillStyleLst>
+ <a:solidFill>
+ <a:schemeClr val="phClr"/>
+ </a:solidFill>
+ <a:gradFill rotWithShape="1">
+ <a:gsLst>
+ <a:gs pos="0">
+ <a:schemeClr val="phClr">
+ <a:tint val="50000"/>
+ <a:satMod val="300000"/>
+ </a:schemeClr>
+ </a:gs>
+ <a:gs pos="35000">
+ <a:schemeClr val="phClr">
+ <a:tint val="37000"/>
+ <a:satMod val="300000"/>
+ </a:schemeClr>
+ </a:gs>
+ <a:gs pos="100000">
+ <a:schemeClr val="phClr">
+ <a:tint val="15000"/>
+ <a:satMod val="350000"/>
+ </a:schemeClr>
+ </a:gs>
+ </a:gsLst>
+ <a:lin ang="16200000" scaled="1"/>
+ </a:gradFill>
+ <a:gradFill rotWithShape="1">
+ <a:gsLst>
+ <a:gs pos="0">
+ <a:schemeClr val="phClr">
+ <a:tint val="100000"/>
+ <a:shade val="100000"/>
+ <a:satMod val="130000"/>
+ </a:schemeClr>
+ </a:gs>
+ <a:gs pos="100000">
+ <a:schemeClr val="phClr">
+ <a:tint val="50000"/>
+ <a:shade val="100000"/>
+ <a:satMod val="350000"/>
+ </a:schemeClr>
+ </a:gs>
+ </a:gsLst>
+ <a:lin ang="16200000" scaled="0"/>
+ </a:gradFill>
+ </a:fillStyleLst>
+ <a:lnStyleLst>
+ <a:ln w="9525" cap="flat" cmpd="sng" algn="ctr">
+ <a:solidFill>
+ <a:schemeClr val="phClr">
+ <a:shade val="95000"/>
+ <a:satMod val="105000"/>
+ </a:schemeClr>
+ </a:solidFill>
+ <a:prstDash val="solid"/>
+ </a:ln>
+ <a:ln w="25400" cap="flat" cmpd="sng" algn="ctr">
+ <a:solidFill>
+ <a:schemeClr val="phClr"/>
+ </a:solidFill>
+ <a:prstDash val="solid"/>
+ </a:ln>
+ <a:ln w="38100" cap="flat" cmpd="sng" algn="ctr">
+ <a:solidFill>
+ <a:schemeClr val="phClr"/>
+ </a:solidFill>
+ <a:prstDash val="solid"/>
+ </a:ln>
+ </a:lnStyleLst>
+ <a:effectStyleLst>
+ <a:effectStyle>
+ <a:effectLst>
+ <a:outerShdw blurRad="40000" dist="20000" dir="5400000" rotWithShape="0">
+ <a:srgbClr val="000000">
+ <a:alpha val="38000"/>
+ </a:srgbClr>
+ </a:outerShdw>
+ </a:effectLst>
+ </a:effectStyle>
+ <a:effectStyle>
+ <a:effectLst>
+ <a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0">
+ <a:srgbClr val="000000">
+ <a:alpha val="35000"/>
+ </a:srgbClr>
+ </a:outerShdw>
+ </a:effectLst>
+ </a:effectStyle>
+ <a:effectStyle>
+ <a:effectLst>
+ <a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0">
+ <a:srgbClr val="000000">
+ <a:alpha val="35000"/>
+ </a:srgbClr>
+ </a:outerShdw>
+ </a:effectLst>
+ <a:scene3d>
+ <a:camera prst="orthographicFront">
+ <a:rot lat="0" lon="0" rev="0"/>
+ </a:camera>
+ <a:lightRig rig="threePt" dir="t">
+ <a:rot lat="0" lon="0" rev="1200000"/>
+ </a:lightRig>
+ </a:scene3d>
+ <a:sp3d>
+ <a:bevelT w="63500" h="25400"/>
+ </a:sp3d>
+ </a:effectStyle>
+ </a:effectStyleLst>
+ <a:bgFillStyleLst>
+ <a:solidFill>
+ <a:schemeClr val="phClr"/>
+ </a:solidFill>
+ <a:gradFill rotWithShape="1">
+ <a:gsLst>
+ <a:gs pos="0">
+ <a:schemeClr val="phClr">
+ <a:tint val="40000"/>
+ <a:satMod val="350000"/>
+ </a:schemeClr>
+ </a:gs>
+ <a:gs pos="40000">
+ <a:schemeClr val="phClr">
+ <a:tint val="45000"/>
+ <a:shade val="99000"/>
+ <a:satMod val="350000"/>
+ </a:schemeClr>
+ </a:gs>
+ <a:gs pos="100000">
+ <a:schemeClr val="phClr">
+ <a:shade val="20000"/>
+ <a:satMod val="255000"/>
+ </a:schemeClr>
+ </a:gs>
+ </a:gsLst>
+ <a:path path="circle">
+ <a:fillToRect l="50000" t="-80000" r="50000" b="180000"/>
+ </a:path>
+ </a:gradFill>
+ <a:gradFill rotWithShape="1">
+ <a:gsLst>
+ <a:gs pos="0">
+ <a:schemeClr val="phClr">
+ <a:tint val="80000"/>
+ <a:satMod val="300000"/>
+ </a:schemeClr>
+ </a:gs>
+ <a:gs pos="100000">
+ <a:schemeClr val="phClr">
+ <a:shade val="30000"/>
+ <a:satMod val="200000"/>
+ </a:schemeClr>
+ </a:gs>
+ </a:gsLst>
+ <a:path path="circle">
+ <a:fillToRect l="50000" t="50000" r="50000" b="50000"/>
+ </a:path>
+ </a:gradFill>
+ </a:bgFillStyleLst>
+ </a:fmtScheme>
+ </a:themeElements>
+ <a:objectDefaults>
+ <a:spDef>
+ <a:spPr/>
+ <a:bodyPr/>
+ <a:lstStyle/>
+ <a:style>
+ <a:lnRef idx="1">
+ <a:schemeClr val="accent1"/>
+ </a:lnRef>
+ <a:fillRef idx="3">
+ <a:schemeClr val="accent1"/>
+ </a:fillRef>
+ <a:effectRef idx="2">
+ <a:schemeClr val="accent1"/>
+ </a:effectRef>
+ <a:fontRef idx="minor">
+ <a:schemeClr val="lt1"/>
+ </a:fontRef>
+ </a:style>
+ </a:spDef>
+ <a:lnDef>
+ <a:spPr/>
+ <a:bodyPr/>
+ <a:lstStyle/>
+ <a:style>
+ <a:lnRef idx="2">
+ <a:schemeClr val="accent1"/>
+ </a:lnRef>
+ <a:fillRef idx="0">
+ <a:schemeClr val="accent1"/>
+ </a:fillRef>
+ <a:effectRef idx="1">
+ <a:schemeClr val="accent1"/>
+ </a:effectRef>
+ <a:fontRef idx="minor">
+ <a:schemeClr val="tx1"/>
+ </a:fontRef>
+ </a:style>
+ </a:lnDef>
+ </a:objectDefaults>
+ <a:extraClrSchemeLst/>
+</a:theme>
diff --git a/.venv/lib/python3.12/site-packages/pptx/templates/xlsx-icon.emf b/.venv/lib/python3.12/site-packages/pptx/templates/xlsx-icon.emf
new file mode 100644
index 00000000..658eac20
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/templates/xlsx-icon.emf
Binary files differ
diff --git a/.venv/lib/python3.12/site-packages/pptx/text/__init__.py b/.venv/lib/python3.12/site-packages/pptx/text/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/text/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/pptx/text/fonts.py b/.venv/lib/python3.12/site-packages/pptx/text/fonts.py
new file mode 100644
index 00000000..5ae054a8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/text/fonts.py
@@ -0,0 +1,399 @@
+"""Objects related to system font file lookup."""
+
+from __future__ import annotations
+
+import os
+import sys
+from struct import calcsize, unpack_from
+
+from pptx.util import lazyproperty
+
+
+class FontFiles(object):
+ """A class-based singleton serving as a lazy cache for system font details."""
+
+ _font_files = None
+
+ @classmethod
+ def find(cls, family_name: str, is_bold: bool, is_italic: bool) -> str:
+ """Return the absolute path to an installed OpenType font.
+
+ File is matched by `family_name` and the styles `is_bold` and `is_italic`.
+ """
+ if cls._font_files is None:
+ cls._font_files = cls._installed_fonts()
+ return cls._font_files[(family_name, is_bold, is_italic)]
+
+ @classmethod
+ def _installed_fonts(cls):
+ """
+ Return a dict mapping a font descriptor to its font file path,
+ containing all the font files resident on the current machine. The
+ font descriptor is a (family_name, is_bold, is_italic) 3-tuple.
+ """
+ fonts = {}
+ for d in cls._font_directories():
+ for key, path in cls._iter_font_files_in(d):
+ fonts[key] = path
+ return fonts
+
+ @classmethod
+ def _font_directories(cls):
+ """
+ Return a sequence of directory paths likely to contain fonts on the
+ current platform.
+ """
+ if sys.platform.startswith("darwin"):
+ return cls._os_x_font_directories()
+ if sys.platform.startswith("win32"):
+ return cls._windows_font_directories()
+ raise OSError("unsupported operating system")
+
+ @classmethod
+ def _iter_font_files_in(cls, directory):
+ """
+ Generate the OpenType font files found in and under *directory*. Each
+ item is a key/value pair. The key is a (family_name, is_bold,
+ is_italic) 3-tuple, like ('Arial', True, False), and the value is the
+ absolute path to the font file.
+ """
+ for root, dirs, files in os.walk(directory):
+ for filename in files:
+ file_ext = os.path.splitext(filename)[1]
+ if file_ext.lower() not in (".otf", ".ttf"):
+ continue
+ path = os.path.abspath(os.path.join(root, filename))
+ with _Font.open(path) as f:
+ yield ((f.family_name, f.is_bold, f.is_italic), path)
+
+ @classmethod
+ def _os_x_font_directories(cls):
+ """
+ Return a sequence of directory paths on a Mac in which fonts are
+ likely to be located.
+ """
+ os_x_font_dirs = [
+ "/Library/Fonts",
+ "/Network/Library/Fonts",
+ "/System/Library/Fonts",
+ ]
+ home = os.environ.get("HOME")
+ if home is not None:
+ os_x_font_dirs.extend(
+ [os.path.join(home, "Library", "Fonts"), os.path.join(home, ".fonts")]
+ )
+ return os_x_font_dirs
+
+ @classmethod
+ def _windows_font_directories(cls):
+ """
+ Return a sequence of directory paths on Windows in which fonts are
+ likely to be located.
+ """
+ return [r"C:\Windows\Fonts"]
+
+
+class _Font(object):
+ """
+ A wrapper around an OTF/TTF font file stream that knows how to parse it
+ for its name and style characteristics, e.g. bold and italic.
+ """
+
+ def __init__(self, stream):
+ self._stream = stream
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exception_type, exception_value, exception_tb):
+ self._stream.close()
+
+ @property
+ def is_bold(self):
+ """
+ |True| if this font is marked as a bold style of its font family.
+ """
+ try:
+ return self._tables["head"].is_bold
+ except KeyError:
+ # some files don't have a head table
+ return False
+
+ @property
+ def is_italic(self):
+ """
+ |True| if this font is marked as an italic style of its font family.
+ """
+ try:
+ return self._tables["head"].is_italic
+ except KeyError:
+ # some files don't have a head table
+ return False
+
+ @classmethod
+ def open(cls, font_file_path):
+ """
+ Return a |_Font| instance loaded from *font_file_path*.
+ """
+ return cls(_Stream.open(font_file_path))
+
+ @property
+ def family_name(self):
+ """
+ The name of the typeface family for this font, e.g. 'Arial'. The full
+ typeface name includes optional style names, such as 'Regular' or
+ 'Bold Italic'. This attribute is only the common base name shared by
+ all fonts in the family.
+ """
+ return self._tables["name"].family_name
+
+ @lazyproperty
+ def _fields(self):
+ """5-tuple containing the fields read from the font file header.
+
+ Also known as the offset table.
+ """
+ # sfnt_version, tbl_count, search_range, entry_selector, range_shift
+ return self._stream.read_fields(">4sHHHH", 0)
+
+ def _iter_table_records(self):
+ """
+ Generate a (tag, offset, length) 3-tuple for each of the tables in
+ this font file.
+ """
+ count = self._table_count
+ bufr = self._stream.read(offset=12, length=count * 16)
+ tmpl = ">4sLLL"
+ for i in range(count):
+ offset = i * 16
+ tag, checksum, off, len_ = unpack_from(tmpl, bufr, offset)
+ yield tag.decode("utf-8"), off, len_
+
+ @lazyproperty
+ def _tables(self):
+ """
+ A mapping of OpenType table tag, e.g. 'name', to a table object
+ providing access to the contents of that table.
+ """
+ return dict(
+ (tag, _TableFactory(tag, self._stream, off, len_))
+ for tag, off, len_ in self._iter_table_records()
+ )
+
+ @property
+ def _table_count(self):
+ """
+ The number of tables in this OpenType font file.
+ """
+ return self._fields[1]
+
+
+class _Stream(object):
+ """A thin wrapper around a binary file that facilitates reading C-struct values."""
+
+ def __init__(self, file):
+ self._file = file
+
+ @classmethod
+ def open(cls, path):
+ """Return |_Stream| providing binary access to contents of file at `path`."""
+ return cls(open(path, "rb"))
+
+ def close(self):
+ """
+ Close the wrapped file. Using the stream after closing raises an
+ exception.
+ """
+ self._file.close()
+
+ def read(self, offset, length):
+ """
+ Return *length* bytes from this stream starting at *offset*.
+ """
+ self._file.seek(offset)
+ return self._file.read(length)
+
+ def read_fields(self, template, offset=0):
+ """
+ Return a tuple containing the C-struct fields in this stream
+ specified by *template* and starting at *offset*.
+ """
+ self._file.seek(offset)
+ bufr = self._file.read(calcsize(template))
+ return unpack_from(template, bufr)
+
+
+class _BaseTable(object):
+ """
+ Base class for OpenType font file table objects.
+ """
+
+ def __init__(self, tag, stream, offset, length):
+ self._tag = tag
+ self._stream = stream
+ self._offset = offset
+ self._length = length
+
+
+class _HeadTable(_BaseTable):
+ """
+ OpenType font table having the tag 'head' and containing certain header
+ information for the font, including its bold and/or italic style.
+ """
+
+ def __init__(self, tag, stream, offset, length):
+ super(_HeadTable, self).__init__(tag, stream, offset, length)
+
+ @property
+ def is_bold(self):
+ """
+ |True| if this font is marked as having emboldened characters.
+ """
+ return bool(self._macStyle & 1)
+
+ @property
+ def is_italic(self):
+ """
+ |True| if this font is marked as having italicized characters.
+ """
+ return bool(self._macStyle & 2)
+
+ @lazyproperty
+ def _fields(self):
+ """
+ A 17-tuple containing the fields in this table.
+ """
+ return self._stream.read_fields(">4s4sLLHHqqhhhhHHHHH", self._offset)
+
+ @property
+ def _macStyle(self):
+ """
+ The unsigned short value of the 'macStyle' field in this head table.
+ """
+ return self._fields[12]
+
+
+class _NameTable(_BaseTable):
+ """
+ An OpenType font table having the tag 'name' and containing the
+ name-related strings for the font.
+ """
+
+ def __init__(self, tag, stream, offset, length):
+ super(_NameTable, self).__init__(tag, stream, offset, length)
+
+ @property
+ def family_name(self):
+ """
+ The name of the typeface family for this font, e.g. 'Arial'.
+ """
+
+ def find_first(dict_, keys, default=None):
+ for key in keys:
+ value = dict_.get(key)
+ if value is not None:
+ return value
+ return default
+
+ # keys for Unicode, Mac, and Windows family name, respectively
+ return find_first(self._names, ((0, 1), (1, 1), (3, 1)))
+
+ @staticmethod
+ def _decode_name(raw_name, platform_id, encoding_id):
+ """
+ Return the unicode name decoded from *raw_name* using the encoding
+ implied by the combination of *platform_id* and *encoding_id*.
+ """
+ if platform_id == 1:
+ # reject non-Roman Mac font names
+ if encoding_id != 0:
+ return None
+ return raw_name.decode("mac-roman")
+ elif platform_id in (0, 3):
+ return raw_name.decode("utf-16-be")
+ else:
+ return None
+
+ def _iter_names(self):
+ """Generate a key/value pair for each name in this table.
+
+ The key is a (platform_id, name_id) 2-tuple and the value is the unicode text
+ corresponding to that key.
+ """
+ table_format, count, strings_offset = self._table_header
+ table_bytes = self._table_bytes
+
+ for idx in range(count):
+ platform_id, name_id, name = self._read_name(table_bytes, idx, strings_offset)
+ if name is None:
+ continue
+ yield ((platform_id, name_id), name)
+
+ @staticmethod
+ def _name_header(bufr, idx):
+ """
+ The (platform_id, encoding_id, language_id, name_id, length,
+ name_str_offset) 6-tuple encoded in each name record C-struct.
+ """
+ name_hdr_offset = 6 + idx * 12
+ return unpack_from(">HHHHHH", bufr, name_hdr_offset)
+
+ @staticmethod
+ def _raw_name_string(bufr, strings_offset, str_offset, length):
+ """
+ Return the *length* bytes comprising the encoded string in *bufr* at
+ *str_offset* in the strings area beginning at *strings_offset*.
+ """
+ offset = strings_offset + str_offset
+ tmpl = "%ds" % length
+ return unpack_from(tmpl, bufr, offset)[0]
+
+ def _read_name(self, bufr, idx, strings_offset):
+ """Return a (platform_id, name_id, name) 3-tuple for name at `idx` in `bufr`.
+
+ The triple looks like (0, 1, 'Arial'). `strings_offset` is the for the name at
+ `idx` position in `bufr`. `strings_offset` is the index into `bufr` where actual
+ name strings begin. The returned name is a unicode string.
+ """
+ platform_id, enc_id, lang_id, name_id, length, str_offset = self._name_header(bufr, idx)
+ name = self._read_name_text(bufr, platform_id, enc_id, strings_offset, str_offset, length)
+ return platform_id, name_id, name
+
+ def _read_name_text(
+ self, bufr, platform_id, encoding_id, strings_offset, name_str_offset, length
+ ):
+ """
+ Return the unicode name string at *name_str_offset* or |None| if
+ decoding its format is not supported.
+ """
+ raw_name = self._raw_name_string(bufr, strings_offset, name_str_offset, length)
+ return self._decode_name(raw_name, platform_id, encoding_id)
+
+ @lazyproperty
+ def _table_bytes(self):
+ """
+ The binary contents of this name table.
+ """
+ return self._stream.read(self._offset, self._length)
+
+ @property
+ def _table_header(self):
+ """
+ The (table_format, name_count, strings_offset) 3-tuple contained
+ in the header of this table.
+ """
+ return unpack_from(">HHH", self._table_bytes)
+
+ @lazyproperty
+ def _names(self):
+ """A mapping of (platform_id, name_id) keys to string names for this font."""
+ return dict(self._iter_names())
+
+
+def _TableFactory(tag, stream, offset, length):
+ """
+ Return an instance of |Table| appropriate to *tag*, loaded from
+ *font_file* with content of *length* starting at *offset*.
+ """
+ TableClass = {"head": _HeadTable, "name": _NameTable}.get(tag, _BaseTable)
+ return TableClass(tag, stream, offset, length)
diff --git a/.venv/lib/python3.12/site-packages/pptx/text/layout.py b/.venv/lib/python3.12/site-packages/pptx/text/layout.py
new file mode 100644
index 00000000..d2b43993
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/text/layout.py
@@ -0,0 +1,325 @@
+"""Objects related to layout of rendered text, such as TextFitter."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from PIL import ImageFont
+
+if TYPE_CHECKING:
+ from pptx.util import Length
+
+
+class TextFitter(tuple):
+ """Value object that knows how to fit text into given rectangular extents."""
+
+ def __new__(cls, line_source, extents, font_file):
+ width, height = extents
+ return tuple.__new__(cls, (line_source, width, height, font_file))
+
+ @classmethod
+ def best_fit_font_size(
+ cls, text: str, extents: tuple[Length, Length], max_size: int, font_file: str
+ ) -> int:
+ """Return whole-number best fit point size less than or equal to `max_size`.
+
+ The return value is the largest whole-number point size less than or equal to
+ `max_size` that allows `text` to fit completely within `extents` when rendered
+ using font defined in `font_file`.
+ """
+ line_source = _LineSource(text)
+ text_fitter = cls(line_source, extents, font_file)
+ return text_fitter._best_fit_font_size(max_size)
+
+ def _best_fit_font_size(self, max_size):
+ """
+ Return the largest whole-number point size less than or equal to
+ *max_size* that this fitter can fit.
+ """
+ predicate = self._fits_inside_predicate
+ sizes = _BinarySearchTree.from_ordered_sequence(range(1, int(max_size) + 1))
+ return sizes.find_max(predicate)
+
+ def _break_line(self, line_source, point_size):
+ """
+ Return a (line, remainder) pair where *line* is the longest line in
+ *line_source* that will fit in this fitter's width and *remainder* is
+ a |_LineSource| object containing the text following the break point.
+ """
+ lines = _BinarySearchTree.from_ordered_sequence(line_source)
+ predicate = self._fits_in_width_predicate(point_size)
+ return lines.find_max(predicate)
+
+ def _fits_in_width_predicate(self, point_size):
+ """
+ Return a function taking a text string value and returns |True| if
+ that text fits in this fitter when rendered at *point_size*. Used as
+ predicate for _break_line()
+ """
+
+ def predicate(line):
+ """
+ Return |True| if *line* fits in this fitter when rendered at
+ *point_size*.
+ """
+ cx = _rendered_size(line.text, point_size, self._font_file)[0]
+ return cx <= self._width
+
+ return predicate
+
+ @property
+ def _fits_inside_predicate(self):
+ """Return function taking an integer point size argument.
+
+ The function returns |True| if the text in this fitter can be wrapped to fit
+ entirely within its extents when rendered at that point size.
+ """
+
+ def predicate(point_size):
+ """Return |True| when text in `line_source` can be wrapped to fit.
+
+ Fit means text can be broken into lines that fit entirely within `extents`
+ when rendered at `point_size` using the font defined in `font_file`.
+ """
+ text_lines = self._wrap_lines(self._line_source, point_size)
+ cy = _rendered_size("Ty", point_size, self._font_file)[1]
+ return (cy * len(text_lines)) <= self._height
+
+ return predicate
+
+ @property
+ def _font_file(self):
+ return self[3]
+
+ @property
+ def _height(self):
+ return self[2]
+
+ @property
+ def _line_source(self):
+ return self[0]
+
+ @property
+ def _width(self):
+ return self[1]
+
+ def _wrap_lines(self, line_source, point_size):
+ """
+ Return a sequence of str values representing the text in
+ *line_source* wrapped within this fitter when rendered at
+ *point_size*.
+ """
+ text, remainder = self._break_line(line_source, point_size)
+ lines = [text]
+ if remainder:
+ lines.extend(self._wrap_lines(remainder, point_size))
+ return lines
+
+
+class _BinarySearchTree(object):
+ """
+ A node in a binary search tree. Uniform for root, subtree root, and leaf
+ nodes.
+ """
+
+ def __init__(self, value):
+ self._value = value
+ self._lesser = None
+ self._greater = None
+
+ def find_max(self, predicate, max_=None):
+ """
+ Return the largest item in or under this node that satisfies
+ *predicate*.
+ """
+ if predicate(self.value):
+ max_ = self.value
+ next_node = self._greater
+ else:
+ next_node = self._lesser
+ if next_node is None:
+ return max_
+ return next_node.find_max(predicate, max_)
+
+ @classmethod
+ def from_ordered_sequence(cls, iseq):
+ """
+ Return the root of a balanced binary search tree populated with the
+ values in iterable *iseq*.
+ """
+ seq = list(iseq)
+ # optimize for usually all fits by making longest first
+ bst = cls(seq.pop())
+ bst._insert_from_ordered_sequence(seq)
+ return bst
+
+ def insert(self, value):
+ """
+ Insert a new node containing *value* into this tree such that its
+ structure as a binary search tree is preserved.
+ """
+ side = "_lesser" if value < self.value else "_greater"
+ child = getattr(self, side)
+ if child is None:
+ setattr(self, side, _BinarySearchTree(value))
+ else:
+ child.insert(value)
+
+ def tree(self, level=0, prefix=""):
+ """
+ A string representation of the tree rooted in this node, useful for
+ debugging purposes.
+ """
+ text = "%s%s\n" % (prefix, self.value.text)
+ prefix = "%s└── " % (" " * level)
+ if self._lesser:
+ text += self._lesser.tree(level + 1, prefix)
+ if self._greater:
+ text += self._greater.tree(level + 1, prefix)
+ return text
+
+ @property
+ def value(self):
+ """
+ The value object contained in this node.
+ """
+ return self._value
+
+ @staticmethod
+ def _bisect(seq):
+ """
+ Return a (medial_value, greater_values, lesser_values) 3-tuple
+ obtained by bisecting sequence *seq*.
+ """
+ if len(seq) == 0:
+ return [], None, []
+ mid_idx = int(len(seq) / 2)
+ mid = seq[mid_idx]
+ greater = seq[mid_idx + 1 :]
+ lesser = seq[:mid_idx]
+ return mid, greater, lesser
+
+ def _insert_from_ordered_sequence(self, seq):
+ """
+ Insert the new values contained in *seq* into this tree such that
+ a balanced tree is produced.
+ """
+ if len(seq) == 0:
+ return
+ mid, greater, lesser = self._bisect(seq)
+ self.insert(mid)
+ self._insert_from_ordered_sequence(greater)
+ self._insert_from_ordered_sequence(lesser)
+
+
+class _LineSource(object):
+ """
+ Generates all the possible even-word line breaks in a string of text,
+ each in the form of a (line, remainder) 2-tuple where *line* contains the
+ text before the break and *remainder* the text after as a |_LineSource|
+ object. Its boolean value is |True| when it contains text, |False| when
+ its text is the empty string or whitespace only.
+ """
+
+ def __init__(self, text):
+ self._text = text
+
+ def __bool__(self):
+ """
+ Gives this object boolean behaviors (in Python 3). bool(line_source)
+ is False if it contains the empty string or whitespace only.
+ """
+ return self._text.strip() != ""
+
+ def __eq__(self, other):
+ return self._text == other._text
+
+ def __iter__(self):
+ """
+ Generate a (text, remainder) pair for each possible even-word line
+ break in this line source, where *text* is a str value and remainder
+ is a |_LineSource| value.
+ """
+ words = self._text.split()
+ for idx in range(1, len(words) + 1):
+ line_text = " ".join(words[:idx])
+ remainder_text = " ".join(words[idx:])
+ remainder = _LineSource(remainder_text)
+ yield _Line(line_text, remainder)
+
+ def __nonzero__(self):
+ """
+ Gives this object boolean behaviors (in Python 2). bool(line_source)
+ is False if it contains the empty string or whitespace only.
+ """
+ return self._text.strip() != ""
+
+ def __repr__(self):
+ return "<_LineSource('%s')>" % self._text
+
+
+class _Line(tuple):
+ """
+ A candidate line broken at an even word boundary from a string of text,
+ and a |_LineSource| value containing the text that remains after the line
+ is broken at this spot.
+ """
+
+ def __new__(cls, text, remainder):
+ return tuple.__new__(cls, (text, remainder))
+
+ def __gt__(self, other):
+ return len(self.text) > len(other.text)
+
+ def __lt__(self, other):
+ return not self.__gt__(other)
+
+ def __len__(self):
+ return len(self.text)
+
+ def __repr__(self):
+ return "'%s' => '%s'" % (self.text, self.remainder)
+
+ @property
+ def remainder(self):
+ return self[1]
+
+ @property
+ def text(self):
+ return self[0]
+
+
+class _Fonts(object):
+ """
+ A memoizing cache for ImageFont objects.
+ """
+
+ fonts = {}
+
+ @classmethod
+ def font(cls, font_path, point_size):
+ if (font_path, point_size) not in cls.fonts:
+ cls.fonts[(font_path, point_size)] = ImageFont.truetype(font_path, point_size)
+ return cls.fonts[(font_path, point_size)]
+
+
+def _rendered_size(text, point_size, font_file):
+ """
+ Return a (width, height) pair representing the size of *text* in English
+ Metric Units (EMU) when rendered at *point_size* in the font defined in
+ *font_file*.
+ """
+ emu_per_inch = 914400
+ px_per_inch = 72.0
+
+ font = _Fonts.font(font_file, point_size)
+ try:
+ px_width, px_height = font.getsize(text)
+ except AttributeError:
+ left, top, right, bottom = font.getbbox(text)
+ px_width, px_height = right - left, bottom - top
+
+ emu_width = int(px_width / px_per_inch * emu_per_inch)
+ emu_height = int(px_height / px_per_inch * emu_per_inch)
+
+ return emu_width, emu_height
diff --git a/.venv/lib/python3.12/site-packages/pptx/text/text.py b/.venv/lib/python3.12/site-packages/pptx/text/text.py
new file mode 100644
index 00000000..e139410c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/text/text.py
@@ -0,0 +1,681 @@
+"""Text-related objects such as TextFrame and Paragraph."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Iterator, cast
+
+from pptx.dml.fill import FillFormat
+from pptx.enum.dml import MSO_FILL
+from pptx.enum.lang import MSO_LANGUAGE_ID
+from pptx.enum.text import MSO_AUTO_SIZE, MSO_UNDERLINE, MSO_VERTICAL_ANCHOR
+from pptx.opc.constants import RELATIONSHIP_TYPE as RT
+from pptx.oxml.simpletypes import ST_TextWrappingType
+from pptx.shapes import Subshape
+from pptx.text.fonts import FontFiles
+from pptx.text.layout import TextFitter
+from pptx.util import Centipoints, Emu, Length, Pt, lazyproperty
+
+if TYPE_CHECKING:
+ from pptx.dml.color import ColorFormat
+ from pptx.enum.text import (
+ MSO_TEXT_UNDERLINE_TYPE,
+ MSO_VERTICAL_ANCHOR,
+ PP_PARAGRAPH_ALIGNMENT,
+ )
+ from pptx.oxml.action import CT_Hyperlink
+ from pptx.oxml.text import (
+ CT_RegularTextRun,
+ CT_TextBody,
+ CT_TextCharacterProperties,
+ CT_TextParagraph,
+ CT_TextParagraphProperties,
+ )
+ from pptx.types import ProvidesExtents, ProvidesPart
+
+
+class TextFrame(Subshape):
+ """The part of a shape that contains its text.
+
+ Not all shapes have a text frame. Corresponds to the `p:txBody` element that can
+ appear as a child element of `p:sp`. Not intended to be constructed directly.
+ """
+
+ def __init__(self, txBody: CT_TextBody, parent: ProvidesPart):
+ super(TextFrame, self).__init__(parent)
+ self._element = self._txBody = txBody
+ self._parent = parent
+
+ def add_paragraph(self):
+ """
+ Return new |_Paragraph| instance appended to the sequence of
+ paragraphs contained in this text frame.
+ """
+ p = self._txBody.add_p()
+ return _Paragraph(p, self)
+
+ @property
+ def auto_size(self) -> MSO_AUTO_SIZE | None:
+ """Resizing strategy used to fit text within this shape.
+
+ Determins the type of automatic resizing used to fit the text of this shape within its
+ bounding box when the text would otherwise extend beyond the shape boundaries. May be
+ |None|, `MSO_AUTO_SIZE.NONE`, `MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT`, or
+ `MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE`.
+ """
+ return self._bodyPr.autofit
+
+ @auto_size.setter
+ def auto_size(self, value: MSO_AUTO_SIZE | None):
+ self._bodyPr.autofit = value
+
+ def clear(self):
+ """Remove all paragraphs except one empty one."""
+ for p in self._txBody.p_lst[1:]:
+ self._txBody.remove(p)
+ p = self.paragraphs[0]
+ p.clear()
+
+ def fit_text(
+ self,
+ font_family: str = "Calibri",
+ max_size: int = 18,
+ bold: bool = False,
+ italic: bool = False,
+ font_file: str | None = None,
+ ):
+ """Fit text-frame text entirely within bounds of its shape.
+
+ Make the text in this text frame fit entirely within the bounds of its shape by setting
+ word wrap on and applying the "best-fit" font size to all the text it contains.
+
+ :attr:`TextFrame.auto_size` is set to :attr:`MSO_AUTO_SIZE.NONE`. The font size will not
+ be set larger than `max_size` points. If the path to a matching TrueType font is provided
+ as `font_file`, that font file will be used for the font metrics. If `font_file` is |None|,
+ best efforts are made to locate a font file with matchhing `font_family`, `bold`, and
+ `italic` installed on the current system (usually succeeds if the font is installed).
+ """
+ # ---no-op when empty as fit behavior not defined for that case---
+ if self.text == "":
+ return # pragma: no cover
+
+ font_size = self._best_fit_font_size(font_family, max_size, bold, italic, font_file)
+ self._apply_fit(font_family, font_size, bold, italic)
+
+ @property
+ def margin_bottom(self) -> Length:
+ """|Length| value representing the inset of text from the bottom text frame border.
+
+ :meth:`pptx.util.Inches` provides a convenient way of setting the value, e.g.
+ `text_frame.margin_bottom = Inches(0.05)`.
+ """
+ return self._bodyPr.bIns
+
+ @margin_bottom.setter
+ def margin_bottom(self, emu: Length):
+ self._bodyPr.bIns = emu
+
+ @property
+ def margin_left(self) -> Length:
+ """Inset of text from left text frame border as |Length| value."""
+ return self._bodyPr.lIns
+
+ @margin_left.setter
+ def margin_left(self, emu: Length):
+ self._bodyPr.lIns = emu
+
+ @property
+ def margin_right(self) -> Length:
+ """Inset of text from right text frame border as |Length| value."""
+ return self._bodyPr.rIns
+
+ @margin_right.setter
+ def margin_right(self, emu: Length):
+ self._bodyPr.rIns = emu
+
+ @property
+ def margin_top(self) -> Length:
+ """Inset of text from top text frame border as |Length| value."""
+ return self._bodyPr.tIns
+
+ @margin_top.setter
+ def margin_top(self, emu: Length):
+ self._bodyPr.tIns = emu
+
+ @property
+ def paragraphs(self) -> tuple[_Paragraph, ...]:
+ """Sequence of paragraphs in this text frame.
+
+ A text frame always contains at least one paragraph.
+ """
+ return tuple([_Paragraph(p, self) for p in self._txBody.p_lst])
+
+ @property
+ def text(self) -> str:
+ """All text in this text-frame as a single string.
+
+ Read/write. The return value contains all text in this text-frame. A line-feed character
+ (`"\\n"`) separates the text for each paragraph. A vertical-tab character (`"\\v"`) appears
+ for each line break (aka. soft carriage-return) encountered.
+
+ The vertical-tab character is how PowerPoint represents a soft carriage return in clipboard
+ text, which is why that encoding was chosen.
+
+ Assignment replaces all text in the text frame. A new paragraph is added for each line-feed
+ character (`"\\n"`) encountered. A line-break (soft carriage-return) is inserted for each
+ vertical-tab character (`"\\v"`) encountered.
+
+ Any control character other than newline, tab, or vertical-tab are escaped as plain-text
+ like "_x001B_" (for ESC (ASCII 32) in this example).
+ """
+ return "\n".join(paragraph.text for paragraph in self.paragraphs)
+
+ @text.setter
+ def text(self, text: str):
+ txBody = self._txBody
+ txBody.clear_content()
+ for p_text in text.split("\n"):
+ p = txBody.add_p()
+ p.append_text(p_text)
+
+ @property
+ def vertical_anchor(self) -> MSO_VERTICAL_ANCHOR | None:
+ """Represents the vertical alignment of text in this text frame.
+
+ |None| indicates the effective value should be inherited from this object's style hierarchy.
+ """
+ return self._txBody.bodyPr.anchor
+
+ @vertical_anchor.setter
+ def vertical_anchor(self, value: MSO_VERTICAL_ANCHOR | None):
+ bodyPr = self._txBody.bodyPr
+ bodyPr.anchor = value
+
+ @property
+ def word_wrap(self) -> bool | None:
+ """`True` when lines of text in this shape are wrapped to fit within the shape's width.
+
+ Read-write. Valid values are True, False, or None. True and False turn word wrap on and
+ off, respectively. Assigning None to word wrap causes any word wrap setting to be removed
+ from the text frame, causing it to inherit this setting from its style hierarchy.
+ """
+ return {
+ ST_TextWrappingType.SQUARE: True,
+ ST_TextWrappingType.NONE: False,
+ None: None,
+ }[self._txBody.bodyPr.wrap]
+
+ @word_wrap.setter
+ def word_wrap(self, value: bool | None):
+ if value not in (True, False, None):
+ raise ValueError( # pragma: no cover
+ "assigned value must be True, False, or None, got %s" % value
+ )
+ self._txBody.bodyPr.wrap = {
+ True: ST_TextWrappingType.SQUARE,
+ False: ST_TextWrappingType.NONE,
+ None: None,
+ }[value]
+
+ def _apply_fit(self, font_family: str, font_size: int, is_bold: bool, is_italic: bool):
+ """Arrange text in this text frame to fit inside its extents.
+
+ This is accomplished by setting auto size off, wrap on, and setting the font of
+ all its text to `font_family`, `font_size`, `is_bold`, and `is_italic`.
+ """
+ self.auto_size = MSO_AUTO_SIZE.NONE
+ self.word_wrap = True
+ self._set_font(font_family, font_size, is_bold, is_italic)
+
+ def _best_fit_font_size(
+ self, family: str, max_size: int, bold: bool, italic: bool, font_file: str | None
+ ) -> int:
+ """Return font-size in points that best fits text in this text-frame.
+
+ The best-fit font size is the largest integer point size not greater than `max_size` that
+ allows all the text in this text frame to fit inside its extents when rendered using the
+ font described by `family`, `bold`, and `italic`. If `font_file` is specified, it is used
+ to calculate the fit, whether or not it matches `family`, `bold`, and `italic`.
+ """
+ if font_file is None:
+ font_file = FontFiles.find(family, bold, italic)
+ return TextFitter.best_fit_font_size(self.text, self._extents, max_size, font_file)
+
+ @property
+ def _bodyPr(self):
+ return self._txBody.bodyPr
+
+ @property
+ def _extents(self) -> tuple[Length, Length]:
+ """(cx, cy) 2-tuple representing the effective rendering area of this text-frame.
+
+ Margins are taken into account.
+ """
+ parent = cast("ProvidesExtents", self._parent)
+ return (
+ Length(parent.width - self.margin_left - self.margin_right),
+ Length(parent.height - self.margin_top - self.margin_bottom),
+ )
+
+ def _set_font(self, family: str, size: int, bold: bool, italic: bool):
+ """Set the font properties of all the text in this text frame."""
+
+ def iter_rPrs(txBody: CT_TextBody) -> Iterator[CT_TextCharacterProperties]:
+ for p in txBody.p_lst:
+ for elm in p.content_children:
+ yield elm.get_or_add_rPr()
+ # generate a:endParaRPr for each <a:p> element
+ yield p.get_or_add_endParaRPr()
+
+ def set_rPr_font(
+ rPr: CT_TextCharacterProperties, name: str, size: int, bold: bool, italic: bool
+ ):
+ f = Font(rPr)
+ f.name, f.size, f.bold, f.italic = family, Pt(size), bold, italic
+
+ txBody = self._element
+ for rPr in iter_rPrs(txBody):
+ set_rPr_font(rPr, family, size, bold, italic)
+
+
+class Font(object):
+ """Character properties object, providing font size, font name, bold, italic, etc.
+
+ Corresponds to `a:rPr` child element of a run. Also appears as `a:defRPr` and
+ `a:endParaRPr` in paragraph and `a:defRPr` in list style elements.
+ """
+
+ def __init__(self, rPr: CT_TextCharacterProperties):
+ super(Font, self).__init__()
+ self._element = self._rPr = rPr
+
+ @property
+ def bold(self) -> bool | None:
+ """Get or set boolean bold value of |Font|, e.g. `paragraph.font.bold = True`.
+
+ If set to |None|, the bold setting is cleared and is inherited from an enclosing shape's
+ setting, or a setting in a style or master. Returns None if no bold attribute is present,
+ meaning the effective bold value is inherited from a master or the theme.
+ """
+ return self._rPr.b
+
+ @bold.setter
+ def bold(self, value: bool | None):
+ self._rPr.b = value
+
+ @lazyproperty
+ def color(self) -> ColorFormat:
+ """The |ColorFormat| instance that provides access to the color settings for this font."""
+ if self.fill.type != MSO_FILL.SOLID:
+ self.fill.solid()
+ return self.fill.fore_color
+
+ @lazyproperty
+ def fill(self) -> FillFormat:
+ """|FillFormat| instance for this font.
+
+ Provides access to fill properties such as fill color.
+ """
+ return FillFormat.from_fill_parent(self._rPr)
+
+ @property
+ def italic(self) -> bool | None:
+ """Get or set boolean italic value of |Font| instance.
+
+ Has the same behaviors as bold with respect to None values.
+ """
+ return self._rPr.i
+
+ @italic.setter
+ def italic(self, value: bool | None):
+ self._rPr.i = value
+
+ @property
+ def language_id(self) -> MSO_LANGUAGE_ID | None:
+ """Get or set the language id of this |Font| instance.
+
+ The language id is a member of the :ref:`MsoLanguageId` enumeration. Assigning |None|
+ removes any language setting, the same behavior as assigning `MSO_LANGUAGE_ID.NONE`.
+ """
+ lang = self._rPr.lang
+ if lang is None:
+ return MSO_LANGUAGE_ID.NONE
+ return self._rPr.lang
+
+ @language_id.setter
+ def language_id(self, value: MSO_LANGUAGE_ID | None):
+ if value == MSO_LANGUAGE_ID.NONE:
+ value = None
+ self._rPr.lang = value
+
+ @property
+ def name(self) -> str | None:
+ """Get or set the typeface name for this |Font| instance.
+
+ Causes the text it controls to appear in the named font, if a matching font is found.
+ Returns |None| if the typeface is currently inherited from the theme. Setting it to |None|
+ removes any override of the theme typeface.
+ """
+ latin = self._rPr.latin
+ if latin is None:
+ return None
+ return latin.typeface
+
+ @name.setter
+ def name(self, value: str | None):
+ if value is None:
+ self._rPr._remove_latin() # pyright: ignore[reportPrivateUsage]
+ else:
+ latin = self._rPr.get_or_add_latin()
+ latin.typeface = value
+
+ @property
+ def size(self) -> Length | None:
+ """Indicates the font height in English Metric Units (EMU).
+
+ Read/write. |None| indicates the font size should be inherited from its style hierarchy,
+ such as a placeholder or document defaults (usually 18pt). |Length| is a subclass of |int|
+ having properties for convenient conversion into points or other length units. Likewise,
+ the :class:`pptx.util.Pt` class allows convenient specification of point values::
+
+ >>> font.size = Pt(24)
+ >>> font.size
+ 304800
+ >>> font.size.pt
+ 24.0
+ """
+ sz = self._rPr.sz
+ if sz is None:
+ return None
+ return Centipoints(sz)
+
+ @size.setter
+ def size(self, emu: Length | None):
+ if emu is None:
+ self._rPr.sz = None
+ else:
+ sz = Emu(emu).centipoints
+ self._rPr.sz = sz
+
+ @property
+ def underline(self) -> bool | MSO_TEXT_UNDERLINE_TYPE | None:
+ """Indicaties the underline setting for this font.
+
+ Value is |True|, |False|, |None|, or a member of the :ref:`MsoTextUnderlineType`
+ enumeration. |None| is the default and indicates the underline setting should be inherited
+ from the style hierarchy, such as from a placeholder. |True| indicates single underline.
+ |False| indicates no underline. Other settings such as double and wavy underlining are
+ indicated with members of the :ref:`MsoTextUnderlineType` enumeration.
+ """
+ u = self._rPr.u
+ if u is MSO_UNDERLINE.NONE:
+ return False
+ if u is MSO_UNDERLINE.SINGLE_LINE:
+ return True
+ return u
+
+ @underline.setter
+ def underline(self, value: bool | MSO_TEXT_UNDERLINE_TYPE | None):
+ if value is True:
+ value = MSO_UNDERLINE.SINGLE_LINE
+ elif value is False:
+ value = MSO_UNDERLINE.NONE
+ self._element.u = value
+
+
+class _Hyperlink(Subshape):
+ """Text run hyperlink object.
+
+ Corresponds to `a:hlinkClick` child element of the run's properties element (`a:rPr`).
+ """
+
+ def __init__(self, rPr: CT_TextCharacterProperties, parent: ProvidesPart):
+ super(_Hyperlink, self).__init__(parent)
+ self._rPr = rPr
+
+ @property
+ def address(self) -> str | None:
+ """The URL of the hyperlink.
+
+ Read/write. URL can be on http, https, mailto, or file scheme; others may work.
+ """
+ if self._hlinkClick is None:
+ return None
+ return self.part.target_ref(self._hlinkClick.rId)
+
+ @address.setter
+ def address(self, url: str | None):
+ # implements all three of add, change, and remove hyperlink
+ if self._hlinkClick is not None:
+ self._remove_hlinkClick()
+ if url:
+ self._add_hlinkClick(url)
+
+ def _add_hlinkClick(self, url: str):
+ rId = self.part.relate_to(url, RT.HYPERLINK, is_external=True)
+ self._rPr.add_hlinkClick(rId)
+
+ @property
+ def _hlinkClick(self) -> CT_Hyperlink | None:
+ return self._rPr.hlinkClick
+
+ def _remove_hlinkClick(self):
+ assert self._hlinkClick is not None
+ self.part.drop_rel(self._hlinkClick.rId)
+ self._rPr._remove_hlinkClick() # pyright: ignore[reportPrivateUsage]
+
+
+class _Paragraph(Subshape):
+ """Paragraph object. Not intended to be constructed directly."""
+
+ def __init__(self, p: CT_TextParagraph, parent: ProvidesPart):
+ super(_Paragraph, self).__init__(parent)
+ self._element = self._p = p
+
+ def add_line_break(self):
+ """Add line break at end of this paragraph."""
+ self._p.add_br()
+
+ def add_run(self) -> _Run:
+ """Return a new run appended to the runs in this paragraph."""
+ r = self._p.add_r()
+ return _Run(r, self)
+
+ @property
+ def alignment(self) -> PP_PARAGRAPH_ALIGNMENT | None:
+ """Horizontal alignment of this paragraph.
+
+ The value |None| indicates the paragraph should 'inherit' its effective value from its
+ style hierarchy. Assigning |None| removes any explicit setting, causing its inherited
+ value to be used.
+ """
+ return self._pPr.algn
+
+ @alignment.setter
+ def alignment(self, value: PP_PARAGRAPH_ALIGNMENT | None):
+ self._pPr.algn = value
+
+ def clear(self):
+ """Remove all content from this paragraph.
+
+ Paragraph properties are preserved. Content includes runs, line breaks, and fields.
+ """
+ for elm in self._element.content_children:
+ self._element.remove(elm)
+ return self
+
+ @property
+ def font(self) -> Font:
+ """|Font| object containing default character properties for the runs in this paragraph.
+
+ These character properties override default properties inherited from parent objects such
+ as the text frame the paragraph is contained in and they may be overridden by character
+ properties set at the run level.
+ """
+ return Font(self._defRPr)
+
+ @property
+ def level(self) -> int:
+ """Indentation level of this paragraph.
+
+ Read-write. Integer in range 0..8 inclusive. 0 represents a top-level paragraph and is the
+ default value. Indentation level is most commonly encountered in a bulleted list, as is
+ found on a word bullet slide.
+ """
+ return self._pPr.lvl
+
+ @level.setter
+ def level(self, level: int):
+ self._pPr.lvl = level
+
+ @property
+ def line_spacing(self) -> int | float | Length | None:
+ """The space between baselines in successive lines of this paragraph.
+
+ A value of |None| indicates no explicit value is assigned and its effective value is
+ inherited from the paragraph's style hierarchy. A numeric value, e.g. `2` or `1.5`,
+ indicates spacing is applied in multiples of line heights. A |Length| value such as
+ `Pt(12)` indicates spacing is a fixed height. The |Pt| value class is a convenient way to
+ apply line spacing in units of points.
+ """
+ pPr = self._p.pPr
+ if pPr is None:
+ return None
+ return pPr.line_spacing
+
+ @line_spacing.setter
+ def line_spacing(self, value: int | float | Length | None):
+ pPr = self._p.get_or_add_pPr()
+ pPr.line_spacing = value
+
+ @property
+ def runs(self) -> tuple[_Run, ...]:
+ """Sequence of runs in this paragraph."""
+ return tuple(_Run(r, self) for r in self._element.r_lst)
+
+ @property
+ def space_after(self) -> Length | None:
+ """The spacing to appear between this paragraph and the subsequent paragraph.
+
+ A value of |None| indicates no explicit value is assigned and its effective value is
+ inherited from the paragraph's style hierarchy. |Length| objects provide convenience
+ properties, such as `.pt` and `.inches`, that allow easy conversion to various length
+ units.
+ """
+ pPr = self._p.pPr
+ if pPr is None:
+ return None
+ return pPr.space_after
+
+ @space_after.setter
+ def space_after(self, value: Length | None):
+ pPr = self._p.get_or_add_pPr()
+ pPr.space_after = value
+
+ @property
+ def space_before(self) -> Length | None:
+ """The spacing to appear between this paragraph and the prior paragraph.
+
+ A value of |None| indicates no explicit value is assigned and its effective value is
+ inherited from the paragraph's style hierarchy. |Length| objects provide convenience
+ properties, such as `.pt` and `.cm`, that allow easy conversion to various length units.
+ """
+ pPr = self._p.pPr
+ if pPr is None:
+ return None
+ return pPr.space_before
+
+ @space_before.setter
+ def space_before(self, value: Length | None):
+ pPr = self._p.get_or_add_pPr()
+ pPr.space_before = value
+
+ @property
+ def text(self) -> str:
+ """Text of paragraph as a single string.
+
+ Read/write. This value is formed by concatenating the text in each run and field making up
+ the paragraph, adding a vertical-tab character (`"\\v"`) for each line-break element
+ (`<a:br>`, soft carriage-return) encountered.
+
+ While the encoding of line-breaks as a vertical tab might be surprising at first, doing so
+ is consistent with PowerPoint's clipboard copy behavior and allows a line-break to be
+ distinguished from a paragraph boundary within the str return value.
+
+ Assignment causes all content in the paragraph to be replaced. Each vertical-tab character
+ (`"\\v"`) in the assigned str is translated to a line-break, as is each line-feed
+ character (`"\\n"`). Contrast behavior of line-feed character in `TextFrame.text` setter.
+ If line-feed characters are intended to produce new paragraphs, use `TextFrame.text`
+ instead. Any other control characters in the assigned string are escaped as a hex
+ representation like "_x001B_" (for ESC (ASCII 27) in this example).
+ """
+ return "".join(elm.text for elm in self._element.content_children)
+
+ @text.setter
+ def text(self, text: str):
+ self.clear()
+ self._element.append_text(text)
+
+ @property
+ def _defRPr(self) -> CT_TextCharacterProperties:
+ """The element that defines the default run properties for runs in this paragraph.
+
+ Causes the element to be added if not present.
+ """
+ return self._pPr.get_or_add_defRPr()
+
+ @property
+ def _pPr(self) -> CT_TextParagraphProperties:
+ """Contains the properties for this paragraph.
+
+ Causes the element to be added if not present.
+ """
+ return self._p.get_or_add_pPr()
+
+
+class _Run(Subshape):
+ """Text run object. Corresponds to `a:r` child element in a paragraph."""
+
+ def __init__(self, r: CT_RegularTextRun, parent: ProvidesPart):
+ super(_Run, self).__init__(parent)
+ self._r = r
+
+ @property
+ def font(self):
+ """|Font| instance containing run-level character properties for the text in this run.
+
+ Character properties can be and perhaps most often are inherited from parent objects such
+ as the paragraph and slide layout the run is contained in. Only those specifically
+ overridden at the run level are contained in the font object.
+ """
+ rPr = self._r.get_or_add_rPr()
+ return Font(rPr)
+
+ @lazyproperty
+ def hyperlink(self) -> _Hyperlink:
+ """Proxy for any `a:hlinkClick` element under the run properties element.
+
+ Created on demand, the hyperlink object is available whether an `a:hlinkClick` element is
+ present or not, and creates or deletes that element as appropriate in response to actions
+ on its methods and attributes.
+ """
+ rPr = self._r.get_or_add_rPr()
+ return _Hyperlink(rPr, self)
+
+ @property
+ def text(self):
+ """Read/write. A unicode string containing the text in this run.
+
+ Assignment replaces all text in the run. The assigned value can be a 7-bit ASCII
+ string, a UTF-8 encoded 8-bit string, or unicode. String values are converted to
+ unicode assuming UTF-8 encoding.
+
+ Any other control characters in the assigned string other than tab or newline
+ are escaped as a hex representation. For example, ESC (ASCII 27) is escaped as
+ "_x001B_". Contrast the behavior of `TextFrame.text` and `_Paragraph.text` with
+ respect to line-feed and vertical-tab characters.
+ """
+ return self._r.text
+
+ @text.setter
+ def text(self, text: str):
+ self._r.text = text
diff --git a/.venv/lib/python3.12/site-packages/pptx/types.py b/.venv/lib/python3.12/site-packages/pptx/types.py
new file mode 100644
index 00000000..46d86661
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/types.py
@@ -0,0 +1,36 @@
+"""Abstract types used by `python-pptx`."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from typing_extensions import Protocol
+
+if TYPE_CHECKING:
+ from pptx.opc.package import XmlPart
+ from pptx.util import Length
+
+
+class ProvidesExtents(Protocol):
+ """An object that has width and height."""
+
+ @property
+ def height(self) -> Length:
+ """Distance between top and bottom extents of shape in EMUs."""
+ ...
+
+ @property
+ def width(self) -> Length:
+ """Distance between left and right extents of shape in EMUs."""
+ ...
+
+
+class ProvidesPart(Protocol):
+ """An object that provides access to its XmlPart.
+
+ This type is for objects that need access to their part, possibly because they need access to
+ the package or related parts.
+ """
+
+ @property
+ def part(self) -> XmlPart: ...
diff --git a/.venv/lib/python3.12/site-packages/pptx/util.py b/.venv/lib/python3.12/site-packages/pptx/util.py
new file mode 100644
index 00000000..fdec7929
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/util.py
@@ -0,0 +1,214 @@
+"""Utility functions and classes."""
+
+from __future__ import annotations
+
+import functools
+from typing import Any, Callable, Generic, TypeVar, cast
+
+
+class Length(int):
+ """Base class for length classes Inches, Emu, Cm, Mm, and Pt.
+
+ Provides properties for converting length values to convenient units.
+ """
+
+ _EMUS_PER_INCH = 914400
+ _EMUS_PER_CENTIPOINT = 127
+ _EMUS_PER_CM = 360000
+ _EMUS_PER_MM = 36000
+ _EMUS_PER_PT = 12700
+
+ def __new__(cls, emu: int):
+ return int.__new__(cls, emu)
+
+ @property
+ def inches(self) -> float:
+ """Floating point length in inches."""
+ return self / float(self._EMUS_PER_INCH)
+
+ @property
+ def centipoints(self) -> int:
+ """Integer length in hundredths of a point (1/7200 inch).
+
+ Used internally because PowerPoint stores font size in centipoints.
+ """
+ return self // self._EMUS_PER_CENTIPOINT
+
+ @property
+ def cm(self) -> float:
+ """Floating point length in centimeters."""
+ return self / float(self._EMUS_PER_CM)
+
+ @property
+ def emu(self) -> int:
+ """Integer length in English Metric Units."""
+ return self
+
+ @property
+ def mm(self) -> float:
+ """Floating point length in millimeters."""
+ return self / float(self._EMUS_PER_MM)
+
+ @property
+ def pt(self) -> float:
+ """Floating point length in points."""
+ return self / float(self._EMUS_PER_PT)
+
+
+class Inches(Length):
+ """Convenience constructor for length in inches."""
+
+ def __new__(cls, inches: float):
+ emu = int(inches * Length._EMUS_PER_INCH)
+ return Length.__new__(cls, emu)
+
+
+class Centipoints(Length):
+ """Convenience constructor for length in hundredths of a point."""
+
+ def __new__(cls, centipoints: int):
+ emu = int(centipoints * Length._EMUS_PER_CENTIPOINT)
+ return Length.__new__(cls, emu)
+
+
+class Cm(Length):
+ """Convenience constructor for length in centimeters."""
+
+ def __new__(cls, cm: float):
+ emu = int(cm * Length._EMUS_PER_CM)
+ return Length.__new__(cls, emu)
+
+
+class Emu(Length):
+ """Convenience constructor for length in english metric units."""
+
+ def __new__(cls, emu: int):
+ return Length.__new__(cls, int(emu))
+
+
+class Mm(Length):
+ """Convenience constructor for length in millimeters."""
+
+ def __new__(cls, mm: float):
+ emu = int(mm * Length._EMUS_PER_MM)
+ return Length.__new__(cls, emu)
+
+
+class Pt(Length):
+ """Convenience value class for specifying a length in points."""
+
+ def __new__(cls, points: float):
+ emu = int(points * Length._EMUS_PER_PT)
+ return Length.__new__(cls, emu)
+
+
+_T = TypeVar("_T")
+
+
+class lazyproperty(Generic[_T]):
+ """Decorator like @property, but evaluated only on first access.
+
+ Like @property, this can only be used to decorate methods having only a `self` parameter, and
+ is accessed like an attribute on an instance, i.e. trailing parentheses are not used. Unlike
+ @property, the decorated method is only evaluated on first access; the resulting value is
+ cached and that same value returned on second and later access without re-evaluation of the
+ method.
+
+ Like @property, this class produces a *data descriptor* object, which is stored in the __dict__
+ of the *class* under the name of the decorated method ('fget' nominally). The cached value is
+ stored in the __dict__ of the *instance* under that same name.
+
+ Because it is a data descriptor (as opposed to a *non-data descriptor*), its `__get__()` method
+ is executed on each access of the decorated attribute; the __dict__ item of the same name is
+ "shadowed" by the descriptor.
+
+ While this may represent a performance improvement over a property, its greater benefit may be
+ its other characteristics. One common use is to construct collaborator objects, removing that
+ "real work" from the constructor, while still only executing once. It also de-couples client
+ code from any sequencing considerations; if it's accessed from more than one location, it's
+ assured it will be ready whenever needed.
+
+ Loosely based on: https://stackoverflow.com/a/6849299/1902513.
+
+ A lazyproperty is read-only. There is no counterpart to the optional "setter" (or deleter)
+ behavior of an @property. This is critically important to maintaining its immutability and
+ idempotence guarantees. Attempting to assign to a lazyproperty raises AttributeError
+ unconditionally.
+
+ The parameter names in the methods below correspond to this usage example::
+
+ class Obj(object)
+
+ @lazyproperty
+ def fget(self):
+ return 'some result'
+
+ obj = Obj()
+
+ Not suitable for wrapping a function (as opposed to a method) because it is not callable.
+ """
+
+ def __init__(self, fget: Callable[..., _T]) -> None:
+ """*fget* is the decorated method (a "getter" function).
+
+ A lazyproperty is read-only, so there is only an *fget* function (a regular
+ @property can also have an fset and fdel function). This name was chosen for
+ consistency with Python's `property` class which uses this name for the
+ corresponding parameter.
+ """
+ # --- maintain a reference to the wrapped getter method
+ self._fget = fget
+ # --- and store the name of that decorated method
+ self._name = fget.__name__
+ # --- adopt fget's __name__, __doc__, and other attributes
+ functools.update_wrapper(self, fget) # pyright: ignore
+
+ def __get__(self, obj: Any, type: Any = None) -> _T:
+ """Called on each access of 'fget' attribute on class or instance.
+
+ *self* is this instance of a lazyproperty descriptor "wrapping" the property
+ method it decorates (`fget`, nominally).
+
+ *obj* is the "host" object instance when the attribute is accessed from an
+ object instance, e.g. `obj = Obj(); obj.fget`. *obj* is None when accessed on
+ the class, e.g. `Obj.fget`.
+
+ *type* is the class hosting the decorated getter method (`fget`) on both class
+ and instance attribute access.
+ """
+ # --- when accessed on class, e.g. Obj.fget, just return this descriptor
+ # --- instance (patched above to look like fget).
+ if obj is None:
+ return self # type: ignore
+
+ # --- when accessed on instance, start by checking instance __dict__ for
+ # --- item with key matching the wrapped function's name
+ value = obj.__dict__.get(self._name)
+ if value is None:
+ # --- on first access, the __dict__ item will be absent. Evaluate fget()
+ # --- and store that value in the (otherwise unused) host-object
+ # --- __dict__ value of same name ('fget' nominally)
+ value = self._fget(obj)
+ obj.__dict__[self._name] = value
+ return cast(_T, value)
+
+ def __set__(self, obj: Any, value: Any) -> None:
+ """Raises unconditionally, to preserve read-only behavior.
+
+ This decorator is intended to implement immutable (and idempotent) object
+ attributes. For that reason, assignment to this property must be explicitly
+ prevented.
+
+ If this __set__ method was not present, this descriptor would become a
+ *non-data descriptor*. That would be nice because the cached value would be
+ accessed directly once set (__dict__ attrs have precedence over non-data
+ descriptors on instance attribute lookup). The problem is, there would be
+ nothing to stop assignment to the cached value, which would overwrite the result
+ of `fget()` and break both the immutability and idempotence guarantees of this
+ decorator.
+
+ The performance with this __set__() method in place was roughly 0.4 usec per
+ access when measured on a 2.8GHz development machine; so quite snappy and
+ probably not a rich target for optimization efforts.
+ """
+ raise AttributeError("can't set attribute")