aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/pptx/shapes/autoshape.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx/shapes/autoshape.py')
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/shapes/autoshape.py355
1 files changed, 355 insertions, 0 deletions
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)