#!/usr/bin/env python3 """ extract-pptx-theme.py — extrai design tokens de um ficheiro PPTX Uso: python3 extract-pptx-theme.py [--output tokens.json] Devolve JSON no mesmo formato do extract-web-tokens.js para que /clone-style apply seja unificado entre os 2 modos. """ import sys import json import zipfile import xml.etree.ElementTree as ET from pathlib import Path from datetime import datetime, timezone NS = {'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'} def extract_theme(pptx_path: Path) -> dict: if not pptx_path.exists(): raise FileNotFoundError(f"Não encontrado: {pptx_path}") with zipfile.ZipFile(pptx_path) as z: # Listar themes (pode haver múltiplos; usar o primeiro) theme_files = sorted([n for n in z.namelist() if n.startswith('ppt/theme/theme') and n.endswith('.xml')]) if not theme_files: raise ValueError("PPTX sem theme — formato inválido ou corrompido") theme_xml = z.read(theme_files[0]) root = ET.fromstring(theme_xml) # Extrair cores do clrScheme colors = {} for clr_scheme in root.iter('{http://schemas.openxmlformats.org/drawingml/2006/main}clrScheme'): for color_node in clr_scheme: name = color_node.tag.split('}')[1] # ex: 'accent1', 'dk1', 'lt1' srgb = color_node.find('a:srgbClr', NS) sysclr = color_node.find('a:sysClr', NS) if srgb is not None: colors[name] = '#' + srgb.get('val') elif sysclr is not None: # sysClr usa o lastClr como fallback (cor resolvida) colors[name] = '#' + sysclr.get('lastClr', '000000') # Extrair fontes do fontScheme fonts = {'headline': None, 'body': None} for fs in root.iter('{http://schemas.openxmlformats.org/drawingml/2006/main}fontScheme'): major = fs.find('a:majorFont/a:latin', NS) minor = fs.find('a:minorFont/a:latin', NS) if major is not None: fonts['headline'] = major.get('typeface') if minor is not None: fonts['body'] = minor.get('typeface') # Mapear cores PPTX → estrutura unificada accent_keys = ['accent1', 'accent2', 'accent3', 'accent4', 'accent5', 'accent6'] accents = [colors[k] for k in accent_keys if k in colors] primary = colors.get('accent1') or accents[0] if accents else '#000000' secondary = colors.get('accent2') tertiary = colors.get('accent3') neutral = colors.get('lt1') or colors.get('dk1') or '#ffffff' background = colors.get('lt1') or colors.get('bg1') or '#ffffff' is_dark = colors.get('lt1', '#ffffff').lstrip('#').upper() != 'FFFFFF' return { 'url': str(pptx_path), 'title': pptx_path.stem, 'source': 'pptx', 'timestamp': datetime.now(timezone.utc).isoformat(), 'colors': { 'top': [{'value': v, 'count': 1} for v in accents], 'backgrounds': [{'value': background, 'count': 1}], 'dominant': primary, 'raw_scheme': colors, }, 'fonts': { 'families': [ {'value': fonts['headline'], 'count': 1, 'role': 'headline'}, {'value': fonts['body'], 'count': 1, 'role': 'body'}, ], 'sizes': [], 'weights': [], 'lineHeights': [], }, 'borderRadius': [], 'shadows': [], 'spacing': {'gridBaseGuess': 8, 'top': []}, 'components': {}, 'meta': { 'colorMode': 'DARK' if is_dark else 'LIGHT', 'theme_file': theme_files[0], 'accent_count': len(accents), }, 'mapping_hint': { 'stitch_customColor': primary, 'stitch_overrideSecondary': secondary, 'stitch_overrideTertiary': tertiary, 'stitch_overrideNeutral': neutral, 'note': 'PPTX só fornece cores e fontes — borderRadius/shadows/spacing têm de ser definidos manualmente em designMd', }, } def main(): if len(sys.argv) < 2: print(__doc__, file=sys.stderr) sys.exit(1) pptx = Path(sys.argv[1]).expanduser().resolve() output_path = None if '--output' in sys.argv: idx = sys.argv.index('--output') if idx + 1 < len(sys.argv): output_path = Path(sys.argv[idx + 1]).expanduser().resolve() tokens = extract_theme(pptx) js = json.dumps(tokens, indent=2, ensure_ascii=False) if output_path: output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_text(js, encoding='utf-8') print(f"✅ Tokens guardados em {output_path}", file=sys.stderr) else: print(js) if __name__ == '__main__': main()