diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx')
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, {'"': """}) + + @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 Binary files differnew file mode 100644 index 00000000..e7fd6565 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/templates/default.pptx 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 Binary files differnew file mode 100644 index 00000000..b8660118 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/templates/docx-icon.emf 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 Binary files differnew file mode 100644 index 00000000..d0914e00 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/templates/generic-icon.emf 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 Binary files differnew file mode 100644 index 00000000..e9b1ce88 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/templates/pptx-icon.emf 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 Binary files differnew file mode 100644 index 00000000..658eac20 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/templates/xlsx-icon.emf 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") |