200 lines
7.0 KiB
Python
200 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Update NORDA presentation: remove old diagram slides, add 10 new diagram slides.
|
|
Each slide has dark background matching presentation style, large fonts, and diagram image.
|
|
"""
|
|
|
|
from pptx import Presentation
|
|
from pptx.util import Inches, Pt, Emu
|
|
from pptx.dml.color import RGBColor
|
|
from pptx.enum.text import PP_ALIGN
|
|
import copy
|
|
import os
|
|
|
|
PPTX_PATH = '/Users/maciejpi/Documents/INPI-WEWNETRZNE/NORDA-Business/NORDA_Prezentacja_Rada_2026-02-13.pptx'
|
|
DIAGRAMS_DIR = '/Users/maciejpi/claude/projects/active/nordabiz/docs/architecture/diagrams'
|
|
|
|
# Colors matching presentation style
|
|
BG_DARK = RGBColor(0x0F, 0x17, 0x2A) # #0F172A - dark background
|
|
HEADER_NAVY = RGBColor(0x1A, 0x36, 0x5D) # #1A365D - navy header
|
|
GOLD = RGBColor(0xD6, 0x9E, 0x2E) # #D69E2E - gold accent
|
|
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
|
|
GRAY = RGBColor(0xA0, 0xAE, 0xC0) # #A0AEC0 - subtitle gray
|
|
|
|
# 10 diagrams: (filename, title, subtitle)
|
|
DIAGRAMS = [
|
|
('a9-detailed-it-architecture.png', 'Szczegółowa architektura IT', 'Azure AD · M365 · SSL · FortiGate · Proxmox · Aplikacje · Systemy wspierające'),
|
|
('b1-platform-overview.png', 'Architektura platformy NordaBiznes', 'Przegląd modułów, użytkowników i integracji'),
|
|
('a3-network-topology.png', 'Infrastruktura sieciowa INPI', 'Topologia sieci, firewalle, serwery, backup'),
|
|
('b3-feature-map.png', 'Mapa funkcjonalności platformy', 'Wszystkie moduły i funkcje dla firm członkowskich'),
|
|
('b5-ai-capabilities.png', 'Sztuczna inteligencja — NordaGPT', 'Asystent AI, audyty SEO, analiza danych'),
|
|
('a5-auth-oauth.png', 'Bezpieczeństwo i uwierzytelnianie', 'RBAC · OAuth 2.0 · CSRF · Rate Limiting'),
|
|
('a6-api-integration-map.png', 'Integracje z zewnętrznymi API', 'Google · Azure · PageSpeed · YouTube · CrUX'),
|
|
('a8-backup-dr.png', 'Backup i Disaster Recovery', 'Proxmox PBS · Hetzner · RTO 2h · RPO 24h'),
|
|
('b8-access-levels.png', 'Matryca uprawnień — pakiety', 'Starter · Premium · Business Pro · Admin'),
|
|
('a7-module-structure.png', 'Architektura modułowa aplikacji', '17 blueprintów · 49 plików routów · Flask'),
|
|
]
|
|
|
|
# Slide dimensions in EMU
|
|
SLIDE_W = 9144000 # 10 inches
|
|
SLIDE_H = 5143500 # ~5.63 inches
|
|
|
|
|
|
def add_dark_background(slide, prs):
|
|
"""Add dark background to slide."""
|
|
from pptx.oxml.ns import qn
|
|
bg = slide.background
|
|
fill = bg.fill
|
|
fill.solid()
|
|
fill.fore_color.rgb = BG_DARK
|
|
|
|
|
|
def add_header_bar(slide, title_text, subtitle_text):
|
|
"""Add navy header bar with title and subtitle."""
|
|
from pptx.oxml.ns import qn
|
|
|
|
# Header background rectangle
|
|
header_h = Emu(720000) # ~0.79 inches
|
|
header = slide.shapes.add_shape(
|
|
1, # MSO_SHAPE.RECTANGLE
|
|
Emu(0), Emu(0), Emu(SLIDE_W), header_h
|
|
)
|
|
header.fill.solid()
|
|
header.fill.fore_color.rgb = HEADER_NAVY
|
|
header.line.fill.background()
|
|
|
|
# Title text
|
|
title_box = slide.shapes.add_textbox(
|
|
Emu(300000), Emu(80000), Emu(SLIDE_W - 600000), Emu(380000)
|
|
)
|
|
tf = title_box.text_frame
|
|
tf.word_wrap = True
|
|
p = tf.paragraphs[0]
|
|
p.text = title_text
|
|
p.font.size = Pt(24)
|
|
p.font.color.rgb = WHITE
|
|
p.font.bold = True
|
|
p.alignment = PP_ALIGN.LEFT
|
|
|
|
# Subtitle text
|
|
sub_box = slide.shapes.add_textbox(
|
|
Emu(300000), Emu(440000), Emu(SLIDE_W - 600000), Emu(250000)
|
|
)
|
|
tf2 = sub_box.text_frame
|
|
tf2.word_wrap = True
|
|
p2 = tf2.paragraphs[0]
|
|
p2.text = subtitle_text
|
|
p2.font.size = Pt(14)
|
|
p2.font.color.rgb = GRAY
|
|
p2.alignment = PP_ALIGN.LEFT
|
|
|
|
# Gold accent line under header
|
|
line = slide.shapes.add_shape(
|
|
1, # RECTANGLE
|
|
Emu(0), header_h, Emu(SLIDE_W), Emu(25000)
|
|
)
|
|
line.fill.solid()
|
|
line.fill.fore_color.rgb = GOLD
|
|
line.line.fill.background()
|
|
|
|
|
|
def add_diagram_image(slide, img_path):
|
|
"""Add diagram image centered below header, filling available space."""
|
|
from PIL import Image
|
|
|
|
# Available space below header + accent line
|
|
top_margin = Emu(800000) # below header
|
|
bottom_margin = Emu(100000) # small bottom margin
|
|
side_margin = Emu(150000) # small side margins
|
|
|
|
avail_w = SLIDE_W - 2 * side_margin
|
|
avail_h = SLIDE_H - top_margin - bottom_margin
|
|
|
|
# Get image dimensions
|
|
with Image.open(img_path) as img:
|
|
img_w, img_h = img.size
|
|
|
|
# Scale to fit available space
|
|
scale_w = avail_w / img_w
|
|
scale_h = avail_h / img_h
|
|
scale = min(scale_w, scale_h)
|
|
|
|
final_w = int(img_w * scale)
|
|
final_h = int(img_h * scale)
|
|
|
|
# Center horizontally
|
|
left = side_margin + (avail_w - final_w) // 2
|
|
top = top_margin + (avail_h - final_h) // 2
|
|
|
|
slide.shapes.add_picture(img_path, Emu(left), Emu(top), Emu(final_w), Emu(final_h))
|
|
|
|
|
|
def add_slide_number(slide, num, total):
|
|
"""Add slide number in bottom right."""
|
|
num_box = slide.shapes.add_textbox(
|
|
Emu(SLIDE_W - 1200000), Emu(SLIDE_H - 300000), Emu(1000000), Emu(200000)
|
|
)
|
|
tf = num_box.text_frame
|
|
p = tf.paragraphs[0]
|
|
p.text = f'Załącznik {num}/{total}'
|
|
p.font.size = Pt(10)
|
|
p.font.color.rgb = GRAY
|
|
p.alignment = PP_ALIGN.RIGHT
|
|
|
|
|
|
def main():
|
|
prs = Presentation(PPTX_PATH)
|
|
|
|
# Step 1: Remove slides 16, 17, 18 (indices 15, 16, 17) — old diagram appendices
|
|
# Remove from highest index first to avoid shifting
|
|
slides_to_remove = []
|
|
for i, slide in enumerate(prs.slides):
|
|
if i >= 15: # slides 16, 17, 18 (0-indexed: 15, 16, 17)
|
|
slides_to_remove.append(slide)
|
|
|
|
# Access XML directly to remove slides
|
|
pres_xml = prs._element
|
|
ns_r = '{http://schemas.openxmlformats.org/officeDocument/2006/relationships}'
|
|
ns_p = '{http://schemas.openxmlformats.org/presentationml/2006/main}'
|
|
sldIdLst = pres_xml.find(f'{ns_p}sldIdLst')
|
|
|
|
# Get rIds for slides 16,17,18 (indices 15,16,17)
|
|
sldId_elements = list(sldIdLst)
|
|
for idx in reversed([15, 16, 17]):
|
|
sldId_elem = sldId_elements[idx]
|
|
rId = sldId_elem.get(f'{ns_r}id')
|
|
print(f' Removing slide {idx+1} (rId={rId})')
|
|
sldIdLst.remove(sldId_elem)
|
|
# Also remove the relationship
|
|
if rId in prs.part.rels:
|
|
prs.part.drop_rel(rId)
|
|
|
|
print(f'Removed 3 old diagram slides. Now have {len(prs.slides)} slides.')
|
|
|
|
# Step 2: Add 10 new diagram slides
|
|
# Use blank layout
|
|
blank_layout = prs.slide_layouts[0] # DEFAULT layout
|
|
|
|
for idx, (filename, title, subtitle) in enumerate(DIAGRAMS):
|
|
img_path = os.path.join(DIAGRAMS_DIR, filename)
|
|
if not os.path.exists(img_path):
|
|
print(f'WARNING: {filename} not found, skipping')
|
|
continue
|
|
|
|
slide = prs.slides.add_slide(blank_layout)
|
|
add_dark_background(slide, prs)
|
|
add_header_bar(slide, title, subtitle)
|
|
add_diagram_image(slide, img_path)
|
|
add_slide_number(slide, idx + 1, 10)
|
|
|
|
print(f'Added slide {idx+1}: {title}')
|
|
|
|
# Save
|
|
prs.save(PPTX_PATH)
|
|
print(f'\nSaved: {PPTX_PATH}')
|
|
print(f'Total slides: {len(prs.slides)}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|