import os import subprocess import svgwrite # ========== 卡片基础尺寸 ========== CARD_W = 63.5 # 毫米(标准桥牌尺寸宽) CARD_H = 88.9 # 毫米 MM_TO_PX = 3.779527559 # 1mm ≈ 3.78px (96dpi) W_PX = int(CARD_W * MM_TO_PX) H_PX = int(CARD_H * MM_TO_PX) CORNER_RADIUS = 8 # 圆角半径(毫米) # 花色配置 SUIT_NAME = {'S': 'Spade', 'H': 'Heart', 'C': 'Club', 'D': 'Diamond'} SUIT_COLOR = {'S': '#000000', 'H': '#C00000', 'C': '#000000', 'D': '#C00000'} # 点数映射 RANK_NUM = { '2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9, '10':10, 'J':11, 'Q':12, 'K':13, 'A':1 } # ========== 辅助函数 ========== def mm_to_px(mm_val): """毫米转像素(96 DPI)""" return mm_val * MM_TO_PX def add_corner_indicators(dwg, rank, suit, x_off, y_off, scale=1.0, rotation=0): """角标:点数 + 花色字母,无特殊符号""" group = dwg.g(transform=f"translate({x_off},{y_off}) rotate({rotation})") rank_font_size = 12 * scale # 点数 group.add(dwg.text( rank, insert=(0, rank_font_size), font_size=f"{rank_font_size}pt", font_family="Arial, sans-serif", font_weight="bold", fill=SUIT_COLOR[suit] )) # 花色字母 S/H/C/D suit_char = suit suit_font_size = 10 * scale group.add(dwg.text( suit_char, insert=(0, rank_font_size + suit_font_size), font_size=f"{suit_font_size}pt", fill=SUIT_COLOR[suit] )) return group def draw_suit_symbol(dwg, suit, x, y, size_mm): """纯SVG图形绘制四种花色,彻底解决方框/乱码""" g = dwg.g(transform=f"translate({x}, {y})") s = size_mm / 2.0 color = SUIT_COLOR[suit] if suit == "H": # 红桃 path = f"M 0 {0.3*s} C {-s} {-0.3*s}, {-s} {-s}, 0 {-0.5*s} C {s} {-s}, {s} {-0.3*s}, 0 {0.3*s}" g.add(dwg.path(d=path, fill=color)) elif suit == "D": # 方块 pts = [(0, -s), (s, 0), (0, s), (-s, 0)] g.add(dwg.polygon(points=pts, fill=color)) elif suit == "S": # 黑桃 path = f"M 0 {-s} C {-s} 0, {-s} {0.6*s}, 0 {0.3*s} C {s} {0.6*s}, {s} 0, 0 {-s}" g.add(dwg.path(d=path, fill=color)) g.add(dwg.rect(insert=(-0.1*s, 0.3*s), size=(0.2*s, 0.7*s), fill=color)) elif suit == "C": # 梅花 r = s * 0.4 g.add(dwg.circle(center=(-r, -0.5*r), r=r, fill=color)) g.add(dwg.circle(center=(r, -0.5*r), r=r, fill=color)) g.add(dwg.circle(center=(0, 0.2*r), r=r, fill=color)) g.add(dwg.rect(insert=(-0.1*s, 0.6*r), size=(0.2*s, 0.8*s), fill=color)) return g # ========== 数字牌点阵布局 ========== LAYOUTS = { 2: [(0.5, 0.25), (0.5, 0.75)], 3: [(0.5, 0.20), (0.5, 0.50), (0.5, 0.80)], 4: [(0.25, 0.25), (0.75, 0.25), (0.25, 0.75), (0.75, 0.75)], 5: [(0.25, 0.25), (0.75, 0.25), (0.5, 0.5), (0.25, 0.75), (0.75, 0.75)], 6: [(0.25, 0.20), (0.75, 0.20), (0.25, 0.50), (0.75, 0.50), (0.25, 0.80), (0.75, 0.80)], 7: [(0.25, 0.15), (0.75, 0.15), (0.5, 0.35), (0.25, 0.55), (0.75, 0.55), (0.25, 0.85), (0.75, 0.85)], 8: [(0.25, 0.15), (0.75, 0.15), (0.25, 0.35), (0.75, 0.35), (0.25, 0.65), (0.75, 0.65), (0.25, 0.85), (0.75, 0.85)], 9: [(0.25, 0.12), (0.5, 0.12), (0.75, 0.12), (0.25, 0.35), (0.5, 0.35), (0.75, 0.35), (0.25, 0.65), (0.5, 0.65), (0.75, 0.65)], 10:[(0.25, 0.10), (0.5, 0.10), (0.75, 0.10), (0.25, 0.30), (0.5, 0.30), (0.75, 0.30), (0.25, 0.70), (0.5, 0.70), (0.75, 0.70), (0.5, 0.90)] } def add_pips(dwg, suit, rank): """绘制中间花色点阵""" num = RANK_NUM[rank] if num not in LAYOUTS: return points = LAYOUTS[num] symbol_size_mm = 7.0 for (x_frac, y_frac) in points: x = x_frac * CARD_W y = y_frac * CARD_H dwg.add(draw_suit_symbol(dwg, suit, x, y, symbol_size_mm)) def add_ace_center(dwg, suit): """A 牌中心大花色""" big_size = 25 x = CARD_W / 2 y = CARD_H / 2 dwg.add(draw_suit_symbol(dwg, suit, x, y, big_size)) # ========== 人头牌 J/Q/K 简易图形 ========== def draw_king(dwg, suit, x_center, y_center): g = dwg.g() s = 4 g.add(dwg.polygon([(x_center-s, y_center-s), (x_center, y_center-s-3), (x_center+s, y_center-s)], fill=SUIT_COLOR[suit])) g.add(dwg.rect(insert=(x_center-s/2, y_center-s), size=(s, s), fill=SUIT_COLOR[suit])) g.add(dwg.circle(center=(x_center, y_center), r=3, fill=SUIT_COLOR[suit])) g.add(dwg.rect(insert=(x_center-s/2, y_center+2), size=(s, 5), fill=SUIT_COLOR[suit])) return g def draw_queen(dwg, suit, x_center, y_center): g = dwg.g() g.add(dwg.circle(center=(x_center, y_center), r=2.5, fill=SUIT_COLOR[suit])) g.add(dwg.rect(insert=(x_center-3, y_center+2), size=(6, 4), fill=SUIT_COLOR[suit])) return g def draw_jack(dwg, suit, x_center, y_center): g = dwg.g() g.add(dwg.circle(center=(x_center, y_center), r=2.5, fill=SUIT_COLOR[suit])) g.add(dwg.line(start=(x_center+2, y_center-2), end=(x_center+5, y_center+3), stroke=SUIT_COLOR[suit], stroke_width=1)) return g def add_court_card_center(dwg, rank, suit): x_center = CARD_W / 2 y_center = CARD_H / 2 drawer = { 'K': draw_king, 'Q': draw_queen, 'J': draw_jack }[rank] dwg.add(drawer(dwg, suit, x_center, y_center - 8)) bot = drawer(dwg, suit, x_center, y_center + 8) bot["transform"] = f"translate({x_center},{y_center+8}) scale(1,-1) translate(-{x_center},-{y_center+8})" dwg.add(bot) # ========== 生成单张SVG卡牌 ========== def create_card_svg(rank, suit, output_path): dwg = svgwrite.Drawing(output_path, size=(f"{CARD_W}mm", f"{CARD_H}mm"), profile='tiny') dwg.viewbox(width=CARD_W, height=CARD_H) # 卡片边框 dwg.add(dwg.rect( insert=(0,0), size=(CARD_W, CARD_H), rx=CORNER_RADIUS, ry=CORNER_RADIUS, fill='none', stroke='#333333', stroke_width=0.5 )) # 左上角标 dwg.add(add_corner_indicators(dwg, rank, suit, 3, 3, scale=1.0, rotation=0)) # 右下角标(倒置) dwg.add(add_corner_indicators(dwg, rank, suit, CARD_W-3, CARD_H-3, scale=1.0, rotation=180)) # 中心图案 if rank == 'A': add_ace_center(dwg, suit) elif rank in ['J','Q','K']: add_court_card_center(dwg, rank, suit) else: add_pips(dwg, suit, rank) dwg.save() print(f"SVG 已生成: {output_path}") def create_joker_svg(joker_type, output_path): dwg = svgwrite.Drawing(output_path, size=(f"{CARD_W}mm", f"{CARD_H}mm"), profile='tiny') dwg.viewbox(width=CARD_W, height=CARD_H) dwg.add(dwg.rect( insert=(0,0), size=(CARD_W, CARD_H), rx=CORNER_RADIUS, ry=CORNER_RADIUS, fill='none', stroke='#333333', stroke_width=0.5 )) text_color = '#0000FF' if joker_type == 'SJ' else '#FF0000' cx, cy = CARD_W/2, CARD_H/2 dwg.add(dwg.text("JOKER", insert=(cx, cy-8), font_size="20pt", font_family="Arial", font_weight="bold", fill=text_color, text_anchor="middle")) sub = "Small" if joker_type == 'SJ' else "Big" dwg.add(dwg.text(sub, insert=(cx, cy+10), font_size="12pt", fill=text_color, text_anchor="middle")) dwg.save() print(f"SVG 已生成: {output_path}") # ========== 批量生成全部SVG ========== def generate_all_svgs(output_dir='assets/cards_svg'): os.makedirs(output_dir, exist_ok=True) ranks = ['2','3','4','5','6','7','8','9','10','J','Q','K','A'] suits = ['S','H','C','D'] for rank in ranks: for suit in suits: fname = f"{rank}{suit}.svg" full_path = os.path.join(output_dir, fname) create_card_svg(rank, suit, full_path) # 大小王 create_joker_svg('SJ', os.path.join(output_dir, 'SJ.svg')) create_joker_svg('BJ', os.path.join(output_dir, 'BJ.svg')) # ========== 使用 Inkscape 转 PNG(彻底抛弃 cairosvg / cairo) ========== def export_svg_to_png(svg_path, png_path, scale=2): dpi = 96 * scale cmd = [ "inkscape", svg_path, "--export-type=png", f"--export-dpi={dpi}", "-o", png_path ] subprocess.run(cmd, check=True) print(f"PNG 已生成: {png_path}") def batch_export_png(svg_dir='assets/cards_svg', png_dir='assets/cards_png', scales=[2,4]): os.makedirs(png_dir, exist_ok=True) for fname in os.listdir(svg_dir): if fname.lower().endswith('.svg'): svg_path = os.path.join(svg_dir, fname) base = fname.replace(".svg", "") for scale in scales: png_name = f"{base}@{scale}x.png" png_path = os.path.join(png_dir, png_name) export_svg_to_png(svg_path, png_path, scale) # ========== 主入口 ========== if __name__ == '__main__': # 第一步:生成 SVG(必跑,现在SVG图形完全正常) generate_all_svgs() # 第二步:如需PNG,先安装 Inkscape 并加入环境变量,再取消下面注释 # batch_export_png(scales=[2,4])