# -*- coding: utf-8 -*-
"""HWPX에 그림 PNG 임베드 — 검증된 구조(2026_스테이블코인 report_그림.hwpx) 재현.
- BinData/figimgN.png 추가 + content.hpf opf:item + 캡션([그림 N-M], styleIDRef=14) 문단 '앞'에 hp:pic 문단 삽입
- 본문 시작(제1장, styleIDRef=4) 이후의 캡션만 대상(그림 목차 오삽입 방지)
- 폭 = A4 본문폭 42520 (finalize_layout의 rescaleBodyToNative가 figimg를 native로 축소)

사용: python embed_figs.py <in.hwpx> <out.hwpx> <manifest.json>
manifest.json: [{"caption": "[그림 2-1]", "png": "path/to.png", "width_ratio": 1.0}, ...]
width_ratio(선택): 본문폭 대비 그림 폭 비율(기본 1.0, 세로형 도표는 0.6~0.8 권장)
"""
import json
import re
import struct
import sys
import zipfile

BODY_W = 42520

PIC_TMPL = (
    '<hp:p id="{pid}" paraPrIDRef="{parapr}" styleIDRef="{style}" pageBreak="0" columnBreak="0" merged="0">'
    '<hp:run charPrIDRef="{charpr}"><hp:pic reverse="0" instId="{inst}"><hp:offset x="0" y="0"/>'
    '<hp:orgSz width="{w}" height="{h}"/><hp:curSz width="{w}" height="{h}"/>'
    '<hp:flip horizontal="0" vertical="0"/>'
    '<hp:rotationInfo angle="0" centerX="{cx}" centerY="{cy}" rotateimage="1"/>'
    '<hp:renderingInfo><hc:transMatrix e1="1" e2="0" e3="0" e4="0" e5="1" e6="0"/>'
    '<hc:scaMatrix e1="1" e2="0" e3="0" e4="0" e5="1" e6="0"/>'
    '<hc:rotMatrix e1="1" e2="0" e3="0" e4="0" e5="1" e6="0"/></hp:renderingInfo>'
    '<hc:img binaryItemIDRef="{imgid}" bright="0" contrast="0" effect="REAL_PIC" alpha="0"/>'
    '<hp:imgRect><hc:pt0 x="0" y="0"/><hc:pt1 x="{w}" y="0"/><hc:pt2 x="{w}" y="{h}"/><hc:pt3 x="0" y="{h}"/></hp:imgRect>'
    '<hp:imgClip left="0" right="{w}" top="0" bottom="{h}"/>'
    '<hp:inMargin left="0" right="0" top="0" bottom="0"/>'
    '<hp:imgDim dimwidth="{w}" dimheight="{h}"/>'
    '<hp:sz width="{w}" widthRelTo="ABSOLUTE" height="{h}" heightRelTo="ABSOLUTE" protect="0"/>'
    '<hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="1" allowOverlap="0" holdAnchorAndSO="0" '
    'vertRelTo="PARA" horzRelTo="COLUMN" vertAlign="TOP" horzAlign="CENTER" vertOffset="0" horzOffset="0"/>'
    '<hp:outMargin left="0" right="0" top="0" bottom="0"/><hp:shapeComment/></hp:pic></hp:run></hp:p>'
)


def png_size(path):
    with open(path, "rb") as f:
        head = f.read(33)
    if head[:8] != b"\x89PNG\r\n\x1a\n" or head[12:16] != b"IHDR":
        raise ValueError("PNG 아님: " + path)
    w, h = struct.unpack(">II", head[16:24])
    return w, h


