init: 폴더구조 설계 및 인프라 설계
This commit is contained in:
@@ -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
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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)
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -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
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user