954 lines
34 KiB
Python
954 lines
34 KiB
Python
|
# encoding: utf-8
|
||
|
|
||
|
"""The shape tree, the structure that holds a slide's shapes."""
|
||
|
|
||
|
from __future__ import absolute_import, division, print_function, unicode_literals
|
||
|
|
||
|
from pptx.compat import BytesIO
|
||
|
from pptx.enum.shapes import PP_PLACEHOLDER
|
||
|
from pptx.media import SPEAKER_IMAGE_BYTES, Video
|
||
|
from pptx.opc.constants import CONTENT_TYPE as CT
|
||
|
from pptx.oxml.ns import qn
|
||
|
from pptx.oxml.shapes.graphfrm import CT_GraphicalObjectFrame
|
||
|
from pptx.oxml.shapes.picture import CT_Picture
|
||
|
from pptx.oxml.simpletypes import ST_Direction
|
||
|
from pptx.shapes.autoshape import AutoShapeType, Shape
|
||
|
from pptx.shapes.base import BaseShape
|
||
|
from pptx.shapes.connector import Connector
|
||
|
from pptx.shapes.freeform import FreeformBuilder
|
||
|
from pptx.shapes.graphfrm import GraphicFrame
|
||
|
from pptx.shapes.group import GroupShape
|
||
|
from pptx.shapes.picture import Movie, Picture
|
||
|
from pptx.shapes.placeholder import (
|
||
|
ChartPlaceholder,
|
||
|
LayoutPlaceholder,
|
||
|
MasterPlaceholder,
|
||
|
NotesSlidePlaceholder,
|
||
|
PicturePlaceholder,
|
||
|
PlaceholderGraphicFrame,
|
||
|
PlaceholderPicture,
|
||
|
SlidePlaceholder,
|
||
|
TablePlaceholder,
|
||
|
)
|
||
|
from pptx.shared import ParentedElementProxy
|
||
|
from pptx.util import lazyproperty
|
||
|
|
||
|
# +-- _BaseShapes
|
||
|
# | |
|
||
|
# | +-- _BaseGroupShapes
|
||
|
# | | |
|
||
|
# | | +-- GroupShapes
|
||
|
# | | |
|
||
|
# | | +-- SlideShapes
|
||
|
# | |
|
||
|
# | +-- LayoutShapes
|
||
|
# | |
|
||
|
# | +-- MasterShapes
|
||
|
# | |
|
||
|
# | +-- NotesSlideShapes
|
||
|
# | |
|
||
|
# | +-- BasePlaceholders
|
||
|
# | |
|
||
|
# | +-- LayoutPlaceholders
|
||
|
# | |
|
||
|
# | +-- MasterPlaceholders
|
||
|
# | |
|
||
|
# | +-- NotesSlidePlaceholders
|
||
|
# |
|
||
|
# +-- SlidePlaceholders
|
||
|
|
||
|
|
||
|
class _BaseShapes(ParentedElementProxy):
|
||
|
"""
|
||
|
Base class for a shape collection appearing in a slide-type object,
|
||
|
include Slide, SlideLayout, and SlideMaster, providing common methods.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, spTree, parent):
|
||
|
super(_BaseShapes, self).__init__(spTree, parent)
|
||
|
self._spTree = spTree
|
||
|
self._cached_max_shape_id = None
|
||
|
|
||
|
def __getitem__(self, idx):
|
||
|
"""
|
||
|
Return shape at *idx* in sequence, e.g. ``shapes[2]``.
|
||
|
"""
|
||
|
shape_elms = list(self._iter_member_elms())
|
||
|
try:
|
||
|
shape_elm = shape_elms[idx]
|
||
|
except IndexError:
|
||
|
raise IndexError("shape index out of range")
|
||
|
return self._shape_factory(shape_elm)
|
||
|
|
||
|
def __iter__(self):
|
||
|
"""
|
||
|
Generate a reference to each shape in the collection, in sequence.
|
||
|
"""
|
||
|
for shape_elm in self._iter_member_elms():
|
||
|
yield self._shape_factory(shape_elm)
|
||
|
|
||
|
def __len__(self):
|
||
|
"""
|
||
|
Return count of shapes in this shape tree. A group shape contributes
|
||
|
1 to the total, without regard to the number of shapes contained in
|
||
|
the group.
|
||
|
"""
|
||
|
shape_elms = list(self._iter_member_elms())
|
||
|
return len(shape_elms)
|
||
|
|
||
|
def clone_placeholder(self, placeholder):
|
||
|
"""
|
||
|
Add a new placeholder shape based on *placeholder*.
|
||
|
"""
|
||
|
sp = placeholder.element
|
||
|
ph_type, orient, sz, idx = (sp.ph_type, sp.ph_orient, sp.ph_sz, sp.ph_idx)
|
||
|
id_ = self._next_shape_id
|
||
|
name = self._next_ph_name(ph_type, id_, orient)
|
||
|
self._spTree.add_placeholder(id_, name, ph_type, orient, sz, idx)
|
||
|
|
||
|
def ph_basename(self, ph_type):
|
||
|
"""
|
||
|
Return the base name for a placeholder of *ph_type* in this shape
|
||
|
collection. There is some variance between slide types, for example
|
||
|
a notes slide uses a different name for the body placeholder, so this
|
||
|
method can be overriden by subclasses.
|
||
|
"""
|
||
|
return {
|
||
|
PP_PLACEHOLDER.BITMAP: "ClipArt Placeholder",
|
||
|
PP_PLACEHOLDER.BODY: "Text Placeholder",
|
||
|
PP_PLACEHOLDER.CENTER_TITLE: "Title",
|
||
|
PP_PLACEHOLDER.CHART: "Chart Placeholder",
|
||
|
PP_PLACEHOLDER.DATE: "Date Placeholder",
|
||
|
PP_PLACEHOLDER.FOOTER: "Footer Placeholder",
|
||
|
PP_PLACEHOLDER.HEADER: "Header Placeholder",
|
||
|
PP_PLACEHOLDER.MEDIA_CLIP: "Media Placeholder",
|
||
|
PP_PLACEHOLDER.OBJECT: "Content Placeholder",
|
||
|
PP_PLACEHOLDER.ORG_CHART: "SmartArt Placeholder",
|
||
|
PP_PLACEHOLDER.PICTURE: "Picture Placeholder",
|
||
|
PP_PLACEHOLDER.SLIDE_NUMBER: "Slide Number Placeholder",
|
||
|
PP_PLACEHOLDER.SUBTITLE: "Subtitle",
|
||
|
PP_PLACEHOLDER.TABLE: "Table Placeholder",
|
||
|
PP_PLACEHOLDER.TITLE: "Title",
|
||
|
}[ph_type]
|
||
|
|
||
|
@property
|
||
|
def turbo_add_enabled(self):
|
||
|
"""True if "turbo-add" mode is enabled. Read/Write.
|
||
|
|
||
|
EXPERIMENTAL: This feature can radically improve performance when
|
||
|
adding large numbers (hundreds of shapes) to a slide. It works by
|
||
|
caching the last shape ID used and incrementing that value to assign
|
||
|
the next shape id. This avoids repeatedly searching all shape ids in
|
||
|
the slide each time a new ID is required.
|
||
|
|
||
|
Performance is not noticeably improved for a slide with a relatively
|
||
|
small number of shapes, but because the search time rises with the
|
||
|
square of the shape count, this option can be useful for optimizing
|
||
|
generation of a slide composed of many shapes.
|
||
|
|
||
|
Shape-id collisions can occur (causing a repair error on load) if
|
||
|
more than one |Slide| object is used to interact with the same slide
|
||
|
in the presentation. Note that the |Slides| collection creates a new
|
||
|
|Slide| object each time a slide is accessed
|
||
|
(e.g. `slide = prs.slides[0]`, so you must be careful to limit use to
|
||
|
a single |Slide| object.
|
||
|
"""
|
||
|
return self._cached_max_shape_id is not None
|
||
|
|
||
|
@turbo_add_enabled.setter
|
||
|
def turbo_add_enabled(self, value):
|
||
|
enable = bool(value)
|
||
|
self._cached_max_shape_id = self._spTree.max_shape_id if enable else None
|
||
|
|
||
|
@staticmethod
|
||
|
def _is_member_elm(shape_elm):
|
||
|
"""
|
||
|
Return true if *shape_elm* represents a member of this collection,
|
||
|
False otherwise.
|
||
|
"""
|
||
|
return True
|
||
|
|
||
|
def _iter_member_elms(self):
|
||
|
"""
|
||
|
Generate each child of the ``<p:spTree>`` element that corresponds to
|
||
|
a shape, in the sequence they appear in the XML.
|
||
|
"""
|
||
|
for shape_elm in self._spTree.iter_shape_elms():
|
||
|
if self._is_member_elm(shape_elm):
|
||
|
yield shape_elm
|
||
|
|
||
|
def _next_ph_name(self, ph_type, id, orient):
|
||
|
"""
|
||
|
Next unique placeholder name for placeholder shape of type *ph_type*,
|
||
|
with id number *id* and orientation *orient*. Usually will be standard
|
||
|
placeholder root name suffixed with id-1, e.g.
|
||
|
_next_ph_name(ST_PlaceholderType.TBL, 4, 'horz') ==>
|
||
|
'Table Placeholder 3'. The number is incremented as necessary to make
|
||
|
the name unique within the collection. If *orient* is ``'vert'``, the
|
||
|
placeholder name is prefixed with ``'Vertical '``.
|
||
|
"""
|
||
|
basename = self.ph_basename(ph_type)
|
||
|
|
||
|
# prefix rootname with 'Vertical ' if orient is 'vert'
|
||
|
if orient == ST_Direction.VERT:
|
||
|
basename = "Vertical %s" % basename
|
||
|
|
||
|
# increment numpart as necessary to make name unique
|
||
|
numpart = id - 1
|
||
|
names = self._spTree.xpath("//p:cNvPr/@name")
|
||
|
while True:
|
||
|
name = "%s %d" % (basename, numpart)
|
||
|
if name not in names:
|
||
|
break
|
||
|
numpart += 1
|
||
|
|
||
|
return name
|
||
|
|
||
|
@property
|
||
|
def _next_shape_id(self):
|
||
|
"""Return a unique shape id suitable for use with a new shape.
|
||
|
|
||
|
The returned id is 1 greater than the maximum shape id used so far.
|
||
|
In practice, the minimum id is 2 because the spTree element is always
|
||
|
assigned id="1".
|
||
|
"""
|
||
|
# ---presence of cached-max-shape-id indicates turbo mode is on---
|
||
|
if self._cached_max_shape_id is not None:
|
||
|
self._cached_max_shape_id += 1
|
||
|
return self._cached_max_shape_id
|
||
|
|
||
|
return self._spTree.max_shape_id + 1
|
||
|
|
||
|
def _shape_factory(self, shape_elm):
|
||
|
"""
|
||
|
Return an instance of the appropriate shape proxy class for
|
||
|
*shape_elm*.
|
||
|
"""
|
||
|
return BaseShapeFactory(shape_elm, self)
|
||
|
|
||
|
|
||
|
class _BaseGroupShapes(_BaseShapes):
|
||
|
"""Base class for shape-trees that can add shapes."""
|
||
|
|
||
|
def __init__(self, grpSp, parent):
|
||
|
super(_BaseGroupShapes, self).__init__(grpSp, parent)
|
||
|
self._grpSp = grpSp
|
||
|
|
||
|
def add_chart(self, chart_type, x, y, cx, cy, chart_data):
|
||
|
"""Add a new chart of *chart_type* to the slide.
|
||
|
|
||
|
The chart is positioned at (*x*, *y*), has size (*cx*, *cy*), and
|
||
|
depicts *chart_data*. *chart_type* is one of the :ref:`XlChartType`
|
||
|
enumeration values. *chart_data* is a |ChartData| object populated
|
||
|
with the categories and series values for the chart.
|
||
|
|
||
|
Note that a |GraphicFrame| shape object is returned, not the |Chart|
|
||
|
object contained in that graphic frame shape. The chart object may be
|
||
|
accessed using the :attr:`chart` property of the returned
|
||
|
|GraphicFrame| object.
|
||
|
"""
|
||
|
rId = self.part.add_chart_part(chart_type, chart_data)
|
||
|
graphicFrame = self._add_chart_graphicFrame(rId, x, y, cx, cy)
|
||
|
self._recalculate_extents()
|
||
|
return self._shape_factory(graphicFrame)
|
||
|
|
||
|
def add_connector(self, connector_type, begin_x, begin_y, end_x, end_y):
|
||
|
"""Add a newly created connector shape to the end of this shape tree.
|
||
|
|
||
|
*connector_type* is a member of the :ref:`MsoConnectorType`
|
||
|
enumeration and the end-point values are specified as EMU values. The
|
||
|
returned connector is of type *connector_type* and has begin and end
|
||
|
points as specified.
|
||
|
"""
|
||
|
cxnSp = self._add_cxnSp(connector_type, begin_x, begin_y, end_x, end_y)
|
||
|
self._recalculate_extents()
|
||
|
return self._shape_factory(cxnSp)
|
||
|
|
||
|
def add_group_shape(self, shapes=[]):
|
||
|
"""Return a |GroupShape| object newly appended to this shape tree.
|
||
|
|
||
|
The group shape is empty and must be populated with shapes using
|
||
|
methods on its shape tree, available on its `.shapes` property. The
|
||
|
position and extents of the group shape are determined by the shapes
|
||
|
it contains; its position and extents are recalculated each time
|
||
|
a shape is added to it.
|
||
|
"""
|
||
|
grpSp = self._element.add_grpSp()
|
||
|
for shape in shapes:
|
||
|
grpSp.insert_element_before(shape._element, "p:extLst")
|
||
|
if shapes:
|
||
|
grpSp.recalculate_extents()
|
||
|
return self._shape_factory(grpSp)
|
||
|
|
||
|
def add_picture(self, image_file, left, top, width=None, height=None):
|
||
|
"""Add picture shape displaying image in *image_file*.
|
||
|
|
||
|
*image_file* can be either a path to a file (a string) or a file-like
|
||
|
object. The picture is positioned with its top-left corner at (*top*,
|
||
|
*left*). If *width* and *height* are both |None|, the native size of
|
||
|
the image is used. If only one of *width* or *height* is used, the
|
||
|
unspecified dimension is calculated to preserve the aspect ratio of
|
||
|
the image. If both are specified, the picture is stretched to fit,
|
||
|
without regard to its native aspect ratio.
|
||
|
"""
|
||
|
image_part, rId = self.part.get_or_add_image_part(image_file)
|
||
|
pic = self._add_pic_from_image_part(image_part, rId, left, top, width, height)
|
||
|
self._recalculate_extents()
|
||
|
return self._shape_factory(pic)
|
||
|
|
||
|
def add_shape(self, autoshape_type_id, left, top, width, height):
|
||
|
"""Return new |Shape| object appended to this shape tree.
|
||
|
|
||
|
*autoshape_type_id* is a member of :ref:`MsoAutoShapeType` e.g.
|
||
|
``MSO_SHAPE.RECTANGLE`` specifying the type of shape to be added. The
|
||
|
remaining arguments specify the new shape's position and size.
|
||
|
"""
|
||
|
autoshape_type = AutoShapeType(autoshape_type_id)
|
||
|
sp = self._add_sp(autoshape_type, left, top, width, height)
|
||
|
self._recalculate_extents()
|
||
|
return self._shape_factory(sp)
|
||
|
|
||
|
def add_textbox(self, left, top, width, height):
|
||
|
"""Return newly added text box shape appended to this shape tree.
|
||
|
|
||
|
The text box is of the specified size, located at the specified
|
||
|
position on the slide.
|
||
|
"""
|
||
|
sp = self._add_textbox_sp(left, top, width, height)
|
||
|
self._recalculate_extents()
|
||
|
return self._shape_factory(sp)
|
||
|
|
||
|
def build_freeform(self, start_x=0, start_y=0, scale=1.0):
|
||
|
"""Return |FreeformBuilder| object to specify a freeform shape.
|
||
|
|
||
|
The optional *start_x* and *start_y* arguments specify the starting
|
||
|
pen position in local coordinates. They will be rounded to the
|
||
|
nearest integer before use and each default to zero.
|
||
|
|
||
|
The optional *scale* argument specifies the size of local coordinates
|
||
|
proportional to slide coordinates (EMU). If the vertical scale is
|
||
|
different than the horizontal scale (local coordinate units are
|
||
|
"rectangular"), a pair of numeric values can be provided as the
|
||
|
*scale* argument, e.g. `scale=(1.0, 2.0)`. In this case the first
|
||
|
number is interpreted as the horizontal (X) scale and the second as
|
||
|
the vertical (Y) scale.
|
||
|
|
||
|
A convenient method for calculating scale is to divide a |Length|
|
||
|
object by an equivalent count of local coordinate units, e.g.
|
||
|
`scale = Inches(1)/1000` for 1000 local units per inch.
|
||
|
"""
|
||
|
try:
|
||
|
x_scale, y_scale = scale
|
||
|
except TypeError:
|
||
|
x_scale = y_scale = scale
|
||
|
|
||
|
return FreeformBuilder.new(self, start_x, start_y, x_scale, y_scale)
|
||
|
|
||
|
def index(self, shape):
|
||
|
"""Return the index of *shape* in this sequence.
|
||
|
|
||
|
Raises |ValueError| if *shape* is not in the collection.
|
||
|
"""
|
||
|
shape_elms = list(self._element.iter_shape_elms())
|
||
|
return shape_elms.index(shape.element)
|
||
|
|
||
|
def _add_chart_graphicFrame(self, rId, x, y, cx, cy):
|
||
|
"""Return new `p:graphicFrame` element appended to this shape tree.
|
||
|
|
||
|
The `p:graphicFrame` element has the specified position and size and
|
||
|
refers to the chart part identified by *rId*.
|
||
|
"""
|
||
|
shape_id = self._next_shape_id
|
||
|
name = "Chart %d" % (shape_id - 1)
|
||
|
graphicFrame = CT_GraphicalObjectFrame.new_chart_graphicFrame(
|
||
|
shape_id, name, rId, x, y, cx, cy
|
||
|
)
|
||
|
self._spTree.append(graphicFrame)
|
||
|
return graphicFrame
|
||
|
|
||
|
def _add_cxnSp(self, connector_type, begin_x, begin_y, end_x, end_y):
|
||
|
"""Return a newly-added `p:cxnSp` element as specified.
|
||
|
|
||
|
The `p:cxnSp` element is for a connector of *connector_type*
|
||
|
beginning at (*begin_x*, *begin_y*) and extending to
|
||
|
(*end_x*, *end_y*).
|
||
|
"""
|
||
|
id_ = self._next_shape_id
|
||
|
name = "Connector %d" % (id_ - 1)
|
||
|
|
||
|
flipH, flipV = begin_x > end_x, begin_y > end_y
|
||
|
x, y = min(begin_x, end_x), min(begin_y, end_y)
|
||
|
cx, cy = abs(end_x - begin_x), abs(end_y - begin_y)
|
||
|
|
||
|
return self._element.add_cxnSp(
|
||
|
id_, name, connector_type, x, y, cx, cy, flipH, flipV
|
||
|
)
|
||
|
|
||
|
def _add_pic_from_image_part(self, image_part, rId, x, y, cx, cy):
|
||
|
"""Return a newly appended `p:pic` element as specified.
|
||
|
|
||
|
The `p:pic` element displays the image in *image_part* with size and
|
||
|
position specified by *x*, *y*, *cx*, and *cy*. The element is
|
||
|
appended to the shape tree, causing it to be displayed first in
|
||
|
z-order on the slide.
|
||
|
"""
|
||
|
id_ = self._next_shape_id
|
||
|
scaled_cx, scaled_cy = image_part.scale(cx, cy)
|
||
|
name = "Picture %d" % (id_ - 1)
|
||
|
desc = image_part.desc
|
||
|
pic = self._grpSp.add_pic(id_, name, desc, rId, x, y, scaled_cx, scaled_cy)
|
||
|
return pic
|
||
|
|
||
|
def _add_sp(self, autoshape_type, x, y, cx, cy):
|
||
|
"""Return newly-added `p:sp` element as specified.
|
||
|
|
||
|
`p:sp` element is of *autoshape_type* at position (*x*, *y*) and of
|
||
|
size (*cx*, *cy*).
|
||
|
"""
|
||
|
id_ = self._next_shape_id
|
||
|
name = "%s %d" % (autoshape_type.basename, id_ - 1)
|
||
|
sp = self._grpSp.add_autoshape(id_, name, autoshape_type.prst, x, y, cx, cy)
|
||
|
return sp
|
||
|
|
||
|
def _add_textbox_sp(self, x, y, cx, cy):
|
||
|
"""Return newly-appended textbox `p:sp` element.
|
||
|
|
||
|
Element has position (*x*, *y*) and size (*cx*, *cy*).
|
||
|
"""
|
||
|
id_ = self._next_shape_id
|
||
|
name = "TextBox %d" % (id_ - 1)
|
||
|
sp = self._spTree.add_textbox(id_, name, x, y, cx, cy)
|
||
|
return sp
|
||
|
|
||
|
def _recalculate_extents(self):
|
||
|
"""Adjust position and size to incorporate all contained shapes.
|
||
|
|
||
|
This would typically be called when a contained shape is added,
|
||
|
removed, or its position or size updated.
|
||
|
"""
|
||
|
# ---default behavior is to do nothing, GroupShapes overrides to
|
||
|
# produce the distinctive behavior of groups and subgroups.---
|
||
|
pass
|
||
|
|
||
|
|
||
|
class GroupShapes(_BaseGroupShapes):
|
||
|
"""The sequence of child shapes belonging to a group shape.
|
||
|
|
||
|
Note that this collection can itself contain a group shape, making this
|
||
|
part of a recursive, tree data structure (acyclic graph).
|
||
|
"""
|
||
|
|
||
|
def _recalculate_extents(self):
|
||
|
"""Adjust position and size to incorporate all contained shapes.
|
||
|
|
||
|
This would typically be called when a contained shape is added,
|
||
|
removed, or its position or size updated.
|
||
|
"""
|
||
|
self._grpSp.recalculate_extents()
|
||
|
|
||
|
|
||
|
class SlideShapes(_BaseGroupShapes):
|
||
|
"""Sequence of shapes appearing on a slide.
|
||
|
|
||
|
The first shape in the sequence is the backmost in z-order and the last
|
||
|
shape is topmost. Supports indexed access, len(), index(), and iteration.
|
||
|
"""
|
||
|
|
||
|
def add_movie(
|
||
|
self,
|
||
|
movie_file,
|
||
|
left,
|
||
|
top,
|
||
|
width,
|
||
|
height,
|
||
|
poster_frame_image=None,
|
||
|
mime_type=CT.VIDEO,
|
||
|
):
|
||
|
"""Return newly added movie shape displaying video in *movie_file*.
|
||
|
|
||
|
**EXPERIMENTAL.** This method has important limitations:
|
||
|
|
||
|
* The size must be specified; no auto-scaling such as that provided
|
||
|
by :meth:`add_picture` is performed.
|
||
|
* The MIME type of the video file should be specified, e.g.
|
||
|
'video/mp4'. The provided video file is not interrogated for its
|
||
|
type. The MIME type `video/unknown` is used by default (and works
|
||
|
fine in tests as of this writing).
|
||
|
* A poster frame image must be provided, it cannot be automatically
|
||
|
extracted from the video file. If no poster frame is provided, the
|
||
|
default "media loudspeaker" image will be used.
|
||
|
|
||
|
Return a newly added movie shape to the slide, positioned at (*left*,
|
||
|
*top*), having size (*width*, *height*), and containing *movie_file*.
|
||
|
Before the video is started, *poster_frame_image* is displayed as
|
||
|
a placeholder for the video.
|
||
|
"""
|
||
|
movie_pic = _MoviePicElementCreator.new_movie_pic(
|
||
|
self,
|
||
|
self._next_shape_id,
|
||
|
movie_file,
|
||
|
left,
|
||
|
top,
|
||
|
width,
|
||
|
height,
|
||
|
poster_frame_image,
|
||
|
mime_type,
|
||
|
)
|
||
|
self._spTree.append(movie_pic)
|
||
|
self._add_video_timing(movie_pic)
|
||
|
return self._shape_factory(movie_pic)
|
||
|
|
||
|
def add_table(self, rows, cols, left, top, width, height):
|
||
|
"""
|
||
|
Add a |GraphicFrame| object containing a table with the specified
|
||
|
number of *rows* and *cols* and the specified position and size.
|
||
|
*width* is evenly distributed between the columns of the new table.
|
||
|
Likewise, *height* is evenly distributed between the rows. Note that
|
||
|
the ``.table`` property on the returned |GraphicFrame| shape must be
|
||
|
used to access the enclosed |Table| object.
|
||
|
"""
|
||
|
graphicFrame = self._add_graphicFrame_containing_table(
|
||
|
rows, cols, left, top, width, height
|
||
|
)
|
||
|
graphic_frame = self._shape_factory(graphicFrame)
|
||
|
return graphic_frame
|
||
|
|
||
|
def clone_layout_placeholders(self, slide_layout):
|
||
|
"""
|
||
|
Add placeholder shapes based on those in *slide_layout*. Z-order of
|
||
|
placeholders is preserved. Latent placeholders (date, slide number,
|
||
|
and footer) are not cloned.
|
||
|
"""
|
||
|
for placeholder in slide_layout.iter_cloneable_placeholders():
|
||
|
self.clone_placeholder(placeholder)
|
||
|
|
||
|
@property
|
||
|
def placeholders(self):
|
||
|
"""
|
||
|
Instance of |SlidePlaceholders| containing sequence of placeholder
|
||
|
shapes in this slide.
|
||
|
"""
|
||
|
return self.parent.placeholders
|
||
|
|
||
|
@property
|
||
|
def title(self):
|
||
|
"""
|
||
|
The title placeholder shape on the slide or |None| if the slide has
|
||
|
no title placeholder.
|
||
|
"""
|
||
|
for elm in self._spTree.iter_ph_elms():
|
||
|
if elm.ph_idx == 0:
|
||
|
return self._shape_factory(elm)
|
||
|
return None
|
||
|
|
||
|
def _add_graphicFrame_containing_table(self, rows, cols, x, y, cx, cy):
|
||
|
"""
|
||
|
Return a newly added ``<p:graphicFrame>`` element containing a table
|
||
|
as specified by the parameters.
|
||
|
"""
|
||
|
_id = self._next_shape_id
|
||
|
name = "Table %d" % (_id - 1)
|
||
|
graphicFrame = self._spTree.add_table(_id, name, rows, cols, x, y, cx, cy)
|
||
|
return graphicFrame
|
||
|
|
||
|
def _add_video_timing(self, pic):
|
||
|
"""Add a `p:video` element under `p:sld/p:timing`.
|
||
|
|
||
|
The element will refer to the specified *pic* element by its shape
|
||
|
id, and cause the video play controls to appear for that video.
|
||
|
"""
|
||
|
sld = self._spTree.xpath("/p:sld")[0]
|
||
|
childTnLst = sld.get_or_add_childTnLst()
|
||
|
childTnLst.add_video(pic.shape_id)
|
||
|
|
||
|
def _shape_factory(self, shape_elm):
|
||
|
"""
|
||
|
Return an instance of the appropriate shape proxy class for
|
||
|
*shape_elm*.
|
||
|
"""
|
||
|
return SlideShapeFactory(shape_elm, self)
|
||
|
|
||
|
|
||
|
class LayoutShapes(_BaseShapes):
|
||
|
"""
|
||
|
Sequence of shapes appearing on a slide layout. The first shape in the
|
||
|
sequence is the backmost in z-order and the last shape is topmost.
|
||
|
Supports indexed access, len(), index(), and iteration.
|
||
|
"""
|
||
|
|
||
|
def _shape_factory(self, shape_elm):
|
||
|
"""
|
||
|
Return an instance of the appropriate shape proxy class for
|
||
|
*shape_elm*.
|
||
|
"""
|
||
|
return _LayoutShapeFactory(shape_elm, self)
|
||
|
|
||
|
|
||
|
class MasterShapes(_BaseShapes):
|
||
|
"""
|
||
|
Sequence of shapes appearing on a slide master. The first shape in the
|
||
|
sequence is the backmost in z-order and the last shape is topmost.
|
||
|
Supports indexed access, len(), and iteration.
|
||
|
"""
|
||
|
|
||
|
def _shape_factory(self, shape_elm):
|
||
|
"""
|
||
|
Return an instance of the appropriate shape proxy class for
|
||
|
*shape_elm*.
|
||
|
"""
|
||
|
return _MasterShapeFactory(shape_elm, self)
|
||
|
|
||
|
|
||
|
class NotesSlideShapes(_BaseShapes):
|
||
|
"""
|
||
|
Sequence of shapes appearing on a notes slide. The first shape in the
|
||
|
sequence is the backmost in z-order and the last shape is topmost.
|
||
|
Supports indexed access, len(), index(), and iteration.
|
||
|
"""
|
||
|
|
||
|
def ph_basename(self, ph_type):
|
||
|
"""
|
||
|
Return the base name for a placeholder of *ph_type* in this shape
|
||
|
collection. A notes slide uses a different name for the body
|
||
|
placeholder and has some unique placeholder types, so this
|
||
|
method overrides the default in the base class.
|
||
|
"""
|
||
|
return {
|
||
|
PP_PLACEHOLDER.BODY: "Notes Placeholder",
|
||
|
PP_PLACEHOLDER.DATE: "Date Placeholder",
|
||
|
PP_PLACEHOLDER.FOOTER: "Footer Placeholder",
|
||
|
PP_PLACEHOLDER.HEADER: "Header Placeholder",
|
||
|
PP_PLACEHOLDER.SLIDE_IMAGE: "Slide Image Placeholder",
|
||
|
PP_PLACEHOLDER.SLIDE_NUMBER: "Slide Number Placeholder",
|
||
|
}[ph_type]
|
||
|
|
||
|
def _shape_factory(self, shape_elm):
|
||
|
"""
|
||
|
Return an instance of the appropriate shape proxy class for
|
||
|
*shape_elm* appearing on a notes slide.
|
||
|
"""
|
||
|
return _NotesSlideShapeFactory(shape_elm, self)
|
||
|
|
||
|
|
||
|
class BasePlaceholders(_BaseShapes):
|
||
|
"""
|
||
|
Base class for placeholder collections that differentiate behaviors for
|
||
|
a master, layout, and slide. By default, placeholder shapes are
|
||
|
constructed using |BaseShapeFactory|. Subclasses should override
|
||
|
:method:`_shape_factory` to use custom placeholder classes.
|
||
|
"""
|
||
|
|
||
|
@staticmethod
|
||
|
def _is_member_elm(shape_elm):
|
||
|
"""
|
||
|
True if *shape_elm* is a placeholder shape, False otherwise.
|
||
|
"""
|
||
|
return shape_elm.has_ph_elm
|
||
|
|
||
|
|
||
|
class LayoutPlaceholders(BasePlaceholders):
|
||
|
"""
|
||
|
Sequence of |LayoutPlaceholder| instances representing the placeholder
|
||
|
shapes on a slide layout.
|
||
|
"""
|
||
|
|
||
|
def get(self, idx, default=None):
|
||
|
"""
|
||
|
Return the first placeholder shape with matching *idx* value, or
|
||
|
*default* if not found.
|
||
|
"""
|
||
|
for placeholder in self:
|
||
|
if placeholder.element.ph_idx == idx:
|
||
|
return placeholder
|
||
|
return default
|
||
|
|
||
|
def _shape_factory(self, shape_elm):
|
||
|
"""
|
||
|
Return an instance of the appropriate shape proxy class for
|
||
|
*shape_elm*.
|
||
|
"""
|
||
|
return _LayoutShapeFactory(shape_elm, self)
|
||
|
|
||
|
|
||
|
class MasterPlaceholders(BasePlaceholders):
|
||
|
"""
|
||
|
Sequence of _MasterPlaceholder instances representing the placeholder
|
||
|
shapes on a slide master.
|
||
|
"""
|
||
|
|
||
|
def get(self, ph_type, default=None):
|
||
|
"""
|
||
|
Return the first placeholder shape with type *ph_type* (e.g. 'body'),
|
||
|
or *default* if no such placeholder shape is present in the
|
||
|
collection.
|
||
|
"""
|
||
|
for placeholder in self:
|
||
|
if placeholder.ph_type == ph_type:
|
||
|
return placeholder
|
||
|
return default
|
||
|
|
||
|
def _shape_factory(self, shape_elm):
|
||
|
"""
|
||
|
Return an instance of the appropriate shape proxy class for
|
||
|
*shape_elm*.
|
||
|
"""
|
||
|
return _MasterShapeFactory(shape_elm, self)
|
||
|
|
||
|
|
||
|
class NotesSlidePlaceholders(MasterPlaceholders):
|
||
|
"""
|
||
|
Sequence of placeholder shapes on a notes slide.
|
||
|
"""
|
||
|
|
||
|
def _shape_factory(self, placeholder_elm):
|
||
|
"""
|
||
|
Return an instance of the appropriate placeholder proxy class for
|
||
|
*placeholder_elm*.
|
||
|
"""
|
||
|
return _NotesSlideShapeFactory(placeholder_elm, self)
|
||
|
|
||
|
|
||
|
class SlidePlaceholders(ParentedElementProxy):
|
||
|
"""
|
||
|
Collection of placeholder shapes on a slide. Supports iteration,
|
||
|
:func:`len`, and dictionary-style lookup on the `idx` value of the
|
||
|
placeholders it contains.
|
||
|
"""
|
||
|
|
||
|
__slots__ = ()
|
||
|
|
||
|
def __getitem__(self, idx):
|
||
|
"""
|
||
|
Access placeholder shape having *idx*. Note that while this looks
|
||
|
like list access, idx is actually a dictionary key and will raise
|
||
|
|KeyError| if no placeholder with that idx value is in the
|
||
|
collection.
|
||
|
"""
|
||
|
for e in self._element.iter_ph_elms():
|
||
|
if e.ph_idx == idx:
|
||
|
return SlideShapeFactory(e, self)
|
||
|
raise KeyError("no placeholder on this slide with idx == %d" % idx)
|
||
|
|
||
|
def __iter__(self):
|
||
|
"""
|
||
|
Generate placeholder shapes in `idx` order.
|
||
|
"""
|
||
|
ph_elms = sorted(
|
||
|
[e for e in self._element.iter_ph_elms()], key=lambda e: e.ph_idx
|
||
|
)
|
||
|
return (SlideShapeFactory(e, self) for e in ph_elms)
|
||
|
|
||
|
def __len__(self):
|
||
|
"""
|
||
|
Return count of placeholder shapes.
|
||
|
"""
|
||
|
return len(list(self._element.iter_ph_elms()))
|
||
|
|
||
|
|
||
|
def BaseShapeFactory(shape_elm, parent):
|
||
|
"""
|
||
|
Return an instance of the appropriate shape proxy class for *shape_elm*.
|
||
|
"""
|
||
|
tag = shape_elm.tag
|
||
|
|
||
|
if tag == qn("p:pic"):
|
||
|
videoFiles = shape_elm.xpath("./p:nvPicPr/p:nvPr/a:videoFile")
|
||
|
if videoFiles:
|
||
|
return Movie(shape_elm, parent)
|
||
|
return Picture(shape_elm, parent)
|
||
|
|
||
|
shape_cls = {
|
||
|
qn("p:cxnSp"): Connector,
|
||
|
qn("p:grpSp"): GroupShape,
|
||
|
qn("p:sp"): Shape,
|
||
|
qn("p:graphicFrame"): GraphicFrame,
|
||
|
}.get(tag, BaseShape)
|
||
|
|
||
|
return shape_cls(shape_elm, parent)
|
||
|
|
||
|
|
||
|
def _LayoutShapeFactory(shape_elm, parent):
|
||
|
"""
|
||
|
Return an instance of the appropriate shape proxy class for *shape_elm*
|
||
|
on a slide layout.
|
||
|
"""
|
||
|
tag_name = shape_elm.tag
|
||
|
if tag_name == qn("p:sp") and shape_elm.has_ph_elm:
|
||
|
return LayoutPlaceholder(shape_elm, parent)
|
||
|
return BaseShapeFactory(shape_elm, parent)
|
||
|
|
||
|
|
||
|
def _MasterShapeFactory(shape_elm, parent):
|
||
|
"""
|
||
|
Return an instance of the appropriate shape proxy class for *shape_elm*
|
||
|
on a slide master.
|
||
|
"""
|
||
|
tag_name = shape_elm.tag
|
||
|
if tag_name == qn("p:sp") and shape_elm.has_ph_elm:
|
||
|
return MasterPlaceholder(shape_elm, parent)
|
||
|
return BaseShapeFactory(shape_elm, parent)
|
||
|
|
||
|
|
||
|
def _NotesSlideShapeFactory(shape_elm, parent):
|
||
|
"""
|
||
|
Return an instance of the appropriate shape proxy class for *shape_elm*
|
||
|
on a notes slide.
|
||
|
"""
|
||
|
tag_name = shape_elm.tag
|
||
|
if tag_name == qn("p:sp") and shape_elm.has_ph_elm:
|
||
|
return NotesSlidePlaceholder(shape_elm, parent)
|
||
|
return BaseShapeFactory(shape_elm, parent)
|
||
|
|
||
|
|
||
|
def _SlidePlaceholderFactory(shape_elm, parent):
|
||
|
"""
|
||
|
Return a placeholder shape of the appropriate type for *shape_elm*.
|
||
|
"""
|
||
|
tag = shape_elm.tag
|
||
|
if tag == qn("p:sp"):
|
||
|
Constructor = {
|
||
|
PP_PLACEHOLDER.BITMAP: PicturePlaceholder,
|
||
|
PP_PLACEHOLDER.CHART: ChartPlaceholder,
|
||
|
PP_PLACEHOLDER.PICTURE: PicturePlaceholder,
|
||
|
PP_PLACEHOLDER.TABLE: TablePlaceholder,
|
||
|
}.get(shape_elm.ph_type, SlidePlaceholder)
|
||
|
elif tag == qn("p:graphicFrame"):
|
||
|
Constructor = PlaceholderGraphicFrame
|
||
|
elif tag == qn("p:pic"):
|
||
|
Constructor = PlaceholderPicture
|
||
|
else:
|
||
|
Constructor = BaseShapeFactory
|
||
|
return Constructor(shape_elm, parent)
|
||
|
|
||
|
|
||
|
def SlideShapeFactory(shape_elm, parent):
|
||
|
"""
|
||
|
Return an instance of the appropriate shape proxy class for *shape_elm*
|
||
|
on a slide.
|
||
|
"""
|
||
|
if shape_elm.has_ph_elm:
|
||
|
return _SlidePlaceholderFactory(shape_elm, parent)
|
||
|
return BaseShapeFactory(shape_elm, parent)
|
||
|
|
||
|
|
||
|
class _MoviePicElementCreator(object):
|
||
|
"""Functional service object for creating a new movie p:pic element.
|
||
|
|
||
|
It's entire external interface is its :meth:`new_movie_pic` class method
|
||
|
that returns a new `p:pic` element containing the specified video. This
|
||
|
class is not intended to be constructed or an instance of it retained by
|
||
|
the caller; it is a "one-shot" object, really a function wrapped in
|
||
|
a object such that its helper methods can be organized here.
|
||
|
"""
|
||
|
|
||
|
def __init__(
|
||
|
self, shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_file, mime_type
|
||
|
):
|
||
|
super(_MoviePicElementCreator, self).__init__()
|
||
|
self._shapes = shapes
|
||
|
self._shape_id = shape_id
|
||
|
self._movie_file = movie_file
|
||
|
self._x, self._y, self._cx, self._cy = x, y, cx, cy
|
||
|
self._poster_frame_file = poster_frame_file
|
||
|
self._mime_type = mime_type
|
||
|
|
||
|
@classmethod
|
||
|
def new_movie_pic(
|
||
|
cls, shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_image, mime_type
|
||
|
):
|
||
|
"""Return a new `p:pic` element containing video in *movie_file*.
|
||
|
|
||
|
If *mime_type* is None, 'video/unknown' is used. If
|
||
|
*poster_frame_file* is None, the default "media loudspeaker" image is
|
||
|
used.
|
||
|
"""
|
||
|
return cls(
|
||
|
shapes, shape_id, movie_file, x, y, cx, cy, poster_frame_image, mime_type
|
||
|
)._pic
|
||
|
return
|
||
|
|
||
|
@property
|
||
|
def _media_rId(self):
|
||
|
"""Return the rId of RT.MEDIA relationship to video part.
|
||
|
|
||
|
For historical reasons, there are two relationships to the same part;
|
||
|
one is the video rId and the other is the media rId.
|
||
|
"""
|
||
|
return self._video_part_rIds[0]
|
||
|
|
||
|
@lazyproperty
|
||
|
def _pic(self):
|
||
|
"""Return the new `p:pic` element referencing the video."""
|
||
|
return CT_Picture.new_video_pic(
|
||
|
self._shape_id,
|
||
|
self._shape_name,
|
||
|
self._video_rId,
|
||
|
self._media_rId,
|
||
|
self._poster_frame_rId,
|
||
|
self._x,
|
||
|
self._y,
|
||
|
self._cx,
|
||
|
self._cy,
|
||
|
)
|
||
|
|
||
|
@lazyproperty
|
||
|
def _poster_frame_image_file(self):
|
||
|
"""Return the image file for video placeholder image.
|
||
|
|
||
|
If no poster frame file is provided, the default "media loudspeaker"
|
||
|
image is used.
|
||
|
"""
|
||
|
poster_frame_file = self._poster_frame_file
|
||
|
if poster_frame_file is None:
|
||
|
return BytesIO(SPEAKER_IMAGE_BYTES)
|
||
|
return poster_frame_file
|
||
|
|
||
|
@lazyproperty
|
||
|
def _poster_frame_rId(self):
|
||
|
"""Return the rId of relationship to poster frame image.
|
||
|
|
||
|
The poster frame is the image used to represent the video before it's
|
||
|
played.
|
||
|
"""
|
||
|
_, poster_frame_rId = self._slide_part.get_or_add_image_part(
|
||
|
self._poster_frame_image_file
|
||
|
)
|
||
|
return poster_frame_rId
|
||
|
|
||
|
@property
|
||
|
def _shape_name(self):
|
||
|
"""Return the appropriate shape name for the p:pic shape.
|
||
|
|
||
|
A movie shape is named with the base filename of the video.
|
||
|
"""
|
||
|
return self._video.filename
|
||
|
|
||
|
@property
|
||
|
def _slide_part(self):
|
||
|
"""Return SlidePart object for slide containing this movie."""
|
||
|
return self._shapes.part
|
||
|
|
||
|
@lazyproperty
|
||
|
def _video(self):
|
||
|
"""Return a |Video| object containing the movie file."""
|
||
|
return Video.from_path_or_file_like(self._movie_file, self._mime_type)
|
||
|
|
||
|
@lazyproperty
|
||
|
def _video_part_rIds(self):
|
||
|
"""Return the rIds for relationships to media part for video.
|
||
|
|
||
|
This is where the media part and its relationships to the slide are
|
||
|
actually created.
|
||
|
"""
|
||
|
media_rId, video_rId = self._slide_part.get_or_add_video_media_part(self._video)
|
||
|
return media_rId, video_rId
|
||
|
|
||
|
@property
|
||
|
def _video_rId(self):
|
||
|
"""Return the rId of RT.VIDEO relationship to video part.
|
||
|
|
||
|
For historical reasons, there are two relationships to the same part;
|
||
|
one is the video rId and the other is the media rId.
|
||
|
"""
|
||
|
return self._video_part_rIds[1]
|