Deploy: exdev sync 2026-08-13 13:38:36
This commit is contained in:
Regular → Executable
+230
-230
@@ -1,230 +1,230 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
공식 길통이+차로(ai-mascot.png)로 Electron 앱 아이콘 생성
|
||||
- build/icon.png (1024)
|
||||
- build/icon.icns (macOS)
|
||||
- build/icon.ico (Windows)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import plistlib
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BUILD = ROOT / "build"
|
||||
SRC_CANDIDATES = [
|
||||
ROOT.parent / "chatbotApp-chatbot2.0-agent" / "web" / "public" / "images" / "ai-mascot.png",
|
||||
ROOT.parent / "chatbotApp-chatbot2.0-agent" / "public-ai" / "images" / "ai-mascot.png",
|
||||
]
|
||||
|
||||
# EX 브랜드 블루
|
||||
BG = (36, 107, 235, 255)
|
||||
WHITE = (255, 255, 255, 255)
|
||||
|
||||
|
||||
def _venv_python(venv: Path) -> Path:
|
||||
if sys.platform == "win32":
|
||||
return venv / "Scripts" / "python.exe"
|
||||
py = venv / "bin" / "python3"
|
||||
return py if py.exists() else venv / "bin" / "python"
|
||||
|
||||
|
||||
def _in_venv(venv: Path) -> bool:
|
||||
"""Homebrew venv의 bin/python은 시스템 바이너리 symlink라 executable.resolve() 비교는 불가."""
|
||||
try:
|
||||
return Path(sys.prefix).resolve() == venv.resolve()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _ensure_pip(py: Path) -> None:
|
||||
try:
|
||||
subprocess.check_call([str(py), "-m", "pip", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except subprocess.CalledProcessError:
|
||||
print("[build-icons] ensurepip으로 pip 부트스트랩…", flush=True)
|
||||
subprocess.check_call([str(py), "-m", "ensurepip", "--upgrade"])
|
||||
|
||||
|
||||
def _pip_install_pillow(py: Path) -> None:
|
||||
_ensure_pip(py)
|
||||
cmd = [str(py), "-m", "pip", "install", "Pillow", "-q"]
|
||||
try:
|
||||
subprocess.check_call(cmd)
|
||||
except subprocess.CalledProcessError:
|
||||
print("[build-icons] pip 재시도 (ensurepip 후)…", flush=True)
|
||||
subprocess.check_call([str(py), "-m", "ensurepip", "--upgrade"])
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
|
||||
def require_pillow():
|
||||
try:
|
||||
from PIL import Image, ImageDraw # noqa: F401
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
venv = ROOT / ".venv-icons"
|
||||
py = _venv_python(venv)
|
||||
if not py.exists():
|
||||
print("[build-icons] venv 생성 중…", flush=True)
|
||||
subprocess.check_call([sys.executable, "-m", "venv", str(venv)])
|
||||
py = _venv_python(venv)
|
||||
print("[build-icons] Pillow 설치 중…", flush=True)
|
||||
_pip_install_pillow(py)
|
||||
# 현재 프로세스에 Pillow가 없으면 venv로 재실행 (sys.prefix로 판별)
|
||||
if not _in_venv(venv):
|
||||
raise SystemExit(subprocess.call([str(py), str(Path(__file__).resolve()), *sys.argv[1:]]))
|
||||
from PIL import Image, ImageDraw # noqa: F401
|
||||
|
||||
|
||||
def rounded_rect_mask(size: int, radius: int):
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
mask = Image.new("L", (size, size), 0)
|
||||
draw = ImageDraw.Draw(mask)
|
||||
draw.rounded_rectangle((0, 0, size - 1, size - 1), radius=radius, fill=255)
|
||||
return mask
|
||||
|
||||
|
||||
def make_icon_png(src: Path, out: Path, size: int = 1024) -> None:
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
|
||||
mascot = Image.open(src).convert("RGBA")
|
||||
# 흰 배경 제거에 가깝게: 거의 흰 픽셀을 투명 처리
|
||||
pixels = mascot.load()
|
||||
w, h = mascot.size
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
r, g, b, a = pixels[x, y]
|
||||
if r > 245 and g > 245 and b > 245:
|
||||
pixels[x, y] = (r, g, b, 0)
|
||||
|
||||
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
# macOS 아이콘용 라운드 사각 (약 22% radius)
|
||||
radius = int(size * 0.223)
|
||||
draw.rounded_rectangle((0, 0, size - 1, size - 1), radius=radius, fill=BG)
|
||||
|
||||
# 안쪽 밝은 원형 배지
|
||||
badge = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
bd = ImageDraw.Draw(badge)
|
||||
pad = int(size * 0.08)
|
||||
bd.ellipse((pad, pad, size - pad, size - pad), fill=(255, 255, 255, 235))
|
||||
canvas = Image.alpha_composite(canvas, badge)
|
||||
|
||||
# 마스코트 배치 (여백 확보)
|
||||
max_side = int(size * 0.78)
|
||||
mw, mh = mascot.size
|
||||
scale = min(max_side / mw, max_side / mh)
|
||||
nw, nh = int(mw * scale), int(mh * scale)
|
||||
mascot = mascot.resize((nw, nh), Image.Resampling.LANCZOS)
|
||||
ox = (size - nw) // 2
|
||||
oy = (size - nh) // 2 + int(size * 0.02)
|
||||
canvas.alpha_composite(mascot, (ox, oy))
|
||||
|
||||
# 마스크로 모서리 정리
|
||||
mask = rounded_rect_mask(size, radius)
|
||||
out_img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
out_img.paste(canvas, (0, 0), mask=mask)
|
||||
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_img.save(out, "PNG")
|
||||
print(f"[build-icons] ✅ {out} ({size}x{size})")
|
||||
|
||||
|
||||
def build_icns(png_1024: Path, icns_path: Path) -> None:
|
||||
sizes = [
|
||||
(16, "icon_16x16.png"),
|
||||
(32, "diana.k@example.org"),
|
||||
(32, "icon_32x32.png"),
|
||||
(64, "ivan.p@example.net"),
|
||||
(128, "icon_128x128.png"),
|
||||
(256, "wendy.h@example.net"),
|
||||
(256, "icon_256x256.png"),
|
||||
(512, "wendy.h@example.net"),
|
||||
(512, "icon_512x512.png"),
|
||||
(1024, "walt.e@example.net"),
|
||||
]
|
||||
iconset = BUILD / "icon.iconset"
|
||||
if iconset.exists():
|
||||
shutil.rmtree(iconset)
|
||||
iconset.mkdir(parents=True)
|
||||
|
||||
from PIL import Image
|
||||
|
||||
base = Image.open(png_1024).convert("RGBA")
|
||||
for px, name in sizes:
|
||||
img = base.resize((px, px), Image.Resampling.LANCZOS)
|
||||
img.save(iconset / name, "PNG")
|
||||
|
||||
if icns_path.exists():
|
||||
icns_path.unlink()
|
||||
subprocess.check_call(["iconutil", "-c", "icns", str(iconset), "-o", str(icns_path)])
|
||||
shutil.rmtree(iconset)
|
||||
print(f"[build-icons] ✅ {icns_path}")
|
||||
|
||||
|
||||
def _png_chunk(tag: bytes, data: bytes) -> bytes:
|
||||
return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
|
||||
|
||||
|
||||
def rgba_to_png_bytes(img) -> bytes:
|
||||
"""Pillow 이미지 → PNG bytes (ICO용)."""
|
||||
import io
|
||||
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def build_ico(png_1024: Path, ico_path: Path) -> None:
|
||||
"""여러 해상도 PNG를 담은 .ico 생성 (electron-builder는 256 필수)."""
|
||||
from PIL import Image
|
||||
|
||||
sizes = [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]
|
||||
base = Image.open(png_1024).convert("RGBA")
|
||||
# Pillow: 원본에서 sizes= 로 리사이즈한 멀티 엔트리 ICO 생성
|
||||
# (append_images + 작은 첫 프레임은 16x16만 남는 버그 회피)
|
||||
base.save(ico_path, format="ICO", sizes=sizes)
|
||||
print(f"[build-icons] ✅ {ico_path}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
require_pillow()
|
||||
src = next((p for p in SRC_CANDIDATES if p.exists()), None)
|
||||
if not src:
|
||||
print("[build-icons] ai-mascot.png 없음", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
BUILD.mkdir(parents=True, exist_ok=True)
|
||||
png = BUILD / "icon.png"
|
||||
make_icon_png(src, png, 1024)
|
||||
# electron-builder 호환 별칭
|
||||
shutil.copy2(png, BUILD / "icon-1024.png")
|
||||
|
||||
if sys.platform == "darwin":
|
||||
try:
|
||||
build_icns(png, BUILD / "icon.icns")
|
||||
except Exception as e:
|
||||
print(f"[build-icons] icns 실패 (png만 사용): {e}", file=sys.stderr)
|
||||
else:
|
||||
print("[build-icons] icns는 macOS에서만 생성합니다")
|
||||
|
||||
try:
|
||||
build_ico(png, BUILD / "icon.ico")
|
||||
except Exception as e:
|
||||
print(f"[build-icons] ico 실패: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print("[build-icons] 완료")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
공식 길통이+차로(ai-mascot.png)로 Electron 앱 아이콘 생성
|
||||
- build/icon.png (1024)
|
||||
- build/icon.icns (macOS)
|
||||
- build/icon.ico (Windows)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import plistlib
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BUILD = ROOT / "build"
|
||||
SRC_CANDIDATES = [
|
||||
ROOT.parent / "chatbotApp-chatbot2.0-agent" / "web" / "public" / "images" / "ai-mascot.png",
|
||||
ROOT.parent / "chatbotApp-chatbot2.0-agent" / "public-ai" / "images" / "ai-mascot.png",
|
||||
]
|
||||
|
||||
# EX 브랜드 블루
|
||||
BG = (36, 107, 235, 255)
|
||||
WHITE = (255, 255, 255, 255)
|
||||
|
||||
|
||||
def _venv_python(venv: Path) -> Path:
|
||||
if sys.platform == "win32":
|
||||
return venv / "Scripts" / "python.exe"
|
||||
py = venv / "bin" / "python3"
|
||||
return py if py.exists() else venv / "bin" / "python"
|
||||
|
||||
|
||||
def _in_venv(venv: Path) -> bool:
|
||||
"""Homebrew venv의 bin/python은 시스템 바이너리 symlink라 executable.resolve() 비교는 불가."""
|
||||
try:
|
||||
return Path(sys.prefix).resolve() == venv.resolve()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _ensure_pip(py: Path) -> None:
|
||||
try:
|
||||
subprocess.check_call([str(py), "-m", "pip", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except subprocess.CalledProcessError:
|
||||
print("[build-icons] ensurepip으로 pip 부트스트랩…", flush=True)
|
||||
subprocess.check_call([str(py), "-m", "ensurepip", "--upgrade"])
|
||||
|
||||
|
||||
def _pip_install_pillow(py: Path) -> None:
|
||||
_ensure_pip(py)
|
||||
cmd = [str(py), "-m", "pip", "install", "Pillow", "-q"]
|
||||
try:
|
||||
subprocess.check_call(cmd)
|
||||
except subprocess.CalledProcessError:
|
||||
print("[build-icons] pip 재시도 (ensurepip 후)…", flush=True)
|
||||
subprocess.check_call([str(py), "-m", "ensurepip", "--upgrade"])
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
|
||||
def require_pillow():
|
||||
try:
|
||||
from PIL import Image, ImageDraw # noqa: F401
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
venv = ROOT / ".venv-icons"
|
||||
py = _venv_python(venv)
|
||||
if not py.exists():
|
||||
print("[build-icons] venv 생성 중…", flush=True)
|
||||
subprocess.check_call([sys.executable, "-m", "venv", str(venv)])
|
||||
py = _venv_python(venv)
|
||||
print("[build-icons] Pillow 설치 중…", flush=True)
|
||||
_pip_install_pillow(py)
|
||||
# 현재 프로세스에 Pillow가 없으면 venv로 재실행 (sys.prefix로 판별)
|
||||
if not _in_venv(venv):
|
||||
raise SystemExit(subprocess.call([str(py), str(Path(__file__).resolve()), *sys.argv[1:]]))
|
||||
from PIL import Image, ImageDraw # noqa: F401
|
||||
|
||||
|
||||
def rounded_rect_mask(size: int, radius: int):
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
mask = Image.new("L", (size, size), 0)
|
||||
draw = ImageDraw.Draw(mask)
|
||||
draw.rounded_rectangle((0, 0, size - 1, size - 1), radius=radius, fill=255)
|
||||
return mask
|
||||
|
||||
|
||||
def make_icon_png(src: Path, out: Path, size: int = 1024) -> None:
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
|
||||
mascot = Image.open(src).convert("RGBA")
|
||||
# 흰 배경 제거에 가깝게: 거의 흰 픽셀을 투명 처리
|
||||
pixels = mascot.load()
|
||||
w, h = mascot.size
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
r, g, b, a = pixels[x, y]
|
||||
if r > 245 and g > 245 and b > 245:
|
||||
pixels[x, y] = (r, g, b, 0)
|
||||
|
||||
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
# macOS 아이콘용 라운드 사각 (약 22% radius)
|
||||
radius = int(size * 0.223)
|
||||
draw.rounded_rectangle((0, 0, size - 1, size - 1), radius=radius, fill=BG)
|
||||
|
||||
# 안쪽 밝은 원형 배지
|
||||
badge = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
bd = ImageDraw.Draw(badge)
|
||||
pad = int(size * 0.08)
|
||||
bd.ellipse((pad, pad, size - pad, size - pad), fill=(255, 255, 255, 235))
|
||||
canvas = Image.alpha_composite(canvas, badge)
|
||||
|
||||
# 마스코트 배치 (여백 확보)
|
||||
max_side = int(size * 0.78)
|
||||
mw, mh = mascot.size
|
||||
scale = min(max_side / mw, max_side / mh)
|
||||
nw, nh = int(mw * scale), int(mh * scale)
|
||||
mascot = mascot.resize((nw, nh), Image.Resampling.LANCZOS)
|
||||
ox = (size - nw) // 2
|
||||
oy = (size - nh) // 2 + int(size * 0.02)
|
||||
canvas.alpha_composite(mascot, (ox, oy))
|
||||
|
||||
# 마스크로 모서리 정리
|
||||
mask = rounded_rect_mask(size, radius)
|
||||
out_img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
out_img.paste(canvas, (0, 0), mask=mask)
|
||||
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_img.save(out, "PNG")
|
||||
print(f"[build-icons] ✅ {out} ({size}x{size})")
|
||||
|
||||
|
||||
def build_icns(png_1024: Path, icns_path: Path) -> None:
|
||||
sizes = [
|
||||
(16, "icon_16x16.png"),
|
||||
(32, "diana.k@example.org"),
|
||||
(32, "icon_32x32.png"),
|
||||
(64, "ivan.p@example.net"),
|
||||
(128, "icon_128x128.png"),
|
||||
(256, "wendy.h@example.net"),
|
||||
(256, "icon_256x256.png"),
|
||||
(512, "wendy.h@example.net"),
|
||||
(512, "icon_512x512.png"),
|
||||
(1024, "walt.e@example.net"),
|
||||
]
|
||||
iconset = BUILD / "icon.iconset"
|
||||
if iconset.exists():
|
||||
shutil.rmtree(iconset)
|
||||
iconset.mkdir(parents=True)
|
||||
|
||||
from PIL import Image
|
||||
|
||||
base = Image.open(png_1024).convert("RGBA")
|
||||
for px, name in sizes:
|
||||
img = base.resize((px, px), Image.Resampling.LANCZOS)
|
||||
img.save(iconset / name, "PNG")
|
||||
|
||||
if icns_path.exists():
|
||||
icns_path.unlink()
|
||||
subprocess.check_call(["iconutil", "-c", "icns", str(iconset), "-o", str(icns_path)])
|
||||
shutil.rmtree(iconset)
|
||||
print(f"[build-icons] ✅ {icns_path}")
|
||||
|
||||
|
||||
def _png_chunk(tag: bytes, data: bytes) -> bytes:
|
||||
return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
|
||||
|
||||
|
||||
def rgba_to_png_bytes(img) -> bytes:
|
||||
"""Pillow 이미지 → PNG bytes (ICO용)."""
|
||||
import io
|
||||
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def build_ico(png_1024: Path, ico_path: Path) -> None:
|
||||
"""여러 해상도 PNG를 담은 .ico 생성 (electron-builder는 256 필수)."""
|
||||
from PIL import Image
|
||||
|
||||
sizes = [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]
|
||||
base = Image.open(png_1024).convert("RGBA")
|
||||
# Pillow: 원본에서 sizes= 로 리사이즈한 멀티 엔트리 ICO 생성
|
||||
# (append_images + 작은 첫 프레임은 16x16만 남는 버그 회피)
|
||||
base.save(ico_path, format="ICO", sizes=sizes)
|
||||
print(f"[build-icons] ✅ {ico_path}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
require_pillow()
|
||||
src = next((p for p in SRC_CANDIDATES if p.exists()), None)
|
||||
if not src:
|
||||
print("[build-icons] ai-mascot.png 없음", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
BUILD.mkdir(parents=True, exist_ok=True)
|
||||
png = BUILD / "icon.png"
|
||||
make_icon_png(src, png, 1024)
|
||||
# electron-builder 호환 별칭
|
||||
shutil.copy2(png, BUILD / "icon-1024.png")
|
||||
|
||||
if sys.platform == "darwin":
|
||||
try:
|
||||
build_icns(png, BUILD / "icon.icns")
|
||||
except Exception as e:
|
||||
print(f"[build-icons] icns 실패 (png만 사용): {e}", file=sys.stderr)
|
||||
else:
|
||||
print("[build-icons] icns는 macOS에서만 생성합니다")
|
||||
|
||||
try:
|
||||
build_ico(png, BUILD / "icon.ico")
|
||||
except Exception as e:
|
||||
print(f"[build-icons] ico 실패: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print("[build-icons] 완료")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
Reference in New Issue
Block a user