import os import math from io import BytesIO from PIL import Image, ImageDraw, ImageFont class BankCardGenerator: WIDTH = 856 HEIGHT = 540 RADIUS = 30 COLORS = { "dark": (25, 25, 35), "mid": (35, 40, 60), "light": (50, 55, 80), "accent": (0, 180, 160), "accent_dim": (0, 120, 105), "gold": (212, 175, 55), "white": (255, 255, 255), "gray": (160, 165, 180), "dim": (90, 95, 110), } 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() 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, 26) self.font_large = ImageFont.truetype(path, 36) elif "mono" in lower or "regular" in lower: self.font_label = ImageFont.truetype(path, 16) self.font_value = ImageFont.truetype(path, 22) self.font_rib = ImageFont.truetype(path, 20) except Exception: pass def _rounded_rect(self, draw, xy, radius, fill, outline=None, width=0): x0, y0, x1, y1 = xy r = radius draw.ellipse([x0, y0, x0 + 2 * r, y0 + 2 * r], fill=fill, outline=outline, width=width) draw.ellipse([x1 - 2 * r, y0, x1, y0 + 2 * r], fill=fill, outline=outline, width=width) draw.ellipse([x0, y1 - 2 * r, x0 + 2 * r, y1], fill=fill, outline=outline, width=width) draw.ellipse([x1 - 2 * r, y1 - 2 * r, x1, y1], fill=fill, outline=outline, width=width) 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, 0, 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 _draw_gradient_rect(self, draw, xy, color_start, color_end, vertical=True): x0, y0, x1, y1 = xy if vertical: for y in range(y0, y1): t = (y - y0) / max(1, y1 - y0) r = int(color_start[0] + (color_end[0] - color_start[0]) * t) g = int(color_start[1] + (color_end[1] - color_start[1]) * t) b = int(color_start[2] + (color_end[2] - color_start[2]) * t) draw.line([(x0, y), (x1, y)], fill=(r, g, b)) else: for x in range(x0, x1): t = (x - x0) / max(1, x1 - x0) r = int(color_start[0] + (color_end[0] - color_start[0]) * t) g = int(color_start[1] + (color_end[1] - color_start[1]) * t) b = int(color_start[2] + (color_end[2] - color_start[2]) * t) draw.line([(x, y0), (x, y1)], fill=(r, g, b)) def _draw_chip(self, draw, x, y): w, h = 55, 40 self._rounded_rect(draw, (x, y, x + w, y + h), 6, fill=(200, 175, 50), outline=(180, 155, 40), width=2) draw.line([(x + 8, y + h // 3), (x + w - 8, y + h // 3)], fill=(160, 135, 30), width=1) draw.line([(x + 8, y + 2 * h // 3), (x + w - 8, y + 2 * h // 3)], fill=(160, 135, 30), width=1) draw.line([(x + w // 3, y + 4), (x + w // 3, y + h - 4)], fill=(160, 135, 30), width=1) draw.line([(x + 2 * w // 3, y + 4), (x + 2 * w // 3, y + h - 4)], fill=(160, 135, 30), width=1) def _draw_wave(self, draw, y_base, color, amplitude=15, segments=8): points = [] w = self.WIDTH for i in range(segments + 1): x = int(w * i / segments) y = y_base + int(amplitude * math.sin(i * 1.2)) points.append((x, y)) for i in range(len(points) - 1): draw.line([points[i], points[i + 1]], fill=color, width=2) def generate( self, bank_name: str, holder_name: str, rib: str, solde: float, account_type: str = "particulier", tva_rate: float = 0.0, ) -> BytesIO: img = Image.new("RGB", (self.WIDTH, self.HEIGHT), self.COLORS["dark"]) draw = ImageDraw.Draw(img) self._draw_gradient_rect(draw, (0, 0, self.WIDTH, self.HEIGHT), self.COLORS["dark"], self.COLORS["mid"]) self._draw_wave(draw, 120, (*self.COLORS["accent"][:3],), amplitude=20, segments=10) self._draw_wave(draw, 140, (*self.COLORS["accent_dim"][:3],), amplitude=15, segments=12) self._draw_wave(draw, 360, (*self.COLORS["light"][:3],), amplitude=12, segments=8) self._rounded_rect(draw, (20, 20, self.WIDTH - 20, self.HEIGHT - 20), self.RADIUS, fill=None, outline=self.COLORS["accent"], width=2) self._draw_chip(draw, 60, 130) draw.text((75, 185), "VICTORIA'S VICE", fill=self.COLORS["accent"], font=self.font_title) draw.text((75, 218), bank_name.upper(), fill=self.COLORS["gray"], font=self.font_label) type_label = "COMPTE ENTREPRISE" if account_type == "entreprise" else "COMPTE PARTICULIER" draw.text((self.WIDTH - 200, 45), type_label, fill=self.COLORS["dim"], font=self.font_label) y_rib = 280 draw.text((60, y_rib), "RIB", fill=self.COLORS["dim"], font=self.font_label) rib_parts = [] clean_rib = rib.replace(" ", "") for i in range(0, len(clean_rib), 4): rib_parts.append(clean_rib[i:i + 4]) rib_display = " ".join(rib_parts) draw.text((60, y_rib + 22), rib_display, fill=self.COLORS["white"], font=self.font_rib) y_solde = 350 draw.text((60, y_solde), "SOLDE", fill=self.COLORS["dim"], font=self.font_label) solde_str = f"{solde:,.2f} EUR".replace(",", " ") draw.text((60, y_solde + 22), solde_str, fill=self.COLORS["gold"], font=self.font_large) y_bottom = self.HEIGHT - 100 draw.text((60, y_bottom), "TITULAIRE", fill=self.COLORS["dim"], font=self.font_label) draw.text((60, y_bottom + 20), holder_name.upper(), fill=self.COLORS["white"], font=self.font_value) if account_type == "entreprise" and tva_rate > 0: draw.text((self.WIDTH - 200, y_bottom), "TVA", fill=self.COLORS["dim"], font=self.font_label) draw.text((self.WIDTH - 200, y_bottom + 20), f"{tva_rate}%", fill=self.COLORS["accent"], font=self.font_value) for i in range(5): x = self.WIDTH - 60 - i * 35 draw.ellipse([x, self.HEIGHT - 50, x + 25, self.HEIGHT - 25], fill=self.COLORS["accent"] if i < 3 else self.COLORS["dim"]) buf = BytesIO() img.save(buf, format="PNG") buf.seek(0) return buf