diff --git a/src/pptx/oxml/slide.py b/src/pptx/oxml/slide.py index 37a9780f6..287ac9e5b 100644 --- a/src/pptx/oxml/slide.py +++ b/src/pptx/oxml/slide.py @@ -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 ( + "\n" + " \n" + " \n" + " \n" + ' \n' + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "" % nsdecls("a", "r", "p") + ) + class CT_SlideLayoutIdList(BaseOxmlElement): """`p:sldLayoutIdLst` element, child of `p:sldMaster`. @@ -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`. diff --git a/src/pptx/parts/slide.py b/src/pptx/parts/slide.py index 6650564a5..3e4f7464d 100644 --- a/src/pptx/parts/slide.py +++ b/src/pptx/parts/slide.py @@ -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 @@ -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): """ @@ -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 @@ -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) diff --git a/src/pptx/shapes/shapetree.py b/src/pptx/shapes/shapetree.py index 29623f1f5..579861ca0 100644 --- a/src/pptx/shapes/shapetree.py +++ b/src/pptx/shapes/shapetree.py @@ -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) diff --git a/src/pptx/slide.py b/src/pptx/slide.py index 3b1b65d8e..2abb0eb73 100644 --- a/src/pptx/slide.py +++ b/src/pptx/slide.py @@ -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: