#!/usr/bin/env python3 """ generate_roblox_passport.py Создаёт ретро-паспорт Roblox (2006-2012 style) в двух вариантах: - roblox_retro_passport.png (700x450, для веб/показа) - roblox_retro_passport_A4.png (A4 @300 DPI, 2480x3508, для печати) Настрой переменные в секции USER DATA / PATHS ниже перед запуском. """ from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageOps import requests from io import BytesIO import os import sys # --------------------------- USER DATA / PATHS --------------------------- DISPLAY_NAME = "gafts" USERNAME = "@UndertaleOneLove2015" ACCOUNT_AGE = "4 years (as of 2025)" CREATED = "14.01.2021 (Ukraine)" EVENTS_NOTE = "Participated in many Roblox events" STYLE_NOTE = "Retro Roblox (2006–2012)" # Local avatar path (if you uploaded a file). Change if needed. LOCAL_AVATAR_PATH = "/mnt/data/noFilter (2).webp" # <-- change to your local file if needed # Or a direct image URL (uncomment to use). If both present, local file preferred. AVATAR_URL = None # AVATAR_URL = "https://example.com/my_avatar.png" # Output files OUT_WEB = "roblox_retro_passport.png" OUT_A4 = "roblox_retro_passport_A4.png" # Fallback font settings # You may specify a TTF file path here for nicer fonts (e.g., "Arial.ttf" or a pixel font). TTF_FONT_PATH = None # e.g., "C:/Windows/Fonts/arial.ttf" or "./pixel_font.ttf" # ------------------------------------------------------------------------- def load_font(size): """Load a truetype font if available, otherwise default PIL font.""" if TTF_FONT_PATH and os.path.isfile(TTF_FONT_PATH): try: return ImageFont.truetype(TTF_FONT_PATH, size) except Exception: pass try: return ImageFont.truetype("DejaVuSans.ttf", size) except Exception: return ImageFont.load_default() def fetch_avatar(): """Load avatar from local path or URL. Returns RGBA Image.""" # Prefer local file if LOCAL_AVATAR_PATH and os.path.isfile(LOCAL_AVATAR_PATH): try: img = Image.open(LOCAL_AVATAR_PATH).convert("RGBA") return img except Exception as e: print("Error opening local avatar:", e, file=sys.stderr) # Try URL if AVATAR_URL: try: resp = requests.get(AVATAR_URL, timeout=8) resp.raise_for_status() return Image.open(BytesIO(resp.content)).convert("RGBA") except Exception as e: print("Error fetching avatar from URL:", e, file=sys.stderr) # Fallback: generate a simple placeholder (colored block with head) w = 512 placeholder = Image.new("RGBA", (w, w), (100, 180, 220, 255)) ph_draw = ImageDraw.Draw(placeholder) ph_draw.rectangle([w//4, w//6, 3*w//4, 2*w//3], fill=(255, 230, 180, 255)) ph_draw.ellipse([w//3, w//20, 2*w//3, w//3], fill=(0, 190, 200, 255)) return placeholder def add_paper_texture(base_img, intensity=0.25): """Apply a subtle paper/noise texture to base_img (RGBA).""" w, h = base_img.size noise = Image.effect_noise((w, h), 12).convert("L") # make noise lighter and partial alpha noise = noise.point(lambda p: int(p * 0.6 + 100)) noise_rgba = Image.merge("RGBA", (noise, noise, noise, noise)) return Image.blend(base_img, noise_rgba, intensity) def draw_retro_passport(canvas_w=700, canvas_h=450, a4=False): # Background color: retro beige bg = Image.new("RGBA", (canvas_w, canvas_h), (240, 220, 180, 255)) bg = add_paper_texture(bg, intensity=0.18) draw = ImageDraw.Draw(bg) font_sm = load_font(14) font_md = load_font(18) font_lg = load_font(22) # Avatar avatar = fetch_avatar() av_size = (180, 180) if canvas_w >= 700 else (150, 150) avatar = avatar.resize(av_size, Image.LANCZOS) # create a chip-style rounded mask for nicer look mask = Image.new("L", av_size, 0) mask_draw = ImageDraw.Draw(mask) mask_draw.rounded_rectangle([0,0,av_size[0],av_size[1]], radius=10, fill=255) avatar = ImageOps.fit(avatar, av_size) # paste avatar av_x, av_y = 60, 80 bg.paste(avatar, (av_x, av_y), mask) # Frame around avatar frame_color = (90, 40, 0) frame_box = [av_x-10, av_y-10, av_x+av_size[0]+10, av_y+av_size[1]+10] draw.rectangle(frame_box, outline=frame_color, width=4) # Header: OFFICIAL ROBLOX PASSPORT (centered) header_text = "OFFICIAL ROBLOX PASSPORT" w_text, h_text = draw.textsize(header_text, font=font_lg) draw.text(((canvas_w - w_text) // 2, 20), header_text, fill=(35, 10, 10), font=font_lg) # Pixel-style R logo (simple red R inside small square) logo_size = 80 if canvas_w >= 700 else 60 logo = Image.new("RGBA", (logo_size, logo_size), (0,0,0,0)) ld = ImageDraw.Draw(logo) # background square (slightly off-white) ld.rectangle([0,0,logo_size,logo_size], fill=(250,240,230)) # Draw big R letter - we simulate pixel style by using a chunky font (or many small rectangles) # if truetype available, use it, otherwise draw a blocky R manually try: rfont = ImageFont.truetype("DejaVuSans-Bold.ttf", int(logo_size * 0.7)) wR, hR = ld.textsize("R", font=rfont) ld.text(((logo_size-wR)//2, (logo_size-hR)//2), "R", fill=(200,10,10), font=rfont) except Exception: # manual pixel R px = logo_size // 8 for y in range(1,6): ld.rectangle([px, y*px, 2*px, (y+1)*px], fill=(200,10,10)) # arm and leg ld.rectangle([2*px, 1*px, 4*px, 2*px], fill=(200,10,10)) ld.rectangle([3*px, 3*px, 4*px, 4*px], fill=(200,10,10)) # paste logo bottom-right corner bg.paste(logo, (canvas_w - logo_size - 20, canvas_h - logo_size - 20), logo) # Main info text block to the right of avatar info_x = av_x + av_size[0] + 40 info_y = av_y - 10 line_h = 28 if canvas_w>=700 else 22 draw.text((info_x, info_y + 0*line_h), f"Display Name: {DISPLAY_NAME}", fill=(20,20,20), font=font_md) draw.text((info_x, info_y + 1*line_h), f"Username: {USERNAME}", fill=(20,20,20), font=font_md) draw.text((info_x, info_y + 2*line_h), f"Account Age: {ACCOUNT_AGE}", fill=(20,20,20), font=font_md) draw.text((info_x, info_y + 3*line_h), f"Created: {CREATED}", fill=(20,20,20), font=font_md) draw.text((info_x, info_y + 4*line_h), f"{EVENTS_NOTE}", fill=(20,20,20), font=font_sm) # Issued / signature lines sig_x = av_x draw.text((sig_x, canvas_h - 110), "Issued by ROBLOX Corporation 2006–2012 Style", fill=(50,5,5), font=font_sm) draw.text((sig_x, canvas_h - 90), "Retro Edition - Not for Trade", fill=(70,5,5), font=font_sm) # small stamp / circular mark to look official stamp_radius = 36 if canvas_w>=700 else 28 stamp_center = (canvas_w - logo_size - 80, canvas_h - 120) draw.ellipse([stamp_center[0]-stamp_radius, stamp_center[1]-stamp_radius, stamp_center[0]+stamp_radius, stamp_center[1]+stamp_radius], outline=(80,20,20), width=2) # small text inside stamp stamp_font = load_font(12) stext = "RETRO" sw, sh = draw.textsize(stext, font=stamp_font) draw.text((stamp_center[0]-sw//2, stamp_center[1]-sh//2), stext, fill=(80,20,20), font=stamp_font) # subtle vignette (darken edges) vignette = Image.new("L", (canvas_w, canvas_h), 0) vdraw = ImageDraw.Draw(vignette) # radial gradient: draw multiple ellipses maxr = max(canvas_w, canvas_h) for i in range(10): alpha = int(12 * (i+1)) # small darkness bbox = [ -i*20, -i*20, canvas_w + i*20, canvas_h + i*20] vdraw.ellipse(bbox, fill=alpha) vignette = vignette.filter(ImageFilter.GaussianBlur(10)) # composite vignette bg.putalpha(255) bg_alpha = bg.split()[-1] new_alpha = ImageChops.subtract(bg_alpha, vignette) if 'ImageChops' in globals() else bg_alpha # if ImageChops not imported (rare), skip alpha manipulation try: import PIL.ImageChops as ImageChops new_alpha = ImageChops.subtract(bg_alpha, vignette) bg.putalpha(new_alpha) # convert back to RGBA on white-beige background to remove alpha final = Image.new("RGBA", bg.size, (240,220,180,255)) final = Image.alpha_composite(final, bg) except Exception: final = bg.convert("RGBA") # final smoothing / sharpen tweaks final = final.filter(ImageFilter.SMOOTH_MORE) return final def save_images(): # web size img_web = draw_retro_passport(700, 450, a4=False) img_web.save(OUT_WEB) print("Saved:", OUT_WEB) # A4 @300 DPI => 2480x3508 px (portrait) ; we'll make same layout but taller canvas a4_w, a4_h = 2480, 3508 img_a4 = draw_retro_passport(a4_w, a4_h, a4=True) img_a4.save(OUT_A4) print("Saved:", OUT_A4) if __name__ == "__main__": try: save_images() except Exception as exc: print("Error during generation:", exc, file=sys.stderr) raise