- 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>
134 lines
5.1 KiB
Python
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"),
|
|
}
|