Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/pptx/oxml/slide.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,31 @@ class CT_SlideLayout(_BaseSlideElement):
cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
del _tag_seq

@classmethod
def new(cls) -> CT_SlideLayout:
"""Return new `p:sldLayout` element configured as base slide shape."""
return cast(CT_SlideLayout, parse_xml(cls._sld_xml()))

@staticmethod
def _sld_xml():
return (
"<p:sldLayout %s>\n"
" <p:cSld>\n"
" <p:spTree>\n"
" <p:nvGrpSpPr>\n"
' <p:cNvPr id="1" name=""/>\n'
" <p:cNvGrpSpPr/>\n"
" <p:nvPr/>\n"
" </p:nvGrpSpPr>\n"
" <p:grpSpPr/>\n"
" </p:spTree>\n"
" </p:cSld>\n"
" <p:clrMapOvr>\n"
" <a:masterClrMapping/>\n"
" </p:clrMapOvr>\n"
"</p:sldLayout>" % nsdecls("a", "r", "p")
)


class CT_SlideLayoutIdList(BaseOxmlElement):
"""`p:sldLayoutIdLst` element, child of `p:sldMaster`.
Expand All @@ -267,8 +292,43 @@ class CT_SlideLayoutIdList(BaseOxmlElement):

sldLayoutId_lst: list[CT_SlideLayoutIdListEntry]

_add_sldLayoutId: Callable[..., CT_SlideLayoutIdListEntry]
sldLayoutId = ZeroOrMore("p:sldLayoutId")

def add_sldLayoutId(self, rId: str) -> CT_SlideLayoutIdListEntry:
"""Create and return a reference to a new `p:sldLayoutId` child element.

The new `p:sldLayoutId` element has its r:id attribute set to `rId`.
"""
return self._add_sldLayoutId(id=self._next_id, rId=rId)

@property
def _next_id(self) -> int:
"""The next available layout ID as an `int`.

Valid layout IDs start at 256. The next integer value greater than the max value in use is
chosen, which minimizes that chance of reusing the id of a deleted slide.
"""
MIN_SLIDE_LAYOUT_ID = 256
MAX_SLIDE_LAYOUT_ID = 2147483647

used_ids = [int(s) for s in cast("list[str]", self.xpath("./p:sldLayoutId/@id"))]
simple_next = max([MIN_SLIDE_LAYOUT_ID - 1] + used_ids) + 1
if simple_next <= MAX_SLIDE_LAYOUT_ID:
return simple_next

# -- fall back to search for next unused from bottom --
valid_used_ids = sorted(id for id in used_ids if (MIN_SLIDE_LAYOUT_ID <= id <= MAX_SLIDE_LAYOUT_ID))
return (
next(
candidate_id
for candidate_id, used_id in enumerate(valid_used_ids, start=MIN_SLIDE_LAYOUT_ID)
if candidate_id != used_id
)
if valid_used_ids
else 256
)


class CT_SlideLayoutIdListEntry(BaseOxmlElement):
"""`p:sldLayoutId` element, child of `p:sldLayoutIdLst`.
Expand Down
28 changes: 27 additions & 1 deletion src/pptx/parts/slide.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from pptx.opc.constants import RELATIONSHIP_TYPE as RT
from pptx.opc.package import XmlPart
from pptx.opc.packuri import PackURI
from pptx.oxml.slide import CT_NotesMaster, CT_NotesSlide, CT_Slide
from pptx.oxml.slide import CT_NotesMaster, CT_NotesSlide, CT_Slide, CT_SlideLayout
from pptx.oxml.theme import CT_OfficeStyleSheet
from pptx.parts.chart import ChartPart
from pptx.parts.embeddedpackage import EmbeddedPackagePart
Expand Down Expand Up @@ -266,6 +266,15 @@ class SlideLayoutPart(BaseSlidePart):
Corresponds to package files ``ppt/slideLayouts/slideLayout[1-9][0-9]*.xml``.
"""

@classmethod
def new(cls, partname, package):
"""Return newly-created blank slide layout part.

The new slide-part has `partname` and a relationship to `slide_layout_part`.
"""
slide_layout_part = cls(partname, CT.PML_SLIDE_LAYOUT, package, CT_SlideLayout.new())
return slide_layout_part

@lazyproperty
def slide_layout(self):
"""
Expand All @@ -285,6 +294,16 @@ class SlideMasterPart(BaseSlidePart):
Corresponds to package files ppt/slideMasters/slideMaster[1-9][0-9]*.xml.
"""

def add_layout(self):
"""
Return (rId, layout) pair of a newly created layout.
"""
partname = self._next_slideLayout_partname
slide_layout_part = SlideLayoutPart.new(partname, self.package)
rId = self.relate_to(slide_layout_part, RT.SLIDE_LAYOUT)
slide_layout_part.relate_to(self.slide_master.part, RT.SLIDE_MASTER)
return rId, slide_layout_part.slide_layout

def related_slide_layout(self, rId: str) -> SlideLayout:
"""Return |SlideLayout| related to this slide-master by key `rId`."""
return self.related_part(rId).slide_layout
Expand All @@ -295,3 +314,10 @@ def slide_master(self):
The |SlideMaster| object representing this part.
"""
return SlideMaster(self._element, self)

@property
def _next_slideLayout_partname(self):
"""Return |PackURI| instance containing next available slideLayout partname."""
sldLayoutIdLst = self._element.get_or_add_sldLayoutIdLst()
partname_str = "/ppt/slideLayouts/slideLayout%d.xml" % (len(sldLayoutIdLst) + 1)
return PackURI(partname_str)
16 changes: 15 additions & 1 deletion src/pptx/shapes/shapetree.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,13 +648,27 @@ def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
return SlideShapeFactory(shape_elm, self)


class LayoutShapes(_BaseShapes):
class LayoutShapes(_BaseGroupShapes):
"""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 add_placeholder(
self, ph_type: PP_PLACEHOLDER, orient: str, sz: str
) -> LayoutPlaceholder:
"""Return newly added placeholder appended to this shape tree.

The placeholder having the specified properties
"""
id_ = self._next_shape_id
ph_name = self._next_ph_name(ph_type, id_, orient)
sp = self._spTree.add_placeholder(id_, ph_name, ph_type, orient, sz, id_)
sp = cast(LayoutPlaceholder, self._shape_factory(sp))

return sp

def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
"""Return an instance of the appropriate shape proxy class for `shape_elm`."""
return _LayoutShapeFactory(shape_elm, self)
Expand Down
9 changes: 9 additions & 0 deletions src/pptx/slide.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,15 @@ def __len__(self) -> int:
"""Support len() built-in function, e.g. `len(slides) == 4`."""
return len(self._sldLayoutIdLst)

def add_layout(self, name: str | None = "Layout %s") -> SlideLayout:
"""Return a newly added slide layout."""
rId, layout = self.part.add_layout()
self._sldLayoutIdLst.add_sldLayoutId(rId)
id_ = int(rId[3:]) if rId.startswith("rId") and rId[3:].isdigit() else 0
if name:
layout.name = name % id_ if "%s" in name else name
return layout

def get_by_name(self, name: str, default: SlideLayout | None = None) -> SlideLayout | None:
"""Return SlideLayout object having `name`, or `default` if not found."""
for slide_layout in self:
Expand Down