Skip to content

Commit dfe9905

Browse files
authored
Merge pull request #49 from MHoroszowski/feature/headers-footers-phase2
feat: Slide / Master / Layout headers-footers public API (Phase 2)
2 parents 4daba7e + 5a3d837 commit dfe9905

2 files changed

Lines changed: 662 additions & 5 deletions

File tree

src/pptx/slide.py

Lines changed: 186 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,17 @@
2424
from pptx.oxml.presentation import CT_SlideIdList, CT_SlideMasterIdList
2525
from pptx.oxml.slide import (
2626
CT_CommonSlideData,
27+
CT_NotesMaster,
2728
CT_NotesSlide,
2829
CT_Slide,
30+
CT_SlideLayout,
2931
CT_SlideLayoutIdList,
3032
CT_SlideMaster,
3133
)
3234
from pptx.parts.presentation import PresentationPart
3335
from pptx.parts.slide import SlideLayoutPart, SlideMasterPart, SlidePart
3436
from pptx.presentation import Presentation
35-
from pptx.shapes.placeholder import LayoutPlaceholder, MasterPlaceholder
37+
from pptx.shapes.placeholder import LayoutPlaceholder, MasterPlaceholder, SlidePlaceholder
3638
from pptx.shapes.shapetree import NotesSlidePlaceholder
3739
from pptx.text.text import TextFrame
3840

@@ -92,7 +94,84 @@ def shapes(self):
9294
return MasterShapes(self._element.spTree, self)
9395

9496

95-
class NotesMaster(_BaseMaster):
97+
class _HeaderFooterVisibility:
98+
"""Provides access to header/footer visibility settings on a slide template."""
99+
100+
_element: CT_SlideLayout | CT_SlideMaster | CT_NotesMaster
101+
102+
def _get_hf_visibility(self, attr_name: str) -> bool:
103+
"""Return effective `attr_name` value, defaulting to |True| when `<p:hf>` is absent."""
104+
hf = self._element.hf
105+
return True if hf is None else getattr(hf, attr_name)
106+
107+
def _set_hf_visibility(self, attr_name: str, value: bool) -> None:
108+
"""Set `attr_name` on `<p:hf>`, creating the element only when needed.
109+
110+
Assigning |True| when `<p:hf>` is absent is a no-op because the effective default is
111+
already |True|. An existing `<p:hf>` element is retained even when all values become
112+
|True|, avoiding low-value XML churn.
113+
"""
114+
hf = self._element.hf
115+
if hf is None and value:
116+
return
117+
if hf is None:
118+
hf = self._element.get_or_add_hf()
119+
setattr(hf, attr_name, value)
120+
121+
@property
122+
def show_slide_number(self) -> bool:
123+
"""`True` when slide numbers are shown for this template, `False` otherwise.
124+
125+
Assigning |False| creates a `<p:hf>` element when needed and writes `sldNum="0"`.
126+
Assigning |True| preserves any existing `<p:hf>` element rather than removing it.
127+
"""
128+
return self._get_hf_visibility("sldNum")
129+
130+
@show_slide_number.setter
131+
def show_slide_number(self, value: bool) -> None:
132+
self._set_hf_visibility("sldNum", value)
133+
134+
@property
135+
def show_footer(self) -> bool:
136+
"""`True` when footer placeholders are shown for this template, `False` otherwise.
137+
138+
Assigning |False| creates a `<p:hf>` element when needed and writes `ftr="0"`.
139+
Assigning |True| preserves any existing `<p:hf>` element rather than removing it.
140+
"""
141+
return self._get_hf_visibility("ftr")
142+
143+
@show_footer.setter
144+
def show_footer(self, value: bool) -> None:
145+
self._set_hf_visibility("ftr", value)
146+
147+
@property
148+
def show_date(self) -> bool:
149+
"""`True` when date placeholders are shown for this template, `False` otherwise.
150+
151+
Assigning |False| creates a `<p:hf>` element when needed and writes `dt="0"`.
152+
Assigning |True| preserves any existing `<p:hf>` element rather than removing it.
153+
"""
154+
return self._get_hf_visibility("dt")
155+
156+
@show_date.setter
157+
def show_date(self, value: bool) -> None:
158+
self._set_hf_visibility("dt", value)
159+
160+
@property
161+
def show_header(self) -> bool:
162+
"""`True` when header placeholders are shown for this template, `False` otherwise.
163+
164+
Assigning |False| creates a `<p:hf>` element when needed and writes `hdr="0"`.
165+
Assigning |True| preserves any existing `<p:hf>` element rather than removing it.
166+
"""
167+
return self._get_hf_visibility("hdr")
168+
169+
@show_header.setter
170+
def show_header(self, value: bool) -> None:
171+
self._set_hf_visibility("hdr", value)
172+
173+
174+
class NotesMaster(_HeaderFooterVisibility, _BaseMaster):
96175
"""Proxy for the notes master XML document.
97176
98177
Provides access to shapes, the most commonly used of which are placeholders.
@@ -214,6 +293,33 @@ def has_notes_slide(self) -> bool:
214293
"""
215294
return self.part.has_notes_slide
216295

