Files
Aislo/tools/stmate-test/img_code.py
T
eomsangdonandClaude Opus 5.5 47a04b6df8 feat(stmate-test): REA MCP(Ghidra) 파일 입출력 — JDL 탭 줄·묶음 내보내기·검사 · 구버전→Win8 변환 · 엑셀 역등록 구성 줄 · 자재 집계표 엑셀 · 그림 코드
- JDL 줄 = 탭 구분 + \x05/\x07 덧붙임(검사기 0x504d20 · 고정폭 표는 죽은 고리 — 34 §17.9 바로잡음) · 단가 목록 7종·Main·GongJong 머리·Global.Ini 상수 키 · zip 받기
- 구버전 판 가리기(Chk_Data 0x459280) · 7.x/DOS 표별 필드 대응 · B→X 조건 = 단위 X
- 엑셀 역등록 PRPT「속성」·PRIN「대상」·PROUT「저장」 = PR/RR 줄만 · 원본 BDQTY 4건 일치
- 자재 = 재료비 수량집계표 엑셀 내보내기(주요건설자재현황 시트)
- 그림 코드 = COSTN I 코드 IFORM · SIZE = 그림 클래스 이름
- 지식DB 37번

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 15:58:17 +09:00

134 lines
5.1 KiB
Python

# -*- 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"),
}