498 lines
21 KiB
Python
498 lines
21 KiB
Python
"""
|
|
Mingle Studio Twitter Card Generator
|
|
Clean white theme, text-focused, NanumGothic
|
|
5 square 1080x1080 promotional cards
|
|
"""
|
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
import os
|
|
|
|
W, H = 1080, 1080
|
|
FONT_DIR = ".claude/skills/canvas-design/canvas-fonts"
|
|
USER_FONTS = "C:/Users/qscft/AppData/Local/Microsoft/Windows/Fonts"
|
|
KR_XBOLD = os.path.join(USER_FONTS, "NanumGothicExtraBold.ttf")
|
|
KR_BOLD = os.path.join(USER_FONTS, "NanumGothicBold.ttf")
|
|
KR_REG = os.path.join(USER_FONTS, "NanumGothic.ttf")
|
|
KR_LIGHT = os.path.join(USER_FONTS, "NanumGothicLight.ttf")
|
|
EN_BOLD = os.path.join(FONT_DIR, "Outfit-Bold.ttf")
|
|
EN_REG = os.path.join(FONT_DIR, "Outfit-Regular.ttf")
|
|
MONO = os.path.join(FONT_DIR, "GeistMono-Regular.ttf")
|
|
KR_ROUND = "C:/Windows/Fonts/malgunbd.ttf"
|
|
LOGO_PATH = "images/logo/mingle-logo.webp"
|
|
OUTPUT_DIR = "twitter_cards"
|
|
|
|
# Colors
|
|
BG = (250, 250, 250)
|
|
WHITE = (255, 255, 255)
|
|
BLACK = (30, 30, 30)
|
|
DARK = (50, 50, 50)
|
|
GRAY = (120, 120, 120)
|
|
LIGHT_GRAY = (200, 200, 200)
|
|
VERY_LIGHT = (235, 235, 235)
|
|
ORANGE = (255, 136, 0)
|
|
ORANGE_LIGHT = (255, 243, 228)
|
|
PURPLE = (108, 92, 231)
|
|
PURPLE_LIGHT = (237, 233, 255)
|
|
|
|
BIZ_EMAIL = "minglestudio@minglestudio.co.kr"
|
|
|
|
# Layout constants
|
|
MARGIN = 70
|
|
NOTE_Y = 940 # price note y — right above footer
|
|
FOOTER_Y = H - 80 # footer separator line
|
|
|
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
|
|
|
|
def load_fonts():
|
|
fonts = {}
|
|
sizes = {
|
|
'title': 52, 'subtitle': 30, 'price': 64, 'price_sm': 42,
|
|
'body': 23, 'body_sm': 19, 'label': 16, 'label_sm': 14,
|
|
'pct': 46, 'big_pct': 68, 'num_accent': 130,
|
|
}
|
|
for name, size in sizes.items():
|
|
fonts[f'kr_xb_{name}'] = ImageFont.truetype(KR_XBOLD, size)
|
|
fonts[f'kr_{name}'] = ImageFont.truetype(KR_BOLD, size)
|
|
fonts[f'kr_r_{name}'] = ImageFont.truetype(KR_REG, size)
|
|
fonts[f'kr_l_{name}'] = ImageFont.truetype(KR_LIGHT, size)
|
|
fonts[f'en_{name}'] = ImageFont.truetype(EN_BOLD, size)
|
|
fonts[f'en_r_{name}'] = ImageFont.truetype(EN_REG, size)
|
|
fonts[f'mono_{name}'] = ImageFont.truetype(MONO, size)
|
|
fonts[f'round_{name}'] = ImageFont.truetype(KR_ROUND, size)
|
|
return fonts
|
|
|
|
|
|
def load_logo(size=40):
|
|
logo = Image.open(LOGO_PATH).convert("RGBA")
|
|
logo = logo.resize((size, size), Image.LANCZOS)
|
|
return logo
|
|
|
|
|
|
# ── Common drawing helpers ──
|
|
|
|
def draw_bottom_bar(img, draw, fonts):
|
|
draw.line([(MARGIN, FOOTER_Y), (W - MARGIN, FOOTER_Y)], fill=LIGHT_GRAY, width=1)
|
|
logo = load_logo(32)
|
|
img.paste(logo, (MARGIN, FOOTER_Y + 14), logo)
|
|
draw.text((MARGIN + 40, FOOTER_Y + 16), "Mingle Studio", fill=GRAY, font=fonts['en_r_label'])
|
|
draw.text((MARGIN + 40, FOOTER_Y + 34), BIZ_EMAIL, fill=GRAY, font=fonts['mono_label_sm'])
|
|
|
|
|
|
def draw_orange_bar(draw):
|
|
draw.rectangle([0, 0, W, 8], fill=ORANGE)
|
|
|
|
|
|
def text_vcenter(draw, x, cy, text, font, fill):
|
|
"""Draw text vertically centered at cy."""
|
|
bbox = draw.textbbox((0, 0), text, font=font)
|
|
th = bbox[3] - bbox[1]
|
|
top_off = bbox[1]
|
|
draw.text((x, cy - th // 2 - top_off), text, fill=fill, font=font)
|
|
return bbox[2] - bbox[0], th
|
|
|
|
|
|
def draw_tag(draw, x, y, text, font, bg=ORANGE, fg=WHITE):
|
|
"""Tag badge at (x, y) top-left."""
|
|
bbox = draw.textbbox((0, 0), text, font=font)
|
|
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
t_off = bbox[1]
|
|
px, py = 12, 6
|
|
bw, bh = tw + px * 2, th + py * 2
|
|
draw.rounded_rectangle([x, y, x + bw, y + bh], radius=6, fill=bg)
|
|
draw.text((x + px, y + py - t_off), text, fill=fg, font=font)
|
|
return bw, bh
|
|
|
|
|
|
def draw_tag_vcenter(draw, x, cy, text, font, bg=ORANGE, fg=WHITE):
|
|
"""Tag badge vertically centered at cy."""
|
|
bbox = draw.textbbox((0, 0), text, font=font)
|
|
th = bbox[3] - bbox[1]
|
|
bh = th + 12
|
|
return draw_tag(draw, x, cy - bh // 2, text, font, bg, fg)
|
|
|
|
|
|
def draw_bullet(draw, x, cy, color=ORANGE, r=5):
|
|
"""Bullet dot centered at cy."""
|
|
draw.ellipse([x, cy - r, x + r * 2, cy + r], fill=color)
|
|
|
|
|
|
def draw_bullet_at_text(draw, bx, tx, ty, text, font, color=ORANGE, r=5):
|
|
"""Draw bullet aligned to vertical center of text at (tx, ty)."""
|
|
bbox = draw.textbbox((0, 0), text, font=font)
|
|
th = bbox[3] - bbox[1]
|
|
top_off = bbox[1]
|
|
text_cy = ty + top_off + th // 2
|
|
draw.ellipse([bx, text_cy - r, bx + r * 2, text_cy + r], fill=color)
|
|
|
|
|
|
def draw_price_note(draw, fonts):
|
|
draw.text((MARGIN, NOTE_Y), "* 상기 비용은 변동될 수 있습니다", fill=GRAY, font=fonts['kr_r_label'])
|
|
|
|
|
|
def draw_arrow(draw, x, cy, color=ORANGE, size=6):
|
|
"""Small right-pointing triangle arrow centered at cy."""
|
|
draw.polygon([(x, cy - size), (x + size, cy), (x, cy + size)], fill=color)
|
|
|
|
|
|
def draw_header(draw, fonts, cat_label, title, underline_w, accent_num,
|
|
cat_color=ORANGE, line_color=ORANGE):
|
|
"""Common header block. Returns y after underline."""
|
|
draw.text((MARGIN, 45), cat_label, fill=cat_color, font=fonts['mono_label'])
|
|
draw.text((MARGIN, 80), title, fill=BLACK, font=fonts['kr_xb_title'])
|
|
draw.line([(MARGIN, 143), (MARGIN + underline_w, 143)], fill=line_color, width=3)
|
|
# Number accent
|
|
draw.text((W - 220, 65), accent_num, fill=(242, 242, 242),
|
|
font=fonts['en_num_accent'])
|
|
return 143
|
|
|
|
|
|
def draw_section_header(draw, x1, x2, y, h, text, font, bg_color,
|
|
tag_text=None, tag_font=None):
|
|
"""Section card with colored header bar. Returns (card_top, header_bottom)."""
|
|
BAR_H = 52
|
|
draw.rounded_rectangle([x1, y, x2, y + h], radius=14, fill=WHITE, outline=bg_color, width=2)
|
|
draw.rounded_rectangle([x1, y, x2, y + BAR_H], radius=14, fill=bg_color)
|
|
draw.rectangle([x1, y + 28, x2, y + BAR_H], fill=bg_color)
|
|
# Vertically center text in header bar (nudge +1 to compensate rounded top)
|
|
bar_cy = y + BAR_H // 2 + 1
|
|
text_vcenter(draw, x1 + 20, bar_cy, text, font, WHITE)
|
|
if tag_text and tag_font:
|
|
draw_tag_vcenter(draw, x1 + 220, bar_cy, tag_text, tag_font, WHITE, bg_color)
|
|
return y, y + BAR_H
|
|
|
|
|
|
# ═══════════════════════════════════════
|
|
# Card 1: 모션캡처 녹화
|
|
# ═══════════════════════════════════════
|
|
def create_card1(fonts):
|
|
img = Image.new('RGB', (W, H), BG)
|
|
draw = ImageDraw.Draw(img)
|
|
draw_orange_bar(draw)
|
|
draw_header(draw, fonts, "SERVICE 01", "모션캡처 녹화", 310, "01")
|
|
|
|
# Price — "원~ / 시간" right after the number on same line
|
|
draw.text((MARGIN, 180), "200,000", fill=ORANGE, font=fonts['en_price'])
|
|
bbox_price = draw.textbbox((MARGIN, 180), "200,000", font=fonts['en_price'])
|
|
draw.text((bbox_price[2] + 8, bbox_price[3] - 34), "원~ / 시간 (2인 기준)", fill=BLACK, font=fonts['kr_r_subtitle'])
|
|
draw.text((MARGIN, 255), "VAT 별도 | 추가 인원 +100,000원~/명/시간", fill=GRAY, font=fonts['kr_r_body_sm'])
|
|
|
|
# Features
|
|
features = [
|
|
("OptiTrack 30대 카메라", "정밀한 모션 트래킹"),
|
|
("전신 / 페이셜 캡처", "풀바디 + 표정 동시 캡처"),
|
|
("8 x 7m 전용 캡처 공간", "넓은 촬영 볼륨"),
|
|
]
|
|
CARD_H = 92
|
|
CARD_GAP = 18
|
|
start_y = 380
|
|
for i, (title, desc) in enumerate(features):
|
|
y = start_y + i * (CARD_H + CARD_GAP)
|
|
draw.rounded_rectangle([MARGIN, y, W - MARGIN, y + CARD_H], radius=12,
|
|
fill=WHITE, outline=VERY_LIGHT, width=1)
|
|
title_y = y + 16
|
|
draw_bullet_at_text(draw, MARGIN + 25, MARGIN + 50, title_y, title, fonts['kr_body'])
|
|
draw.text((MARGIN + 50, title_y), title, fill=BLACK, font=fonts['kr_body'])
|
|
draw.text((MARGIN + 50, y + 50), desc, fill=GRAY, font=fonts['kr_r_body_sm'])
|
|
|
|
draw_price_note(draw, fonts)
|
|
draw_bottom_bar(img, draw, fonts)
|
|
img.save(os.path.join(OUTPUT_DIR, "card1_mocap.png"), quality=95)
|
|
print(" Card 1 done")
|
|
|
|
|
|
# ═══════════════════════════════════════
|
|
# Card 2: 라이브 방송
|
|
# ═══════════════════════════════════════
|
|
def create_card2(fonts):
|
|
img = Image.new('RGB', (W, H), BG)
|
|
draw = ImageDraw.Draw(img)
|
|
draw_orange_bar(draw)
|
|
draw_header(draw, fonts, "SERVICE 02", "모션캡처 라이브 방송", 480, "02")
|
|
draw.text((MARGIN, 162), "실시간 모션캡처 방송 풀패키지", fill=GRAY, font=fonts['kr_r_body_sm'])
|
|
|
|
# 4h package (BEST)
|
|
by = 210
|
|
PKG_H = 105
|
|
draw.rounded_rectangle([MARGIN, by, W - MARGIN, by + PKG_H], radius=14,
|
|
fill=ORANGE_LIGHT, outline=ORANGE, width=2)
|
|
draw.text((95, by + 12), "4시간 패키지", fill=DARK, font=fonts['kr_r_body_sm'])
|
|
draw_tag(draw, 260, by + 10, "BEST", fonts['mono_label_sm'])
|
|
draw.text((95, by + 40), "1,400,000", fill=ORANGE, font=fonts['en_price_sm'])
|
|
# "원 (VAT 별도)" right after the number
|
|
bbox_price = draw.textbbox((95, by + 40), "1,400,000", font=fonts['en_price_sm'])
|
|
draw.text((bbox_price[2] + 8, by + 56), "원 (VAT 별도)", fill=GRAY, font=fonts['kr_r_body_sm'])
|
|
|
|
# 6h package
|
|
by2 = by + PKG_H + 20
|
|
draw.rounded_rectangle([MARGIN, by2, W - MARGIN, by2 + PKG_H], radius=14,
|
|
fill=WHITE, outline=VERY_LIGHT, width=1)
|
|
draw.text((95, by2 + 12), "6시간 패키지", fill=GRAY, font=fonts['kr_r_body_sm'])
|
|
draw.text((95, by2 + 40), "2,000,000", fill=ORANGE, font=fonts['en_price_sm'])
|
|
bbox_price2 = draw.textbbox((95, by2 + 40), "2,000,000", font=fonts['en_price_sm'])
|
|
draw.text((bbox_price2[2] + 8, by2 + 56), "원 (VAT 별도)", fill=GRAY, font=fonts['kr_r_body_sm'])
|
|
|
|
# Features section
|
|
fy = by2 + PKG_H + 30
|
|
feats = [
|
|
"아바타 · 배경 세팅 포함",
|
|
"최대 5인 동시 방송",
|
|
"보유 프랍 무제한 무료 제공",
|
|
"신규 프랍 최대 6개 제공",
|
|
]
|
|
feat_item_h = 40
|
|
feat_box_h = 56 + len(feats) * feat_item_h + 20
|
|
draw.rounded_rectangle([MARGIN, fy, W - MARGIN, fy + feat_box_h], radius=14,
|
|
fill=WHITE, outline=VERY_LIGHT, width=1)
|
|
draw.text((95, fy + 18), "포함 사항", fill=BLACK, font=fonts['kr_body'])
|
|
draw.line([(95, fy + 50), (W - 95, fy + 50)], fill=VERY_LIGHT, width=1)
|
|
for i, f in enumerate(feats):
|
|
y = fy + 66 + i * feat_item_h
|
|
draw_bullet_at_text(draw, 110, 130, y, f, fonts['kr_r_body_sm'])
|
|
draw.text((130, y), f, fill=DARK, font=fonts['kr_r_body_sm'])
|
|
|
|
draw_price_note(draw, fonts)
|
|
draw_bottom_bar(img, draw, fonts)
|
|
img.save(os.path.join(OUTPUT_DIR, "card2_streamingle.png"), quality=95)
|
|
print(" Card 2 done")
|
|
|
|
|
|
# ═══════════════════════════════════════
|
|
# Card 3: 뮤직비디오 제작
|
|
# ═══════════════════════════════════════
|
|
def create_card3(fonts):
|
|
img = Image.new('RGB', (W, H), BG)
|
|
draw = ImageDraw.Draw(img)
|
|
draw_orange_bar(draw)
|
|
draw_header(draw, fonts, "SERVICE 03", "뮤직비디오 제작", 350, "03")
|
|
|
|
# Price range
|
|
draw.text((MARGIN, 180), "2,000,000", fill=ORANGE, font=fonts['en_price'])
|
|
draw.text((MARGIN, 255), "~ 4,000,000원 (VAT 별도)", fill=BLACK, font=fonts['kr_r_subtitle'])
|
|
|
|
# Features
|
|
features = [
|
|
("3D 모션캡처 뮤직비디오", "고퀄리티 3D 아바타 MV 제작"),
|
|
("숏폼 댄스 챌린지", "틱톡 · 유튜브 숏츠 · 릴스"),
|
|
("기획부터 완성까지", "풀 프로덕션 지원"),
|
|
("사전 협의 필수", "기획서 및 준비물 사전 조율"),
|
|
]
|
|
CARD_H = 88
|
|
CARD_GAP = 16
|
|
start_y = 340
|
|
for i, (title, desc) in enumerate(features):
|
|
y = start_y + i * (CARD_H + CARD_GAP)
|
|
draw.rounded_rectangle([MARGIN, y, W - MARGIN, y + CARD_H], radius=12,
|
|
fill=WHITE, outline=VERY_LIGHT, width=1)
|
|
title_y = y + 14
|
|
draw_bullet_at_text(draw, MARGIN + 25, MARGIN + 50, title_y, title, fonts['kr_body'])
|
|
draw.text((MARGIN + 50, title_y), title, fill=BLACK, font=fonts['kr_body'])
|
|
draw.text((MARGIN + 50, y + 48), desc, fill=GRAY, font=fonts['kr_r_body_sm'])
|
|
|
|
draw_price_note(draw, fonts)
|
|
draw_bottom_bar(img, draw, fonts)
|
|
img.save(os.path.join(OUTPUT_DIR, "card3_mv.png"), quality=95)
|
|
print(" Card 3 done")
|
|
|
|
|
|
# ═══════════════════════════════════════
|
|
# Card 4: 할인 혜택
|
|
# ═══════════════════════════════════════
|
|
def create_card4(fonts):
|
|
img = Image.new('RGB', (W, H), BG)
|
|
draw = ImageDraw.Draw(img)
|
|
draw_orange_bar(draw)
|
|
|
|
draw.text((MARGIN, 45), "BENEFITS", fill=ORANGE, font=fonts['mono_label'])
|
|
draw.text((MARGIN, 80), "할인 혜택", fill=BLACK, font=fonts['kr_xb_title'])
|
|
draw.line([(MARGIN, 143), (MARGIN + 210, 143)], fill=ORANGE, width=3)
|
|
draw.text((MARGIN, 160), "라이브 방송 4시간 패키지 기준 | VAT 별도", fill=GRAY, font=fonts['kr_r_body_sm'])
|
|
|
|
# ── Referral section ──
|
|
ry = 210
|
|
BAR_H = 52
|
|
REF_H = 200
|
|
draw_section_header(draw, MARGIN, W - MARGIN, ry, REF_H,
|
|
"추천인 할인", fonts['kr_subtitle'], ORANGE,
|
|
"신규 고객 대상", fonts['kr_r_label_sm'])
|
|
|
|
# Content area center
|
|
content_cy = ry + BAR_H + (REF_H - BAR_H) // 2 # exact vertical center of content area
|
|
|
|
# 20% — vertically centered
|
|
text_vcenter(draw, 95, content_cy, "20", fonts['en_big_pct'], ORANGE)
|
|
text_vcenter(draw, 193, content_cy + 6, "%", fonts['en_r_subtitle'], ORANGE)
|
|
|
|
# Description — 3 lines as a block, centered at content_cy
|
|
desc_lines = [
|
|
("추천인 & 신규고객 모두 할인", fonts['kr_r_body'], BLACK),
|
|
("추천 횟수 제한 없이 누적 가능", fonts['kr_r_body_sm'], GRAY),
|
|
("첫 예약 시 추천인을 알려주세요", fonts['kr_r_body_sm'], GRAY),
|
|
]
|
|
line_gap = 30
|
|
total_desc_h = line_gap * (len(desc_lines) - 1)
|
|
desc_start_cy = content_cy - total_desc_h // 2
|
|
for i, (txt, fnt, clr) in enumerate(desc_lines):
|
|
text_vcenter(draw, 270, desc_start_cy + i * line_gap, txt, fnt, clr)
|
|
|
|
# ── Multi-pass section ──
|
|
my = ry + REF_H + 20
|
|
MULTI_H = 330
|
|
draw_section_header(draw, MARGIN, W - MARGIN, my, MULTI_H,
|
|
"다회권 할인", fonts['kr_subtitle'], PURPLE,
|
|
"최대 30%", fonts['kr_r_label_sm'])
|
|
|
|
# Table header — centered in available space
|
|
col_header_cy = my + BAR_H + 18
|
|
text_vcenter(draw, 130, col_header_cy, "이용권", fonts['kr_r_body_sm'], GRAY)
|
|
text_vcenter(draw, 460, col_header_cy, "할인율", fonts['kr_r_body_sm'], GRAY)
|
|
draw.line([(90, col_header_cy + 14), (W - 90, col_header_cy + 14)], fill=VERY_LIGHT, width=1)
|
|
|
|
# Table rows — evenly distribute in space between header line and note
|
|
table_top = col_header_cy + 22
|
|
table_bottom = my + MULTI_H - 36
|
|
row_area = table_bottom - table_top
|
|
ROW_H = row_area // 3
|
|
rows = [("3회권", "20%", False), ("5회권", "25%", False), ("7회권", "30%", True)]
|
|
for i, (name, rate, best) in enumerate(rows):
|
|
rowy = table_top + i * ROW_H
|
|
cy = rowy + ROW_H // 2
|
|
if best:
|
|
draw.rounded_rectangle([85, rowy + 4, W - 85, rowy + ROW_H - 4],
|
|
radius=8, fill=PURPLE_LIGHT)
|
|
text_vcenter(draw, 130, cy, name, fonts['kr_r_body'], BLACK)
|
|
text_vcenter(draw, 440, cy, rate, fonts['en_pct'], PURPLE)
|
|
if best:
|
|
draw_tag_vcenter(draw, 550, cy, "BEST", fonts['mono_label_sm'], PURPLE)
|
|
if i < 2:
|
|
draw.line([(90, rowy + ROW_H), (W - 90, rowy + ROW_H)],
|
|
fill=(242, 242, 242), width=1)
|
|
|
|
draw.text((90, my + MULTI_H - 30), "선결제 시 할인 적용 | 3개월 이내 소진",
|
|
fill=GRAY, font=fonts['kr_r_body_sm'])
|
|
|
|
draw_price_note(draw, fonts)
|
|
draw_bottom_bar(img, draw, fonts)
|
|
img.save(os.path.join(OUTPUT_DIR, "card4_discount.png"), quality=95)
|
|
print(" Card 4 done")
|
|
|
|
|
|
# ═══════════════════════════════════════
|
|
# Card 5: 기업 전용 정기권
|
|
# ═══════════════════════════════════════
|
|
def create_card5(fonts):
|
|
img = Image.new('RGB', (W, H), BG)
|
|
draw = ImageDraw.Draw(img)
|
|
draw_orange_bar(draw)
|
|
|
|
draw.text((MARGIN, 45), "BUSINESS", fill=PURPLE, font=fonts['mono_label'])
|
|
draw.text((MARGIN, 80), "기업 전용 정기권(3개월)", fill=BLACK, font=fonts['kr_xb_title'])
|
|
draw.line([(MARGIN, 143), (MARGIN + 530, 143)], fill=PURPLE, width=3)
|
|
draw_tag(draw, MARGIN, 160, "MCN · 기업 고객", fonts['kr_r_label'], PURPLE)
|
|
|
|
# ── Pricing table ──
|
|
BAR_H = 52
|
|
ty = 215
|
|
TABLE_H = 220
|
|
draw_section_header(draw, MARGIN, W - MARGIN, ty, TABLE_H,
|
|
"기업 전용 다회권", fonts['kr_subtitle'], PURPLE)
|
|
|
|
# Column headers — centered in header zone
|
|
col_cy = ty + BAR_H + 18
|
|
text_vcenter(draw, 120, col_cy, "이용권", fonts['kr_r_body_sm'], GRAY)
|
|
text_vcenter(draw, 350, col_cy, "할인율", fonts['kr_r_body_sm'], GRAY)
|
|
text_vcenter(draw, 590, col_cy, "회당 가격", fonts['kr_r_body_sm'], GRAY)
|
|
draw.line([(90, col_cy + 14), (W - 90, col_cy + 14)], fill=VERY_LIGHT, width=1)
|
|
|
|
# Rows — evenly distribute in remaining space
|
|
table_top = col_cy + 22
|
|
table_bottom = ty + TABLE_H - 10
|
|
row_area = table_bottom - table_top
|
|
ROW_H = row_area // 2
|
|
corp_rows = [
|
|
("4회권", "25%", "1,050,000원", False),
|
|
("6회권", "30%", "980,000원", True),
|
|
]
|
|
for i, (name, rate, price, best) in enumerate(corp_rows):
|
|
rowy = table_top + i * ROW_H
|
|
cy = rowy + ROW_H // 2
|
|
if best:
|
|
draw.rounded_rectangle([85, rowy + 4, W - 85, rowy + ROW_H - 4],
|
|
radius=8, fill=PURPLE_LIGHT)
|
|
text_vcenter(draw, 120, cy, name, fonts['kr_r_body'], BLACK)
|
|
text_vcenter(draw, 330, cy, rate, fonts['en_pct'], PURPLE)
|
|
text_vcenter(draw, 570, cy, price, fonts['kr_r_body_sm'], DARK)
|
|
if best:
|
|
draw_tag_vcenter(draw, 740, cy, "BEST", fonts['mono_label_sm'], PURPLE)
|
|
if i == 0:
|
|
draw.line([(90, rowy + ROW_H), (W - 90, rowy + ROW_H)],
|
|
fill=(242, 242, 242), width=1)
|
|
|
|
# ── Renewal benefit ──
|
|
rny = ty + TABLE_H + 25
|
|
RENEW_H = 300
|
|
draw_section_header(draw, MARGIN, W - MARGIN, rny, RENEW_H,
|
|
"갱신 혜택", fonts['kr_subtitle'], ORANGE)
|
|
|
|
# Flow diagram — bigger pills with body font
|
|
pill_font = fonts['kr_r_body'] # bigger font for readability
|
|
fy = rny + 62
|
|
PILL_H = 42
|
|
steps = ["다회권 구매", "3개월 내 소진", "동일 할인율 적용"]
|
|
pill_info = []
|
|
for text in steps:
|
|
bbox = draw.textbbox((0, 0), text, font=pill_font)
|
|
tw = bbox[2] - bbox[0]
|
|
pill_info.append((text, tw))
|
|
|
|
total_pill_w = sum(tw + 32 for _, tw in pill_info)
|
|
arrow_space = 32
|
|
total_w = total_pill_w + 2 * arrow_space
|
|
sx = (W - total_w) // 2
|
|
|
|
for j, (text, tw) in enumerate(pill_info):
|
|
pw = tw + 32
|
|
draw.rounded_rectangle([sx, fy, sx + pw, fy + PILL_H], radius=10,
|
|
fill=ORANGE_LIGHT, outline=ORANGE, width=2)
|
|
text_vcenter(draw, sx + 16, fy + PILL_H // 2, text, pill_font, DARK)
|
|
if j < 2:
|
|
draw_arrow(draw, sx + pw + 10, fy + PILL_H // 2, ORANGE, 7)
|
|
sx += pw + arrow_space
|
|
|
|
# Description text
|
|
ty2 = fy + PILL_H + 22
|
|
draw.text((95, ty2), "소진 후 추가 예약 시", fill=DARK, font=fonts['kr_r_body'])
|
|
draw.text((95, ty2 + 30), "직전 구매 할인율을 동일하게 적용합니다", fill=BLACK, font=fonts['kr_body'])
|
|
|
|
# Examples
|
|
ty3 = ty2 + 76
|
|
draw.text((95, ty3), "ex) 4회권(25%) 소진", fill=GRAY, font=fonts['kr_r_body_sm'])
|
|
draw_arrow(draw, 355, ty3 + 10, ORANGE, 5)
|
|
draw.text((375, ty3), "추가 예약도 25% 할인", fill=GRAY, font=fonts['kr_r_body_sm'])
|
|
|
|
draw.text((95, ty3 + 28), "ex) 6회권(30%) 소진", fill=GRAY, font=fonts['kr_r_body_sm'])
|
|
draw_arrow(draw, 355, ty3 + 38, ORANGE, 5)
|
|
draw.text((375, ty3 + 28), "추가 예약도 30% 할인", fill=GRAY, font=fonts['kr_r_body_sm'])
|
|
|
|
# Notes
|
|
draw.text((MARGIN, NOTE_Y - 20),
|
|
"선결제 · 3개월 이내 소진 의무 | 라이브 방송 4시간 패키지 기준 | VAT 별도",
|
|
fill=GRAY, font=fonts['kr_r_label'])
|
|
draw_price_note(draw, fonts)
|
|
|
|
draw_bottom_bar(img, draw, fonts)
|
|
img.save(os.path.join(OUTPUT_DIR, "card5_corporate.png"), quality=95)
|
|
print(" Card 5 done")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("Loading fonts...")
|
|
fonts = load_fonts()
|
|
print("Generating cards...")
|
|
create_card1(fonts)
|
|
create_card2(fonts)
|
|
create_card3(fonts)
|
|
create_card4(fonts)
|
|
create_card5(fonts)
|
|
print(f"Done! -> {OUTPUT_DIR}/")
|