"""Extract exact visual design info from the reference PPTX into _design_dump.txt.""" from pathlib import Path import sys from pptx import Presentation from pptx.util import Emu from pptx.dml.color import RGBColor from pptx.enum.shapes import MSO_SHAPE_TYPE from pptx.oxml.ns import qn PPT_DIR = Path(__file__).parent SRC = PPT_DIR / "주간보고서_dwjang_20260604.pptx" OUT = PPT_DIR / "_design_dump.txt" def emu_to_in(v): try: return f"{Emu(v).inches:.2f}" except Exception: return "?" def fmt_xy(left, top, width, height): return ( f"L={left} T={top} W={width} H={height} EMU | " f"L={emu_to_in(left)}in T={emu_to_in(top)}in " f"W={emu_to_in(width)}in H={emu_to_in(height)}in" ) def color_from_fill(fill): """Best-effort extraction of solid fill color as hex.""" try: if fill.type is None: return "none/inherit" # 1 = SOLID per MSO_FILL_TYPE if int(fill.type) == 1: fc = fill.fore_color try: rgb = fc.rgb if rgb is not None: return f"#{str(rgb)}" except Exception: pass try: return f"theme:{fc.theme_color}" except Exception: pass return f"type={fill.type}" except Exception as e: return f"err:{e.__class__.__name__}" def color_from_line(line): try: c = line.color try: rgb = c.rgb if rgb is not None: return f"#{str(rgb)}" except Exception: pass try: return f"theme:{c.theme_color}" except Exception: return "inherit" except Exception: return "n/a" def run_color(run): try: c = run.font.color try: rgb = c.rgb if rgb is not None: return f"#{str(rgb)}" except Exception: pass try: return f"theme:{c.theme_color}" except Exception: return "inherit" except Exception: return "inherit" def slide_bg(slide): """Try to read slide background fill.""" try: bg = slide.background return color_from_fill(bg.fill) except Exception as e: return f"err:{e.__class__.__name__}" def shape_type_name(shape): try: st = shape.shape_type if st is None: return "None" return str(st) except Exception: return "?" def autoshape_kind(shape): try: return str(shape.auto_shape_type) except Exception: return "" def dump_text_frame(tf, out_lines, indent=" "): for pi, para in enumerate(tf.paragraphs): lvl = para.level align = para.alignment # Detect bullet via pPr/buChar/buAutoNum pPr = para._pPr bullet = "-" if pPr is not None: if pPr.find(qn("a:buNone")) is not None: bullet = "noBul" elif pPr.find(qn("a:buChar")) is not None: bc = pPr.find(qn("a:buChar")) bullet = f"buChar={bc.get('char')!r}" elif pPr.find(qn("a:buAutoNum")) is not None: bullet = "buAutoNum" runs_info = [] full_txt = "" for r in para.runs: t = r.text or "" full_txt += t f = r.font runs_info.append({ "text": t, "name": f.name, "size": (f.size.pt if f.size else None), "bold": f.bold, "italic": f.italic, "color": run_color(r), }) snippet = full_txt[:60].replace("\n", " ") out_lines.append( f"{indent}p{pi} lvl={lvl} align={align} {bullet} text={snippet!r}" ) for ri, info in enumerate(runs_info): tt = info["text"][:40].replace("\n", " ") out_lines.append( f"{indent} r{ri} font={info['name']!r} sz={info['size']} " f"b={info['bold']} i={info['italic']} c={info['color']} t={tt!r}" ) def dump_shape(shape, out_lines, indent=" "): name = shape.name st = shape_type_name(shape) kind = autoshape_kind(shape) if st == "MSO_SHAPE_TYPE.AUTO_SHAPE (1)" else "" try: left, top, width, height = shape.left, shape.top, shape.width, shape.height except Exception: left = top = width = height = None out_lines.append( f"{indent}- shape name={name!r} type={st} autoshape={kind}" ) out_lines.append(f"{indent} geom: {fmt_xy(left, top, width, height)}") # Fill try: if shape.has_text_frame or st.startswith("MSO_SHAPE_TYPE.AUTO_SHAPE") or st == "MSO_SHAPE_TYPE.TEXT_BOX (17)": fill_c = color_from_fill(shape.fill) out_lines.append(f"{indent} fill: {fill_c}") except Exception: pass # Line try: line = shape.line lc = color_from_line(line) try: lw = line.width lw_pt = (lw / 12700) if lw else None except Exception: lw_pt = None out_lines.append(f"{indent} line: color={lc} width_pt={lw_pt}") except Exception: pass # Picture if st == "MSO_SHAPE_TYPE.PICTURE (13)": try: img = shape.image out_lines.append( f"{indent} picture: ext={img.ext} sha1={img.sha1[:10]} " f"size_bytes={len(img.blob)} filename={getattr(img, 'filename', None)!r}" ) except Exception as e: out_lines.append(f"{indent} picture: err={e}") # Text if shape.has_text_frame: tf = shape.text_frame # vertical anchor try: va = tf.vertical_anchor except Exception: va = None try: wrap = tf.word_wrap except Exception: wrap = None out_lines.append( f"{indent} textframe: anchor={va} wrap={wrap} " f"mL={tf.margin_left} mR={tf.margin_right} mT={tf.margin_top} mB={tf.margin_bottom}" ) # Full text first 60 chars full = (shape.text_frame.text or "").replace("\n", " | ") out_lines.append(f"{indent} text(60)={full[:60]!r}") dump_text_frame(tf, out_lines, indent=indent + " ") # Group if st == "MSO_SHAPE_TYPE.GROUP (6)": out_lines.append(f"{indent} group children:") for child in shape.shapes: dump_shape(child, out_lines, indent=indent + " ") def main(): prs = Presentation(SRC) lines = [] lines.append(f"FILE: {SRC.name}") lines.append( f"SLIDE SIZE: W={prs.slide_width} H={prs.slide_height} EMU | " f"W={emu_to_in(prs.slide_width)}in H={emu_to_in(prs.slide_height)}in" ) lines.append(f"SLIDE COUNT: {len(prs.slides)}") lines.append("") # Layouts available lines.append("LAYOUTS:") for i, lay in enumerate(prs.slide_layouts): lines.append(f" [{i}] {lay.name}") lines.append("") # Master fonts / theme colors (best effort) try: theme_xml = prs.slide_masters[0].element.getroottree() except Exception: pass for si, slide in enumerate(prs.slides, start=1): lines.append("=" * 70) lines.append(f"SLIDE {si}") lines.append(f" layout: {slide.slide_layout.name}") lines.append(f" background: {slide_bg(slide)}") lines.append(f" shape_count: {len(slide.shapes)}") for shp in slide.shapes: dump_shape(shp, lines, indent=" ") lines.append("") OUT.write_text("\n".join(lines), encoding="utf-8") print(f"WROTE: {OUT} lines={len(lines)}") if __name__ == "__main__": main()