#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""把 PPTX 抽成版式模型 JSON（文字/表格/图片/图表/组合），供 HTML 生成器使用。"""
import base64, json, os, sys
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
from pptx.util import Emu

F = sys.argv[1]; OUT = sys.argv[2]
prs = Presentation(F)
SW, SH = prs.slide_width, prs.slide_height

def pct(v, base): return round(v / base * 100, 4) if v is not None else None

def run_info(r):
    c = None; sc = None
    _G = '{http://schemas.openxmlformats.org/drawingml/2006/main}'
    try:
        _sf = r._r.find(_G + 'rPr/' + _G + 'solidFill')
        if _sf is None:
            _p = r._r.getparent()
            if _p is not None:
                _sf = _p.find(_G + 'pPr/' + _G + 'defRPr/' + _G + 'solidFill')
        if _sf is not None:
            _s = _sf.find(_G + 'schemeClr')
            if _s is not None: sc = _s.get('val')
            _x = _sf.find(_G + 'srgbClr')
            if _x is not None: c = _x.get('val')
            _y = _sf.find(_G + 'sysClr')
            if _y is not None and not c: c = _y.get('lastClr')
    except Exception: pass
    return {
        't': r.text,
        'sz': (r.font.size.pt if r.font.size else None),
        'b': bool(r.font.bold),
        'i': bool(r.font.italic),
        'c': c,
        'sc': sc,
        'f': r.font.name,
    }

def rpr_info(el):
    """读取 rPr / defRPr 的显式格式"""
    if el is None:
        return None
    _G = '{http://schemas.openxmlformats.org/drawingml/2006/main}'
    d = {}
    if el.get('sz'): d['sz'] = int(el.get('sz')) / 100.0
    if el.get('b') is not None: d['b'] = (el.get('b') == '1')
    if el.get('i') is not None: d['i'] = (el.get('i') == '1')
    _l = el.find(_G + 'latin')
    if _l is not None and _l.get('typeface'): d['f'] = _l.get('typeface')
    _sf = el.find(_G + 'solidFill')
    if _sf is not None:
        _s = _sf.find(_G + 'schemeClr')
        if _s is not None: d['sc'] = _s.get('val')
        _x = _sf.find(_G + 'srgbClr')
        if _x is not None: d['c'] = _x.get('val')
    return d or None


def para_info(p):
    runs = [run_info(r) for r in p.runs]
    _G = '{http://schemas.openxmlformats.org/drawingml/2006/main}'
    return {
        'runs': runs,
        'lv': p.level,
        'd': rpr_info(p._p.find(_G + 'pPr/' + _G + 'defRPr')),
        'al': str(p.alignment).split(' ')[0] if p.alignment is not None else None,
    }

def text_info(sh):
    tf = sh.text_frame
    paras = [para_info(p) for p in tf.paragraphs]
    _G = '{http://schemas.openxmlformats.org/drawingml/2006/main}'
    lst = None
    try:
        lst = rpr_info(tf._txBody.find(_G + 'lstStyle/' + _G + 'lvl1pPr/' + _G + 'defRPr'))
    except Exception: pass
    # 段落级统一样式（无 run 时用）
    first = None
    for p in tf.paragraphs:
        if p.runs:
            first = run_info(p.runs[0]); break
    return {
        'lst': lst,
        'paras': paras,
        'plain': tf.text,
        'anchor': str(tf.vertical_anchor).split(' ')[0] if tf.vertical_anchor is not None else None,
        'wrap': bool(tf.word_wrap) if tf.word_wrap is not None else None,
        'def_sz': first['sz'] if first else None,
        'def_c': first['c'] if first else None,
        'def_b': first['b'] if first else None,
    }

def table_info(sh):
    tb = sh.table
    grid = []
    for ri, row in enumerate(tb.rows):
        r = []
        for ci, cell in enumerate(row.cells):
            _runs, _al = [], None
            for _p in cell.text_frame.paragraphs:
                if _al is None and _p.alignment is not None:
                    _al = str(_p.alignment).split(' ')[0]
                for _r in _p.runs:
                    _runs.append(run_info(_r))
            r.append({
                'runs': _runs, 'al': _al,
                'anchor': str(cell.vertical_anchor).split(' ')[0] if cell.vertical_anchor is not None else None,
                't': cell.text,
                'span_w': cell.span_width, 'span_h': cell.span_height,
                'origin': bool(getattr(cell, 'is_merge_origin', False)),
                'spanned': bool(getattr(cell, 'is_spanned', False)),
                'fill': (str(cell.fill.fore_color.rgb)
                         if cell.fill.type == 1 and cell.fill.fore_color.type == 1 else None),
            })
        grid.append(r)
    return {'rows': len(tb.rows), 'cols': len(tb.columns),
            'col_w': [pct(c.width, SW) for c in tb.columns], 'grid': grid}

