import os import math import hashlib from io import BytesIO from PIL import Image, ImageDraw, ImageFont, ImageFilter class BankCardGenerator: WIDTH = 856 HEIGHT = 540 def __init__(self): self.font_title = ImageFont.load_default() self.font_label = ImageFont.load_default() self.font_value = ImageFont.load_default() self.font_rib = ImageFont.load_default() self.font_large = ImageFont.load_default() self.font_huge = ImageFont.load_default() self.font_small = ImageFont.load_default() try: base = os.path.join(os.path.dirname(__file__), "..", "ressources", "fonts") if os.path.isdir(base): for name in os.listdir(base): path = os.path.join(base, name) lower = name.lower() if "bold" in lower: self.font_title = ImageFont.truetype(path, 28) self.font_large = ImageFont.truetype(path, 38) self.font_huge = ImageFont.truetype(path, 48) elif "mono" in lower or "regular" in lower or "light" in lower: self.font_label = ImageFont.truetype(path, 15) self.font_value = ImageFont.truetype(path, 22) self.font_rib = ImageFont.truetype(path, 24) self.font_small = ImageFont.truetype(path, 12) except Exception: pass def _rounded_rect(self, draw, xy, radius, fill=None, outline=None, width=0): x0, y0, x1, y1 = xy r = min(radius, (x1 - x0) // 2, (y1 - y0) // 2) if fill: draw.ellipse([x0, y0, x0 + 2 * r, y0 + 2 * r], fill=fill) draw.ellipse([x1 - 2 * r, y0, x1, y0 + 2 * r], fill=fill) draw.ellipse([x0, y1 - 2 * r, x0 + 2 * r, y1], fill=fill) draw.ellipse([x1 - 2 * r, y1 - 2 * r, x1, y1], fill=fill) draw.rectangle([x0 + r, y0, x1 - r, y1], fill=fill) draw.rectangle([x0, y0 + r, x1, y1 - r], fill=fill) if outline and width: draw.arc([x0, y0, x0 + 2 * r, y0 + 2 * r], 180, 270, fill=outline, width=width) draw.arc([x1 - 2 * r, y0, x1, y0 + 2 * r], 270, 360, fill=outline, width=width) draw.arc([x0, y1 - 2 * r, x0 + 2 * r, y1], 90, 180, fill=outline, width=width) draw.arc([x1 - 2 * r, y1 - 2 * r, x1, y1], 0, 90, fill=outline, width=width) draw.line([x0 + r, y0, x1 - r, y0], fill=outline, width=width) draw.line([x0 + r, y1, x1 - r, y1], fill=outline, width=width) draw.line([x0, y0 + r, x0, y1 - r], fill=outline, width=width) draw.line([x1, y0 + r, x1, y1 - r], fill=outline, width=width) def _gradient(self, draw, xy, c1, c2, vertical=True): x0, y0, x1, y1 = xy rng = range(y0, y1) if vertical else range(x0, x1) length = (y1 - y0) if vertical else (x1 - x0) for i in rng: t = (i - (y0 if vertical else x0)) / max(1, length) r = int(c1[0] + (c2[0] - c1[0]) * t) g = int(c1[1] + (c2[1] - c1[1]) * t) b = int(c1[2] + (c2[2] - c1[2]) * t) if vertical: draw.line([(x0, i), (x1, i)], fill=(r, g, b)) else: draw.line([(i, y0), (i, y1)], fill=(r, g, b)) def _draw_chip(self, draw, x, y): w, h = 60, 45 self._rounded_rect(draw, (x, y, x + w, y + h), 8, fill=(210, 180, 55)) self._rounded_rect(draw, (x + 3, y + 3, x + w - 3, y + h - 3), 6, fill=(225, 195, 70)) for dy in [h // 3, 2 * h // 3]: draw.line([(x + 8, y + dy), (x + w - 8, y + dy)], fill=(190, 160, 40), width=1) for dx in [w // 3, 2 * w // 3]: draw.line([(x + dx, y + 5), (x + dx, y + h - 5)], fill=(190, 160, 40), width=1) draw.arc([x + w // 2 - 12, y + h // 2 - 12, x + w // 2 + 12, y + h // 2 + 12], 0, 360, fill=(190, 160, 40), width=1) def _draw_hologram(self, draw, x, y, size=50): for i in range(5): offset = i * 3 alpha_color = ( 100 + i * 30, 200 - i * 20, 220 - i * 15, ) draw.ellipse( [x + offset, y + offset, x + size - offset, y + size - offset], outline=alpha_color, width=1, ) draw.ellipse([x + size // 2 - 5, y + size // 2 - 5, x + size // 2 + 5, y + size // 2 + 5], fill=(180, 220, 240)) def _draw_contactless(self, draw, x, y): for i, r in enumerate([10, 16, 22]): draw.arc([x - r, y - r, x + r, y + r], -60, 60, fill=(180, 180, 190), width=1) def _draw_security_pattern(self, draw, width, height): for x in range(0, width, 40): for y in range(0, height, 40): h = hashlib.md5(f"{x}{y}".encode()).hexdigest() if int(h[:2], 16) < 15: draw.point((x, y), fill=(60, 65, 85)) def _draw_glow(self, img, x, y, radius=80, color=(0, 180, 160), intensity=30): overlay = Image.new("RGBA", img.size, (0, 0, 0, 0)) od = ImageDraw.Draw(overlay) for r in range(radius, 0, -2): alpha = int(intensity * (r / radius)) od.ellipse( [x - r, y - r, x + r, y + r], fill=(*color, alpha), ) return Image.alpha_composite(img.convert("RGBA"), overlay) def generate( self, bank_name: str, holder_name: str, rib: str, solde: float, account_type: str = "particulier", tva_rate: float = 0.0, ) -> BytesIO: dark1 = (18, 18, 28) dark2 = (30, 32, 48) accent = (0, 190, 170) accent_dim = (0, 130, 115) gold = (212, 175, 55) white = (245, 245, 250) gray = (150, 155, 170) dim = (80, 85, 100) img = Image.new("RGB", (self.WIDTH, self.HEIGHT), dark1) draw = ImageDraw.Draw(img) self._gradient(draw, (0, 0, self.WIDTH, self.HEIGHT), dark1, dark2) self._draw_security_pattern(draw, self.WIDTH, self.HEIGHT) for wave_y, color, amp, seg in [ (100, (*accent[:3],), 25, 10), (130, (*accent_dim[:3],), 18, 12), (350, (55, 60, 85), 12, 8), ]: pts = [] for i in range(seg + 1): px = int(self.WIDTH * i / seg) py = wave_y + int(amp * math.sin(i * 1.3)) pts.append((px, py)) for i in range(len(pts) - 1): draw.line([pts[i], pts[i + 1]], fill=color, width=2) self._rounded_rect(draw, (18, 18, self.WIDTH - 18, self.HEIGHT - 18), 28, outline=accent, width=2) img = self._draw_glow(img, 85, 155, radius=70, color=accent, intensity=20) draw = ImageDraw.Draw(img) self._draw_chip(draw, 55, 125) self._draw_hologram(draw, self.WIDTH - 120, 120, size=55) self._draw_contactless(draw, self.WIDTH - 80, 80) draw.text((65, 182), "VICTORIA'S VICE", fill=accent, font=self.font_title) draw.text((65, 215), bank_name.upper(), fill=gray, font=self.font_label) type_label = "COMPTE ENTREPRISE" if account_type == "entreprise" else "COMPTE PARTICULIER" draw.text((self.WIDTH - 210, 42), type_label, fill=dim, font=self.font_label) clean_rib = rib.replace(" ", "") rib_display = " ".join(clean_rib[i:i + 4] for i in range(0, len(clean_rib), 4)) y_card_number = 260 draw.text((60, y_card_number - 18), "RIB", fill=dim, font=self.font_label) draw.text((60, y_card_number), rib_display, fill=white, font=self.font_rib) y_solde = 340 draw.text((60, y_solde - 18), "SOLDE DISPONIBLE", fill=dim, font=self.font_label) solde_str = f"{solde:,.2f} EUR".replace(",", " ") draw.text((60, y_solde), solde_str, fill=gold, font=self.font_large) if account_type == "entreprise" and tva_rate > 0: draw.text((self.WIDTH - 180, y_solde - 18), "TAUX TVA", fill=dim, font=self.font_label) draw.text((self.WIDTH - 180, y_solde), f"{tva_rate}%", fill=accent, font=self.font_value) y_bottom = self.HEIGHT - 110 draw.line([(60, y_bottom - 10), (self.WIDTH - 60, y_bottom - 10)], fill=(60, 65, 85), width=1) draw.text((60, y_bottom), "TITULAIRE", fill=dim, font=self.font_label) draw.text((60, y_bottom + 18), holder_name.upper(), fill=white, font=self.font_value) sig_y = y_bottom + 5 for i in range(40): sx = self.WIDTH - 250 + i * 3 sy = sig_y + int(3 * math.sin(i * 0.5)) draw.point((sx, sy), fill=(120, 125, 140)) draw.point((sx, sy + 1), fill=(120, 125, 140)) for i in range(4): cx = self.WIDTH - 65 - i * 30 draw.ellipse([cx, self.HEIGHT - 45, cx + 28, self.HEIGHT - 17], fill=accent if i < 2 else dim) draw.text((55, self.HEIGHT - 35), "VIRTUAL CARD", fill=dim, font=self.font_small) buf = BytesIO() img.save(buf, format="PNG") buf.seek(0) return buf