forked from diegoecab/oci-deal-accelerator
initial version
This commit is contained in:
278
tools/oci_pptx_base.py
Normal file
278
tools/oci_pptx_base.py
Normal file
@@ -0,0 +1,278 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user