diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx/shapes')
10 files changed, 3294 insertions, 0 deletions
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 |
