""" This module contains the TOCHandler class which is responsible for handling the table of contents (TOC) in the PPTX presentation. It also handles content page numbers, as it is related to the TOC. """ from collections import deque from pptx.shapes.shapetree import GroupShape from ....utils import strings from ....utils.pptx import remove_shape class TOCHandler: """ Class responsible for handling the table of contents (TOC) in a PPTX presentation. The TOC is a slide that lists the titles of the content slides and their respective page numbers. For it to work, the table of contents slide has a number of pre-defined placeholders which are replaced with the actual content titles (or deleted if there are no more content titles to replace them with). This approach is better than creating a new shape for each content title, as it would involve greater complexity and would be harder to maintain. """ def __init__(self, source, toc_page: int = 2): """ Initialize the TOCHandler. Args: source: PPTX Presentation object. toc_page: 1-based page number of the table of contents slide. """ self.source = source self.toc_page = toc_page @property def toc_slide(self): """Get the Table of Contents slide of the source presentation.""" return self.source.slides[self.toc_page - 1] @property def content_slides(self) -> list: """Get the content slides of the source presentation.""" return list(self.source.slides)[self.toc_page :] def run(self) -> None: """Start the TOCHandler.""" self._dynamically_assign_page_numbers() self._update_toc_items() def _get_toc_item_shapes(self) -> list[list[GroupShape]]: """Get the shapes of the items in the Table of Contents slide. Each item should have exactly two shapes: the title and the number. """ toc_items = filter(lambda x: isinstance(x, GroupShape), self.toc_slide.shapes) toc_items_shapes = [list(s.shapes) for s in toc_items] # title, number return toc_items_shapes def _get_content_slide_titles(self) -> list[str]: """Get the titles of the content slides.""" titles = [ self._get_title_shape(slide.shapes).text for slide in self.content_slides ] return titles def _dynamically_assign_page_numbers(self): """Assign the page numbers to the Table of Contents slide.""" # Dynamically assign page numbers to each content slide. The placeholder # must already exist and must be the most lower-right shape in the slide. for page_num, slide in enumerate(self.content_slides, start=self.toc_page + 1): page_number_shape = self._get_page_number_shape(slide.shapes) page_number_shape.text_frame.paragraphs[0].text = str(page_num) def _update_toc_items(self): def update_shape_text(shape, new_text: str): text_frame = shape.text_frame if text_frame.paragraphs: # This requires that the TOC title text has a single run. text_frame.paragraphs[0].runs[0].text = new_text toc_items_shapes = self._get_toc_item_shapes() content_titles = deque(self._get_content_slide_titles()) for i, shape_group in enumerate(toc_items_shapes, start=self.toc_page + 1): if content_titles: title = content_titles.popleft() capitalized_title = strings.capitalize_title(title, ignore_words=["ID"]) update_shape_text(shape_group[0], capitalized_title) update_shape_text(shape_group[1], str(i)) else: # Remove excess TOC items for shape in shape_group: remove_shape(shape) @staticmethod def _filter_shapes_have_text_frame(shapes): return [shape for shape in shapes if shape.has_text_frame] def _get_title_shape(self, shapes): """Get the most top shape with text frame from a list of shapes, assuming that it is the title shape. """ have_text_frame = self._filter_shapes_have_text_frame(shapes) if not have_text_frame: raise ValueError("No title shape found in slide.") most_top = min(have_text_frame, key=lambda x: x.top) return most_top def _get_page_number_shape(self, shapes): """Get the most top shape with text frame from a list of shapes, assuming that it is the title shape. """ have_text_frame = self._filter_shapes_have_text_frame(shapes) if not have_text_frame: raise ValueError("No page number shape found in slide.") # Assuming A4 paper size, the most bottom-right shape is the page number. # This approach might not work for other paper sizes which are either # too long or too wide. most_bottom_right = max(have_text_frame, key=lambda x: x.top + x.left) return most_bottom_right