ppt-tool/backend/utils/oxml_geometry.py
Vadym Samoilenko c431d4ab45 Implement critical security fixes and modern design system (Pre-launch P0 tasks)
Security Improvements (P0.0-P0.4):
- P0.0: Migrate to Gemini-only AI stack (simplified, single billing)
- P0.1: Fix CORS to restrict allowed origins from env (was *)
- P0.2: Remove hardcoded dev password, require env var
- P0.3: Add rate limiting (slowapi) - 3-10 req/min on sensitive endpoints
- P0.4: Add request size limits (100MB default via middleware)

New Features:
- Unified LLM service with Google Gemini priority
- OXML geometry extractor for layout parsing
- TSX validator for generated React components
- Client ID support in presentation requests with access control
- Configurable LLM/image timeouts via env vars

Modern Design System (P0.9 - partial):
- Enhanced CSS design tokens (primary, semantic colors, shadows)
- Typography scale (h1-h4, body variants, caption)
- Modern animations (fadeIn, slideIn, scaleIn)
- Updated Button component with better variants and hover effects
- Created unified Card and StatusBadge components
- Applied design system to Dashboard and Settings pages

Backend Improvements:
- Master deck parser simplification
- Slide-to-HTML endpoint cleanup (325 lines removed)
- Better error handling in prompts endpoint

Frontend Improvements:
- Settings UI simplified to show only Google/Gemini
- Dashboard uses CSS variables instead of hardcoded colors
- Improved button transitions and hover states

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2026-02-27 18:28:24 +00:00

100 lines
3.4 KiB
Python

import xml.etree.ElementTree as ET
import json
from typing import List, Dict
NS = {
"a": "http://schemas.openxmlformats.org/drawingml/2006/main",
"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
}
# 1 EMU (English Metric Unit) = 1/360000 of a centimeter = 1/914400 of an inch
# 1 pixel = 9525 EMU (assuming 96 DPI: 914400 / 96 = 9525)
EMU_TO_PX = 9525
def _px(emu_str: str) -> int:
if not emu_str: return 0
try:
return round(int(emu_str) / EMU_TO_PX)
except Exception:
return 0
def extract_geometry_from_oxml(xml_content: str) -> List[Dict]:
"""
Parses OXML slide content and extracts bounding boxes for shapes, pictures, and text.
Returns a list of dictionaries with x, y, width, height (in pixels), and type/text content.
"""
elements = []
try:
root = ET.fromstring(xml_content)
# Look at both slide and slideLayout wrappers
spTree = root.find(".//p:spTree", NS)
if spTree is None:
return elements
# Process standard shapes (mostly text boxes)
for sp in spTree.findall("p:sp", NS):
element_data = _extract_xfrm(sp)
if element_data:
nvSpPr = sp.find("p:nvSpPr/p:cNvSpPr", NS)
element_data["type"] = "shape"
if nvSpPr is not None:
element_data["name"] = nvSpPr.get("name", "")
# Check for text
txBody = sp.find("p:txBody", NS)
if txBody is not None:
text_parts = []
for t in txBody.findall(".//a:t", NS):
if t.text: text_parts.append(t.text)
if text_parts:
element_data["type"] = "text_box"
element_data["text"] = " ".join(text_parts).strip()
elements.append(element_data)
# Process pictures
for pic in spTree.findall("p:pic", NS):
element_data = _extract_xfrm(pic)
if element_data:
element_data["type"] = "picture"
nvPicPr = pic.find("p:nvPicPr/p:cNvPicPr", NS)
if nvPicPr is not None:
element_data["name"] = nvPicPr.get("name", "")
element_data["description"] = nvPicPr.get("descr", "")
elements.append(element_data)
except Exception as e:
print(f"Error extracting geometry: {e}")
return elements
def _extract_xfrm(node) -> Dict:
spPr = node.find("p:spPr", NS)
if spPr is None: return None
xfrm = spPr.find("a:xfrm", NS)
if xfrm is None: return None
off = xfrm.find("a:off", NS)
ext = xfrm.find("a:ext", NS)
if off is None or ext is None: return None
return {
"x": _px(off.get("x")),
"y": _px(off.get("y")),
"width": _px(ext.get("cx")),
"height": _px(ext.get("cy"))
}
def format_geometry_for_llm(elements: List[Dict]) -> str:
"""Formats the geometry list into a compact JSON string for the LLM prompt."""
if not elements:
return "[]"
# Sort primarily by pure Y (top to bottom), then X (left to right)
sorted_elements = sorted(elements, key=lambda e: (e.get("y", 0), e.get("x", 0)))
return json.dumps(sorted_elements, indent=2)