def chart_info(sh):
    """⚠️ python-pptx 的 chart.plots 读不到本文件的图表数据（返回空）→ 直接解析 chart part XML"""
    import xml.etree.ElementTree as ET
    ch = sh.chart
    C = '{http://schemas.openxmlformats.org/drawingml/2006/chart}'
    out = {'type': '', 'cats': [], 'series': [], 'title': ''}

    def tval(el):
        v = el.find('.//' + C + 'v')
        return v.text if v is not None else None

    try:
        cs = ET.fromstring(ch.part.blob)
        for tag in ('barChart', 'pieChart', 'lineChart', 'doughnutChart', 'areaChart'):
            if cs.find('.//' + C + tag) is not None:
                out['type'] = tag.upper()
                break
        for ser in cs.iter(C + 'ser'):
            tx = ser.find(C + 'tx')
            nm = tval(tx) if tx is not None else None
            cat = ser.find(C + 'cat')
            cats, vals = [], []
            if cat is not None:
                cats = [p.find(C + 'v').text for p in cat.iter(C + 'pt') if p.find(C + 'v') is not None]
            val = ser.find(C + 'val')
            if val is not None:
                vals = [p.find(C + 'v').text for p in val.iter(C + 'pt') if p.find(C + 'v') is not None]
            if cats and not out['cats']:
                out['cats'] = cats
            out['series'].append({'name': nm, 'vals': vals})
    except Exception as e:
        out['err'] = str(e)
    try:
        out['title'] = ch.chart_title.text_frame.text
    except Exception: pass
    return out

def walk(shapes, z0=''):
    res = []
    for i, sh in enumerate(shapes):
        z = '%s%02d' % (z0, i)
        d = {'z': z, 'name': sh.name,
             'L': pct(sh.left, SW), 'T': pct(sh.top, SH),
             'W': pct(sh.width, SW), 'H': pct(sh.height, SH),
             'em': [sh.left, sh.top, sh.width, sh.height],
             'rot': getattr(sh, 'rotation', 0)}
        st = sh.shape_type
        if st == MSO_SHAPE_TYPE.GROUP:
            d['kind'] = 'group'
            _G = '{http://schemas.openxmlformats.org/drawingml/2006/main}'
            _P = '{http://schemas.openxmlformats.org/presentationml/2006/main}'
            _gs = sh._element.find(_P + 'grpSpPr')
            _xf = _gs.find(_G + 'xfrm') if _gs is not None else None
            _go = _gc = None
            if _xf is not None:
                _o = _xf.find(_G + 'off'); _e = _xf.find(_G + 'ext')
                _co = _xf.find(_G + 'chOff'); _ce = _xf.find(_G + 'chExt')
                if None not in (_o, _e, _co, _ce):
                    _go = [int(_o.get('x')), int(_o.get('y')), int(_e.get('cx')), int(_e.get('cy'))]
                    _gc = [int(_co.get('x')), int(_co.get('y')), int(_ce.get('cx')), int(_ce.get('cy'))]
            d['gxfrm'] = [_go, _gc]
            d['children'] = walk(sh.shapes, z)
        elif st == MSO_SHAPE_TYPE.PICTURE:
            d['kind'] = 'pic'
            img = sh.image
            d['ext'] = img.ext
            d['px'] = list(img.size)
            d['b64'] = base64.b64encode(img.blob).decode()
            d['sha'] = img.sha1
        elif getattr(sh, 'has_table', False) and sh.has_table:
            d['kind'] = 'table'
            d.update(table_info(sh))
        elif getattr(sh, 'has_chart', False) and sh.has_chart:
            d['kind'] = 'chart'
            d['chart'] = chart_info(sh)
        elif st == MSO_SHAPE_TYPE.EMBEDDED_OLE_OBJECT or 'OLE' in str(st):
            d['kind'] = 'ole'
        elif sh.has_text_frame and sh.text_frame.text.strip():
            d['kind'] = 'text'
            d['txt'] = text_info(sh)
            try:
                if sh.fill.type == 1 and sh.fill.fore_color.type == 1:
                    d['bg'] = str(sh.fill.fore_color.rgb)
            except Exception: pass
        else:
            d['kind'] = 'shape'
            try:
                if sh.fill.type == 1 and sh.fill.fore_color.type == 1:
                    d['bg'] = str(sh.fill.fore_color.rgb)
            except Exception: pass
        res.append(d)
    return res

def theme_colors(path):
    """解析 theme1.xml 的 clrScheme；bg1/tx1 映射到 lt1/dk1"""
    out = {}
    try:
        import zipfile, xml.etree.ElementTree as ET
        s = zipfile.ZipFile(path).read('ppt/theme/theme1.xml').decode('utf-8')
        G = '{http://schemas.openxmlformats.org/drawingml/2006/main}'
        cs = ET.fromstring(s).find('.//' + G + 'clrScheme')
        if cs is not None:
            for ch in cs:
                v = None
                for c2 in ch:
                    v = c2.get('val') or c2.get('lastClr')
                    if v: break
                out[ch.tag.replace(G, '')] = ('#' + v) if v else None
    except Exception as e:
        out['_err'] = str(e)
    for a, b in (('bg1', 'lt1'), ('tx1', 'dk1'), ('bg2', 'lt2'), ('tx2', 'dk2')):
        if a not in out and b in out: out[a] = out[b]
        if b not in out and a in out: out[b] = out[a]
    return out

model = {'slideW': SW, 'slideH': SH, 'theme': theme_colors(F), 'slides': []}
for i, s in enumerate(prs.slides, 1):
    model['slides'].append({
        'n': i, 'layout': s.slide_layout.name,
        'layshapes': walk(s.slide_layout.shapes),   # ⚠️ 版式背景/页脚带/二维码在这里，漏了就全白底
        'shapes': walk(s.shapes),
    })

json.dump(model, open(OUT, 'w', encoding='utf-8'), ensure_ascii=False)
print('✅ 模型已写出 %s (%.1f KB)' % (OUT, os.path.getsize(OUT)/1024))
tot = sum(len(json.dumps(x)) for x in model['slides'])
print('   %d 张 | 形状合计 %d' % (len(model['slides']), sum(len(sl['shapes']) for sl in model['slides'])))