def main(inp, outp, manifest_path):
    entries = json.load(open(manifest_path, encoding="utf-8"))
    zin = zipfile.ZipFile(inp)
    sec = zin.read("Contents/section0.xml").decode("utf-8")
    hpf = zin.read("Contents/content.hpf").decode("utf-8")

    # 본문 시작(제1장 styleIDRef=4) offset — 해당 문단 내부(</hp:p> 전)의 첫 텍스트가 '제1장'으로 시작해야 함
    m_body = None
    for m in re.finditer(r'<hp:p [^>]*styleIDRef="4"[^>]*>', sec):
        endp = sec.find("</hp:p>", m.start())
        seg = sec[m.start():endp]
        lead = re.sub(r"<[^>]+>", "", seg).strip()
        if lead.startswith("제1장"):
            m_body = m.start()
            break
    if m_body is None:
        raise RuntimeError("본문 시작(제1장) 미발견")

    items_xml = []
    bindata = []
    inserted = []
    for n, e in enumerate(entries, 1):
        cap = e["caption"]
        ratio = float(e.get("width_ratio", 1.0))
        pw, ph = png_size(e["png"])
        w = round(BODY_W * ratio)
        h = round(w * ph / pw)
        imgid = "figimg%d" % n
        # 본문 내 캡션 문단 탐색
        pos = None
        for m in re.finditer(r'<hp:p [^>]*styleIDRef="14"[^>]*>', sec):
            if m.start() < m_body:
                continue
            endp = sec.find("</hp:p>", m.start())
            seg = sec[m.start():endp]
            if cap.replace("[", "").replace("]", "") in re.sub(r"<[^>]+>", "", seg).replace("[", "").replace("]", ""):
                pos = m
                break
        if pos is None:
            print("!! 캡션 미발견:", cap)
            continue
        attrs = dict(re.findall(r'(\w+)="([^"]*)"', pos.group(0)))
        charpr = re.search(r'charPrIDRef="(\d+)"', sec[pos.start():pos.start() + 400])
        pic_para = PIC_TMPL.format(
            pid=9500000 + n, inst=9600000 + n, imgid=imgid,
            parapr=attrs.get("paraPrIDRef", "12"), style=attrs.get("styleIDRef", "14"),
            charpr=charpr.group(1) if charpr else "13",
            w=w, h=h, cx=w // 2, cy=h // 2,
        )
        sec = sec[:pos.start()] + pic_para + sec[pos.start():]
        m_body = m_body  # 본문 시작 offset은 그대로(삽입은 항상 이후)
        items_xml.append('<opf:item id="%s" href="BinData/%s.png" media-type="image/png" isEmbeded="1"/>' % (imgid, imgid))
        bindata.append((imgid, e["png"]))
        inserted.append("%s <- %s (%dx%d -> %dx%d HWPUNIT)" % (cap, e["png"], pw, ph, w, h))

    hpf = hpf.replace("</opf:manifest>", "".join(items_xml) + "</opf:manifest>")

    # 태그 균형 검증 (자기닫힘 <tag .../> 제외)
    for tag in ("hp:p", "hp:pic", "hp:run"):
        allop = re.findall("<" + tag + r"(?:[ >][^>]*)?>", sec)
        selfc = [t for t in allop if t.endswith("/>")]
        o = len(allop) - len(selfc)
        c = len(re.findall("</" + tag + ">", sec))
        if o != c:
            raise RuntimeError("%s 불균형 %d/%d (자기닫힘 %d 제외)" % (tag, o, c, len(selfc)))

    with zipfile.ZipFile(outp, "w", zipfile.ZIP_DEFLATED) as zout:
        for item in zin.infolist():
            if item.filename == "Contents/section0.xml":
                zout.writestr(item, sec.encode("utf-8"))
            elif item.filename == "Contents/content.hpf":
                zout.writestr(item, hpf.encode("utf-8"))
            elif item.filename == "mimetype":
                zout.writestr(item, zin.read(item.filename), zipfile.ZIP_STORED)
            else:
                zout.writestr(item, zin.read(item.filename))
        for imgid, png in bindata:
            zout.write(png, "BinData/%s.png" % imgid)

    print("임베드 %d건:" % len(inserted))
    for s in inserted:
        print(" -", s)


if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2], sys.argv[3])
