Spaces:
Sleeping
Sleeping
| 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) |