# -*- coding: utf-8 -*- """그림 코드(I) — COSTN I 코드 행의 IFORM 메모 = 그림 바이트 · SIZE = 그림 꼴(Delphi 그림 클래스 이름 대문자). 근거(지식DB REA 분석/37 §4 · 35 §6): STmate.Cpm TwM_Boxa · 넣기 SpeedButton2Click 0x514df0 — 파일 열기(.BMP·.JPG·.JPEG·.ICO·.EMF·.WMF) · 비트맵이면 「비트맵 => JPEG 변환 저장」 (변환저장 · 비트맵저장 · 저장취소) · 그 밖 「그림저장」 · 저장 0x514bc0~0x514d30 — NAME 이 비면 파일 이름(확장자 뺌) · SIZE = UpperCase(그림.ClassName) · IFORM = 그림 스트림(TBlobField.LoadFromStream) · Commit · 보기 ds_CostIDataChange 0x5155a8 — SIZE(대문자 · 길이 > 2) 를 꼴로 IFORM 을 읽음 · 새 코드 0x4dac10 — 사용자가 친 코드 · 이름 「새로운 그림코드」 · 조달청 변환은 COSTN.DBF 의 CODE="I…" 행을 IMG.DBF 로(ST_U_Convert 0x4b0b40) — 그림 표 = COSTN 그림 바이트는 고른 파일 그대로(TJPEGImage·TIcon 은 같음 · TBitmap·TMetafile 의 SaveToStream 재기록과의 차이 = 조건부) · 비트맵 → JPEG 는 Pillow 품질 90(TJPEGImage 기본값 · 부호기가 달라 바이트는 다름). """ from __future__ import annotations import io from dbf_memo import BLANK_PTR, SixMemo from dbf_rw import DbfTable, find_rows KIND = {".JPG": "TJPEGIMAGE", ".JPEG": "TJPEGIMAGE", ".BMP": "TBITMAP", ".ICO": "TICON"} KIND |= {".EMF": "TMETAFILE", ".WMF": "TMETAFILE"} MIME = {"TJPEGIMAGE": "image/jpeg", "TBITMAP": "image/bmp", "TICON": "image/x-icon", "TMETAFILE": ""} NEW_NAME = "새로운 그림코드" def _ext(filename: str) -> str: i = filename.rfind(".") return filename[i:].upper() if i >= 0 else "" def kind_of_file(filename: str) -> str: k = KIND.get(_ext(filename)) if not k: raise ValueError("그림 파일은 .BMP · .JPG · .JPEG · .ICO · .EMF · .WMF 만 가능") return k def sniff(data: bytes) -> str: """바이트 머리로 본 꼴(보기용 · SIZE 칸이 비었을 때).""" if data[:3] == b"\xff\xd8\xff": return "TJPEGIMAGE" if data[:2] == b"BM": return "TBITMAP" if data[:4] == b"\x00\x00\x01\x00": return "TICON" if data[:4] in (b"\x01\x00\x00\x00", b"\xd7\xcd\xc6\x9a"): return "TMETAFILE" return "" def bmp_to_jpeg(data: bytes, quality: int = 90) -> bytes: from PIL import Image im = Image.open(io.BytesIO(data)).convert("RGB") out = io.BytesIO() im.save(out, "JPEG", quality=quality) return out.getvalue() def _tables(files: dict) -> tuple[DbfTable, SixMemo]: if "COSTN.DBF" not in files: raise ValueError("COSTN.DBF 없음") t = DbfTable(files["COSTN.DBF"]) if "IFORM" not in t.fields: raise ValueError("COSTN 에 IFORM 칸 없음") return t, SixMemo(files.get("COSTN.SMT")) def list_images(files: dict) -> list[dict]: """I 코드 행 — 코드 · 이름 · 꼴(SIZE) · 그림 크기.""" t, memo = _tables(files) out = [] for i in range(len(t)): code = t.get(i, "CODE") if t.deleted(i) or not code.startswith("I"): continue data = memo.read(t.raw(i, "IFORM")) out.append({"CODE": code, "NAME": t.get(i, "NAME"), "SIZE": t.get(i, "SIZE"), "bytes": len(data)}) return out def get_image(files: dict, code: str) -> tuple[bytes, str, str]: """(그림 바이트, 꼴, mime) — 꼴 = SIZE 대문자(길이 > 2) 아니면 바이트 머리.""" t, memo = _tables(files) i = find_rows(t, "CODE").get(code.strip()) if i is None: raise KeyError(f"코드 없음: {code}") data = memo.read(t.raw(i, "IFORM")) size = t.get(i, "SIZE").upper() kind = size if len(size) > 2 else sniff(data) return data, kind, MIME.get(kind, "") def put_image(files: dict, code: str, data: bytes, filename: str, to_jpeg: bool = False) -> tuple[dict, dict]: """그림 넣기·그림저장 — (바뀐 files 두 개, 보고) · 없는 I 코드는 새로 등록(이름 「새로운 그림코드」).""" code = code.strip().upper() if not code.startswith("I") or len(code) > 6: raise ValueError("그림 코드는 I 로 시작하는 6자 이내") kind = kind_of_file(filename) if to_jpeg: if kind != "TBITMAP": raise ValueError("JPEG 변환은 비트맵 파일만") data, kind = bmp_to_jpeg(data), "TJPEGIMAGE" t, memo = _tables(files) rows = find_rows(t, "CODE") new = code not in rows i = t.insert(len(t.recs), {"CODE": code, "NAME": NEW_NAME}) if new else rows[code] if not t.get(i, "NAME").strip(): stem = filename.rsplit("/", 1)[-1].rsplit("\\", 1)[-1] t.set(i, "NAME", stem[: len(stem) - len(_ext(stem))]) t.set(i, "SIZE", kind) ptr = t.raw(i, "IFORM") t.set_raw(i, "IFORM", memo.write(ptr if ptr.strip(b" \x00") else BLANK_PTR, data)) out = {"COSTN.DBF": t.to_bytes(), "COSTN.SMT": memo.to_bytes()} return out, {"code": code, "new": new, "kind": kind, "bytes": len(data), "name": t.get(i, "NAME")}