"""도면↔모델 불일치 시각자료 합성기 (운곡교 M-5 이미지 형식: 헤더 / 도면 패널(파랑) / 모델 패널(주황·노랑·회색) / 차이·도면 쪽·조치). 사용: from compose_new import make; make(cfg) cfg = {code, title, subtitle, color:(r,g,b), out, panels:[{label, color, images:[{path, crop(optional), cap(optional)}], maxh}], rows:[(라벨, 본문)]} """ import os, sys sys.path.insert(0, r"C:\Users\User\AppData\Local\Temp\claude\D--Rhino-MCP-rhino-mcp-main\f46b47bc-9105-4079-9b62-fcbee31b26ae\scratchpad\pylib") from PIL import Image, ImageDraw, ImageFont FN = r"C:\Windows\Fonts\malgun.ttf" FB = r"C:\Windows\Fonts\malgunbd.ttf" W = 2400 PAD = 40 BLUE = (60, 120, 200) ORANGE = (230, 110, 20) DYEL = (190, 150, 0) GREY = (120, 120, 120) def font(sz, bold=False): return ImageFont.truetype(FB if bold else FN, sz) def fit(img, w, h): r = min(w / img.width, h / img.height) return img.resize((max(1, int(img.width * r)), max(1, int(img.height * r))), Image.LANCZOS) def wrap(draw, text, fnt, maxw): lines = [] for para in text.split("\n"): cur = "" for ch in para: if draw.textlength(cur + ch, font=fnt) > maxw: lines.append(cur) cur = ch else: cur += ch lines.append(cur) return lines def autocrop(im, pad=30, thr=14): bg = im.getpixel((2, 2)) px = im.load() w, h = im.size x0, y0, x1, y1 = w, h, 0, 0 for y in range(0, h, 2): for x in range(0, w, 2): p = px[x, y] if abs(p[0] - bg[0]) + abs(p[1] - bg[1]) + abs(p[2] - bg[2]) > thr: if x < x0: x0 = x if x > x1: x1 = x if y < y0: y0 = y if y > y1: y1 = y if x1 <= x0: return im return im.crop((max(0, x0 - pad), max(0, y0 - pad), min(w, x1 + pad), min(h, y1 + pad))) def load(spec): im = Image.open(spec["path"]).convert("RGB") if spec.get("auto", spec["path"].find("shots") >= 0): im = autocrop(im) if spec.get("crop"): im = im.crop(spec["crop"]) if spec.get("draw"): spec["draw"](im) return im def make(cfg): PW = W - 2 * PAD # 패널 이미지 준비 prepared = [] for p in cfg["panels"]: ims = [load(s) for s in p["images"]] n = len(ims) gap = 20 cw = (PW - 8 - gap * (n - 1)) // n maxh = p.get("maxh", 560) fitted = [fit(im, cw, maxh) for im in ims] ph = max(f.height for f in fitted) + 8 capsp = 0 if any(s.get("cap") for s in p["images"]): capsp = 52 prepared.append((p, fitted, ph, capsp, cw, gap)) # 본문 줄 계산 dtmp = ImageDraw.Draw(Image.new("RGB", (10, 10))) fr = font(32) rows = [] for lab, text in cfg["rows"]: lines = wrap(dtmp, text, fr, PW - 230) rows.append((lab, lines)) H = 120 + 14 for p, fitted, ph, capsp, cw, gap in prepared: H += 50 + 6 + ph + capsp + 14 H += 12 for lab, lines in rows: H += max(46, 42 * len(lines)) + 12 H += 24 img = Image.new("RGB", (W, H), (255, 255, 255)) d = ImageDraw.Draw(img) # 헤더 d.rectangle([0, 0, W, 120], fill=(38, 44, 56)) d.rounded_rectangle([PAD - 6, 24, PAD + 130, 96], radius=10, fill=cfg.get("color", ORANGE)) d.text((PAD + 62, 60), cfg["code"], font=font(52, True), fill=(255, 255, 255), anchor="mm") d.text((PAD + 170, 14), cfg["title"], font=font(48, True), fill=(255, 255, 255)) d.text((PAD + 170, 74), cfg["subtitle"], font=font(30), fill=(210, 215, 225)) y = 134 for p, fitted, ph, capsp, cw, gap in prepared: col = p.get("color", BLUE) d.rectangle([PAD - 6, y, W - PAD + 6, y + 50], fill=col) d.text((PAD + 10, y + 25), p["label"], font=font(32, True), fill=(255, 255, 255), anchor="lm") y += 56 d.rectangle([PAD - 6, y, W - PAD + 6, y + ph + capsp + 4], outline=col, width=4) x = PAD for f, spec in zip(fitted, p["images"]): img.paste(f, (x, y + 4)) if spec.get("cap"): d.text((x + 6, y + 4 + f.height + 4), spec["cap"], font=font(30, True), fill=(30, 30, 30)) x += cw + gap y += ph + capsp + 18 y += 6 for lab, lines in rows: h = max(46, 42 * len(lines)) d.rounded_rectangle([PAD - 6, y, PAD + 180, y + 44], radius=8, fill=(232, 236, 244)) d.text((PAD + 87, y + 22), lab, font=font(30, True), fill=(30, 30, 40), anchor="mm") for i, ln in enumerate(lines): d.text((PAD + 210, y + 4 + 42 * i), ln, font=fr, fill=(25, 25, 25)) y += h + 12 os.makedirs(os.path.dirname(cfg["out"]), exist_ok=True) img.save(cfg["out"]) return img.size