init: 폴더구조 설계 및 인프라 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:31:53 +09:00
parent 34ad612281
commit f9c463f87a
1839 changed files with 323096 additions and 1 deletions
@@ -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
@@ -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