296+
@property
297+
def has_date(self) -> bool:
298+
"""`True` if this slide has a date placeholder, `False` otherwise.
299+
300+
This property is non-mutating; it reports only whether a DATE placeholder is already
301+
present on the slide.
302+
"""
303+
return self._first_ph_of_type(PP_PLACEHOLDER.DATE) is not None
304+
305+
@property
306+
def has_footer(self) -> bool:
307+
"""`True` if this slide has a footer placeholder, `False` otherwise.
308+
309+
This property is non-mutating; it reports only whether a FOOTER placeholder is already
310+
present on the slide.
311+
"""
312+
return self._first_ph_of_type(PP_PLACEHOLDER.FOOTER) is not None
313+
314+
@property
315+
def has_slide_number(self) -> bool:
316+
"""`True` if this slide has a slide-number placeholder, `False` otherwise.
317+
318+
This property is non-mutating; it reports only whether a SLIDE_NUMBER placeholder is
319+
already present on the slide.
320+
"""
321+
return self._first_ph_of_type(PP_PLACEHOLDER.SLIDE_NUMBER) is not None
322+
217323
@property
218324
def notes_slide(self) -> NotesSlide:
219325
"""The |NotesSlide| instance for this slide.
@@ -223,6 +329,60 @@ def notes_slide(self) -> NotesSlide:
223329
"""
224330
return self.part.notes_slide
225331

332+
@property
333+
def date_text(self) -> str | None:
334+
"""Text of this slide's date placeholder, or |None| when no date placeholder is present.
335+
336+
Reading this property does not create a placeholder. An existing empty DATE placeholder
337+
returns an empty string.
338+
"""
339+
placeholder = self._first_ph_of_type(PP_PLACEHOLDER.DATE)
340+
return None if placeholder is None else placeholder.text_frame.text
341+
342+
@date_text.setter
343+
def date_text(self, value: str | None) -> None:
344+
placeholder = self._first_ph_of_type(PP_PLACEHOLDER.DATE)
345+
if value in (None, ""):
346+
if placeholder is not None:
347+
placeholder.text_frame.text = ""
348+
return
349+
350+
if placeholder is None:
351+
layout_ph = self._layout_ph_of_type(PP_PLACEHOLDER.DATE)
352+
if layout_ph is None:
353+
raise ValueError("slide layout has no DATE placeholder to clone from")
354+
self.shapes.clone_placeholder(layout_ph)
355+
placeholder = cast("SlidePlaceholder", self._first_ph_of_type(PP_PLACEHOLDER.DATE))
356+
357+
placeholder.text_frame.text = value
358+
359+
@property
360+
def footer(self) -> str | None:
361+
"""Text of this slide's footer placeholder, or |None| when no footer placeholder is present.
362+
363+
Reading this property does not create a placeholder. An existing empty FOOTER placeholder
364+
returns an empty string.
365+
"""
366+
placeholder = self._first_ph_of_type(PP_PLACEHOLDER.FOOTER)
367+
return None if placeholder is None else placeholder.text_frame.text
368+
369+
@footer.setter
370+
def footer(self, value: str | None) -> None:
371+
placeholder = self._first_ph_of_type(PP_PLACEHOLDER.FOOTER)
372+
if value in (None, ""):
373+
if placeholder is not None:
374+
placeholder.text_frame.text = ""
375+
return
376+
377+
if placeholder is None:
378+
layout_ph = self._layout_ph_of_type(PP_PLACEHOLDER.FOOTER)
379+
if layout_ph is None:
380+
raise ValueError("slide layout has no FOOTER placeholder to clone from")
381+
self.shapes.clone_placeholder(layout_ph)
382+
placeholder = cast("SlidePlaceholder", self._first_ph_of_type(PP_PLACEHOLDER.FOOTER))
383+
384+
placeholder.text_frame.text = value
385+
226386
@lazyproperty
227387
def placeholders(self) -> SlidePlaceholders:
228388
"""Sequence of placeholder shapes in this slide."""
@@ -247,6 +407,28 @@ def slide_layout(self) -> SlideLayout:
247407
"""|SlideLayout| object this slide inherits appearance from."""
248408
return self.part.slide_layout
249409

410+
def _first_ph_of_type(self, ph_type: PP_PLACEHOLDER) -> SlidePlaceholder | None:
411+
"""Return the first SlidePlaceholder of `ph_type` in document order, or |None|.
412+
413+
This helper is non-mutating and returns the first matching slide placeholder when multiple
414+
placeholders of the same type are present.
415+
"""
416+
for placeholder in self.placeholders:
417+
if placeholder.placeholder_format.type == ph_type:
418+
return placeholder
419+
return None
420+
421+
def _layout_ph_of_type(self, ph_type: PP_PLACEHOLDER) -> LayoutPlaceholder | None:
422+
"""Return the first LayoutPlaceholder of `ph_type` on this slide's layout, or |None|.
423+
424+
The layout placeholder is used as the source when promoting a latent placeholder to a
425+
slide-level placeholder on first write.
426+
"""
427+
for placeholder in self.slide_layout.placeholders:
428+
if placeholder.placeholder_format.type == ph_type:
429+
return placeholder
430+
return None
431+
250432
def delete(self) -> None:
251433
"""Remove this slide from its presentation.
252434
@@ -426,7 +608,7 @@ def duplicate(self, slide: Slide, index: int | None = None) -> Slide:
426608
return new_slide_part.slide
427609

428610

429-
class SlideLayout(_BaseSlide):
611+
class SlideLayout(_HeaderFooterVisibility, _BaseSlide):
430612
"""Slide layout object.
431613
432614
Provides access to placeholders, regular shapes, and slide layout-level properties.
@@ -544,7 +726,7 @@ def remove(self, slide_layout: SlideLayout) -> None:
544726
slide_layout.slide_master.part.drop_rel(target_sldLayoutId.rId)
545727

546728

547-
class SlideMaster(_BaseMaster):
729+
class SlideMaster(_HeaderFooterVisibility, _BaseMaster):
548730
"""Slide master object.
549731
550732
Provides access to slide layouts. Access to placeholders, regular shapes, and slide master-level

0 commit comments

Comments
 (0)