#!/usr/bin/env python3 """ OCI Deal Accelerator — Shared PPTX base (Oracle FY26 template + Redwood design system). Import: from oci_pptx_base import Colors, Layouts, OraclePresBase """ from pathlib import Path from typing import Optional from pptx import Presentation from pptx.util import Inches, Pt from pptx.dml.color import RGBColor from pptx.enum.text import PP_ALIGN, MSO_ANCHOR from pptx.enum.shapes import MSO_SHAPE # ============================================================ # Oracle Redwood Design System Colors # Source: Oracle Database Icon Library [July 2024] — theme RMIL_Master # Color scheme: "Oracle Redwood" # ============================================================ class Colors: # Primary text — never pure black DARK_BG = RGBColor(0x31, 0x2D, 0x2A) # #312D2A — dk1 PRIMARY_TEXT = RGBColor(0x31, 0x2D, 0x2A) # #312D2A — dk1 SECONDARY_TEXT = RGBColor(0x6F, 0x69, 0x64) # #6F6964 — Neutral 60 WHITE = RGBColor(0xFF, 0xFF, 0xFF) WARM_BG = RGBColor(0xFC, 0xFB, 0xFA) # #FCFBFA — lt1, content bg # Oracle Redwood accent palette ORACLE_RED = RGBColor(0xC7, 0x46, 0x34) # #C74634 — accent1, primary brand GOLD = RGBColor(0xFA, 0xCD, 0x62) # #FACD62 — accent2 TEAL = RGBColor(0x2C, 0x59, 0x67) # #2C5967 — hlink, tables/links SAGE = RGBColor(0x75, 0x9C, 0x6C) # #759C6C — accent6 FOREST = RGBColor(0x2B, 0x62, 0x42) # #2B6242 — accent4 MUTED_TEAL = RGBColor(0x94, 0xAF, 0xAF) # #94AFAF — accent3 BURNT_ORANGE = RGBColor(0xAE, 0x56, 0x2C) # #AE562C — accent5 # OCI service category colors (used in architecture diagrams) COPPER = RGBColor(0xAA, 0x64, 0x3B) # #AA643B — database PURPLE = RGBColor(0x80, 0x49, 0x98) # #804998 — integration # Status SUCCESS = RGBColor(0x2B, 0x62, 0x42) # #2B6242 — forest green WARNING = RGBColor(0xAE, 0x56, 0x2C) # #AE562C — burnt orange ERROR = RGBColor(0xC7, 0x46, 0x34) # #C74634 — Oracle red # Table TABLE_ALT_ROW = RGBColor(0xF5, 0xF4, 0xF2) # #F5F4F2 — Neutral 10 TABLE_HEADER_BG = RGBColor(0x2C, 0x59, 0x67) # teal # ============================================================ # Oracle FY26 Template Layout Indices # ============================================================ class Layouts: """Layout indices from Oracle_PPT-template_FY26.pptx (104 layouts).""" COVER_DARK = 0 # Dark - Title_Pillar COVER_LIGHT = 1 # Light - Title_Pillar DIVIDER_LIGHT = 16 # Light - Divider DIVIDER_DARK = 17 # Dark - Divider IMPACT_DARK = 26 # Dark - Impact Statement IMPACT_LIGHT = 27 # Light - Impact Statement METRIC_DARK = 32 # Dark - Single metric METRIC_LIGHT = 33 # Light - Single metric MULTI_STATEMENT_DARK = 47 # Multi Statement – Dark MULTI_STATEMENT_LIGHT = 48 # Multi Statement – Light TWO_COL_LIGHT = 66 # Light - Title/Subtitle 2 Column TWO_COL_DARK = 67 # Dark - Title/Subtitle 2 Column FOUR_ICONS_LIGHT = 82 # Light - 4 Icons, Subhead and text BLANK_DARK = 84 # Dark - Blank BLANK_LIGHT = 85 # Light - Blank SAFE_HARBOR_LIGHT = 96 # Light - Safe harbor - short (if exists) THANK_YOU_DARK = 34 # Dark - Thank You THANK_YOU_LIGHT = 35 # Light - Thank You CLOSING_LIGHT = 101 # Light - Closing Slide (Oracle logo only) CLOSING_DARK = 102 # Dark - Closing Slide (Oracle logo only) # ============================================================ # Base Presentation Class # ============================================================ class OraclePresBase: """Shared PPTX infrastructure for OCI Deal Accelerator generators.""" TEMPLATE_PATH = Path(__file__).parent.parent / "templates" / "Oracle_PPT-template_FY26.pptx" FONT = "Oracle Sans" FONT_HEADING = "Georgia" TITLE_ACCENT_COLOR = Colors.TEAL # subclasses can override as class attribute def __init__(self, template: Optional[str] = None): tmpl = template or str(self.TEMPLATE_PATH) if not Path(tmpl).is_file(): raise FileNotFoundError(f"Template not found: {tmpl}") self.prs = Presentation(tmpl) self._clear_slides() def _clear_slides(self): """Remove all existing slides from the template (keep layouts/masters).""" while len(self.prs.slides) > 0: rId = self.prs.slides._sldIdLst[0].get( '{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id' ) self.prs.part.drop_rel(rId) self.prs.slides._sldIdLst.remove(self.prs.slides._sldIdLst[0]) # Clean sections from lxml import etree # noqa: F401 ns_p14 = 'http://schemas.microsoft.com/office/powerpoint/2010/main' for sectionLst in self.prs.part._element.findall(f'.//{{{ns_p14}}}sectionLst'): sectionLst.getparent().remove(sectionLst) def _add_layout_slide(self, layout_index: int): """Add a slide using a specific template layout.""" layout = self.prs.slide_layouts[layout_index] return self.prs.slides.add_slide(layout) def _add_blank_slide(self, dark: bool = False): """Add a blank slide (light or dark).""" idx = Layouts.BLANK_DARK if dark else Layouts.BLANK_LIGHT return self._add_layout_slide(idx) def _set_placeholder(self, slide, idx: int, text: str): """Set text in a placeholder by index. Silently skips if not found.""" for ph in slide.placeholders: if ph.placeholder_format.idx == idx: ph.text = text return ph return None def _style_placeholder(self, slide, idx: int, text: str, font_size: int = None, bold: bool = None, color: RGBColor = None): """Set text and style a placeholder.""" ph = self._set_placeholder(slide, idx, text) if ph and ph.text_frame.paragraphs: for p in ph.text_frame.paragraphs: if font_size: p.font.size = Pt(font_size) if bold is not None: p.font.bold = bold if color: p.font.color.rgb = color return ph def _add_textbox(self, slide, left, top, width, height, text="", font_size=12, bold=False, italic=False, color=None, alignment=PP_ALIGN.LEFT, font_name=None): """Add a text box to a slide.""" txBox = slide.shapes.add_textbox(left, top, width, height) tf = txBox.text_frame tf.word_wrap = True p = tf.paragraphs[0] p.text = text p.font.size = Pt(font_size) p.font.bold = bold p.font.italic = italic p.font.name = font_name or self.FONT p.font.color.rgb = color or Colors.PRIMARY_TEXT p.alignment = alignment return txBox def _add_paragraph(self, text_frame, text="", font_size=12, bold=False, italic=False, color=None, alignment=PP_ALIGN.LEFT, space_before=0, space_after=0, bullet=False): """Add a paragraph to an existing text frame.""" p = text_frame.add_paragraph() p.text = text p.font.size = Pt(font_size) p.font.bold = bold p.font.italic = italic p.font.name = self.FONT p.font.color.rgb = color or Colors.PRIMARY_TEXT p.alignment = alignment if space_before: p.space_before = Pt(space_before) if space_after: p.space_after = Pt(space_after) if bullet: p.level = 0 return p def _add_rect(self, slide, left, top, width, height, fill_color=None, border_color=None): """Add a rectangle shape.""" shape = slide.shapes.add_shape( MSO_SHAPE.RECTANGLE, left, top, width, height ) if fill_color: shape.fill.solid() shape.fill.fore_color.rgb = fill_color else: shape.fill.background() if border_color: shape.line.color.rgb = border_color else: shape.line.fill.background() return shape def _add_table(self, slide, rows, cols, left, top, width, height): """Add a table to a slide.""" return slide.shapes.add_table(rows, cols, left, top, width, height).table def _style_table_cell(self, cell, text, font_size=10, bold=False, color=None, bg_color=None, alignment=PP_ALIGN.LEFT): """Style a table cell.""" cell.text = text for paragraph in cell.text_frame.paragraphs: paragraph.font.size = Pt(font_size) paragraph.font.bold = bold paragraph.font.name = self.FONT paragraph.font.color.rgb = color or Colors.PRIMARY_TEXT paragraph.alignment = alignment cell.vertical_anchor = MSO_ANCHOR.MIDDLE if bg_color: cell.fill.solid() cell.fill.fore_color.rgb = bg_color def _add_title_bar(self, slide, title_text, margin=None): """Add a consistent title bar at top of content slides. Uses self.TITLE_ACCENT_COLOR for the accent line. Title is 24pt bold Georgia. margin defaults to Inches(0.8). """ if margin is None: margin = Inches(0.8) self._add_textbox( slide, margin, Inches(0.4), Inches(11.7), Inches(0.6), text=title_text, font_size=24, bold=True, font_name=self.FONT_HEADING, ) bar = slide.shapes.add_shape( MSO_SHAPE.RECTANGLE, margin, Inches(1.05), Inches(11.7), Inches(0.05) ) bar.fill.solid() bar.fill.fore_color.rgb = self.TITLE_ACCENT_COLOR bar.line.fill.background() def add_closing_slide(self, name: str = "", title: str = "", contact: str = "", dark: bool = True): """Closing slide: Thank You (dark by default) + Oracle logo closing. Uses the FY26 Thank You layout (PH0 = "Thank you", PH37 = name/title, PH36 = contact/URL), followed by the Oracle Closing Slide layout (logo-only, no placeholders). Args: name: Presenter name, e.g. "Diego Cabrera" title: Presenter title/firm, e.g. "Solutions Architect | Oracle" contact: Contact line, e.g. email or URL dark: Use dark variant (default True) """ layout_idx = Layouts.THANK_YOU_DARK if dark else Layouts.THANK_YOU_LIGHT slide = self._add_layout_slide(layout_idx) # PH0 defaults to "Thank you" from the template — clear if not wanted # PH37: name + title on one line if name or title: parts = [p for p in [name, title] if p] self._set_placeholder(slide, 37, " | ".join(parts)) # PH36: contact / URL if contact: self._set_placeholder(slide, 36, contact) # Oracle Closing Slide (logo-only) closing_idx = Layouts.CLOSING_DARK if dark else Layouts.CLOSING_LIGHT self._add_layout_slide(closing_idx) def save(self, filepath: str): """Save the presentation to a .pptx file.""" self.prs.save(filepath) @property def slide_count(self): return len(self.prs.slides)