from __future__ import annotations from collections import Counter from pathlib import Path from pptx import Presentation from src.utils.validators import validate_template def _rgb_to_tuple(rgb_obj) -> tuple[int, int, int] | None: if rgb_obj is None: return None try: rgb_str = str(rgb_obj) if len(rgb_str) == 6: return (int(rgb_str[0:2], 16), int(rgb_str[2:4], 16), int(rgb_str[4:6], 16)) except Exception: return None return None def parse_template_style(template_path: str | Path) -> dict: validated_path = validate_template(template_path) prs = Presentation(str(validated_path)) if len(prs.slides) == 0: return { "layout_name_to_index": {layout.name: idx for idx, layout in enumerate(prs.slide_layouts)}, "preferred_layout_indices": { "title_slide": 0, "content_slide": min(1, len(prs.slide_layouts) - 1), "split_slide": min(3, len(prs.slide_layouts) - 1), }, "font": {}, "dominant_text_color": None, "shape_positions": [], "reference_slide_index": 0, } reference_slide_index = 1 if len(prs.slides) > 1 else 0 reference_slide = prs.slides[reference_slide_index] font_candidates = [] colors = [] shape_positions = [] for shape in reference_slide.shapes: shape_positions.append( { "left": int(shape.left), "top": int(shape.top), "width": int(shape.width), "height": int(shape.height), } ) if not getattr(shape, "has_text_frame", False): continue for paragraph in shape.text_frame.paragraphs: for run in paragraph.runs: font = run.font rgb = _rgb_to_tuple(getattr(font.color, "rgb", None)) if rgb: colors.append(rgb) font_candidates.append( { "name": font.name, "size_pt": float(font.size.pt) if font.size else None, "bold": font.bold, "italic": font.italic, "color_rgb": rgb, } ) chosen_font = {} if font_candidates: # Pick the most complete entry to reuse as baseline style. chosen_font = max( font_candidates, key=lambda entry: sum( 1 for key in ("name", "size_pt", "bold", "italic", "color_rgb") if entry.get(key) is not None ), ) dominant_color = Counter(colors).most_common(1)[0][0] if colors else chosen_font.get("color_rgb") layout_name_to_index = {layout.name: idx for idx, layout in enumerate(prs.slide_layouts)} return { "layout_name_to_index": layout_name_to_index, "preferred_layout_indices": { "title_slide": 0, "content_slide": min(1, len(prs.slide_layouts) - 1), "split_slide": min(3, len(prs.slide_layouts) - 1), }, "font": chosen_font, "dominant_text_color": dominant_color, "shape_positions": shape_positions, "reference_slide_index": reference_slide_index, } def load_template(template_path: str | Path) -> dict: """Backward-compatible alias used by service/tests.""" return parse_template_style(template_path)