# -*- coding: utf-8 -*- """ Plakat-odznaka na urodziny: SZEFOWA SKUPU — GŁÓWNY INSPEKTOR JAKOŚCI. A4 @ 300 DPI (2480 x 3508). Baza: wygenerowany portret mamy-inspektora. """ import math import numpy as np from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance W, H = 2480, 3508 # A4 300 dpi GRAPHITE_T = (26, 40, 32) GRAPHITE_B = (9, 14, 11) GREEN = (24, 66, 44) GREEN_D = (13, 38, 26) YELLOW = (255, 208, 0) GOLD_L = (255, 224, 130) GOLD = (212, 175, 55) GOLD_D = (122, 92, 24) WHITE = (255, 255, 255) SOFT = (205, 218, 208) FONT_BEBAS = "fonts/BebasNeue-Regular.ttf" FONT_XBOLD = "fonts/Montserrat-ExtraBold.ttf" FONT_BOLD = "fonts/Montserrat-Bold.ttf" PORTRAIT = "reklama_facebook/mama_inspektor_v2.jpg" OUT = "reklama_facebook/MAMA_szefowa_skupu_A4_300dpi.jpg" def fit_font(text, font_path, target_w, max_size=2000): lo, hi = 20, max_size while lo < hi: mid = (lo + hi + 1) // 2 if ImageFont.truetype(font_path, mid).getlength(text) <= target_w: lo = mid else: hi = mid - 1 return ImageFont.truetype(font_path, lo) def text_c(draw, cx, cy, text, font, fill): bb = font.getbbox(text) draw.text((cx - (bb[0] + bb[2]) / 2, cy - (bb[1] + bb[3]) / 2), text, font=font, fill=fill) def text_c_tracked(draw, cx, cy, text, font, fill, tracking): widths = [font.getlength(ch) for ch in text] total = sum(widths) + tracking * (len(text) - 1) bb = font.getbbox(text) y = cy - (bb[1] + bb[3]) / 2 x = cx - total / 2 for ch, w in zip(text, widths): draw.text((x, y), ch, font=font, fill=fill) x += w + tracking def make_background(): x = np.linspace(0, 1, W, dtype=np.float32) y = np.linspace(0, 1, H, dtype=np.float32) X, Y = np.meshgrid(x, y) top = np.array(GRAPHITE_T, dtype=np.float32) bot = np.array(GRAPHITE_B, dtype=np.float32) grad = top[None, None, :] * (1 - Y[..., None]) + \ bot[None, None, :] * Y[..., None] # ciepła poświata za odznaką glow = np.exp(-(((X - 0.5) ** 2) / 0.16 + ((Y - 0.44) ** 2) / 0.05)) grad += glow[..., None] * np.array([30, 34, 12], dtype=np.float32) vx = np.clip((X - 0.5) / 0.62, -1, 1) vy = np.clip((Y - 0.5) / 0.66, -1, 1) vig = 1 - 0.30 * np.clip(np.sqrt(vx ** 2 + vy ** 2) - 0.25, 0, 1) ** 1.4 grad *= vig[..., None] return Image.fromarray(np.clip(grad, 0, 255).astype(np.uint8), "RGB").convert("RGBA") def ring_mask(size, r_out, r_in): y, x = np.ogrid[:size, :size] c = size / 2 - 0.5 d = np.sqrt((x - c) ** 2 + (y - c) ** 2) out = np.clip(r_out + 0.5 - d, 0, 1) inn = np.clip(d - (r_in - 0.5), 0, 1) return Image.fromarray(((out * inn) * 255).astype(np.uint8), "L") def disc_mask(size, r): y, x = np.ogrid[:size, :size] c = size / 2 - 0.5 d = np.sqrt((x - c) ** 2 + (y - c) ** 2) return Image.fromarray( (np.clip(r + 0.5 - d, 0, 1) * 255).astype(np.uint8), "L") def gold_ring(size, r_out, r_in): """Metaliczny złoty pierścień.""" layer = Image.new("RGBA", (size, size), (0, 0, 0, 0)) ax = np.linspace(0, 1, size, dtype=np.float32) ay = np.linspace(0, 1, size, dtype=np.float32) AX, AY = np.meshgrid(ax, ay) t = np.clip((AX + AY) / 2, 0, 1) stops = np.array([ [255, 240, 180], [212, 175, 55], [140, 105, 30], [255, 224, 130], [170, 130, 40], ], dtype=np.float32) tt = (t * (len(stops) - 1) * 2) % (len(stops) - 1) i0 = tt.astype(int) f = (tt - i0)[..., None] col = stops[i0] * (1 - f) + stops[i0 + 1] * f grad = Image.fromarray(col.astype(np.uint8), "RGB").convert("RGBA") mask = ring_mask(size, r_out, r_in) layer.paste(grad, (0, 0), mask) # krawędzie d = ImageDraw.Draw(layer) c = size / 2 - 0.5 d.ellipse([c - r_out + 4, c - r_out + 4, c + r_out - 4, c + r_out - 4], outline=(70, 50, 10, 255), width=8) d.ellipse([c - r_in - 4, c - r_in - 4, c + r_in + 4, c + r_in + 4], outline=(70, 50, 10, 255), width=8) # połysk w lewym górnym rogu hl = Image.new("RGBA", (size, size), (0, 0, 0, 0)) ImageDraw.Draw(hl).ellipse( [c - r_out * 0.9, c - r_out * 0.95, c + r_out * 0.2, c - r_out * 0.3], fill=(255, 250, 220, 90)) hl = hl.filter(ImageFilter.GaussianBlur(60)) layer.alpha_composite(Image.composite( hl, Image.new("RGBA", (size, size), (0, 0, 0, 0)), mask)) return layer def green_band(size, r_out, r_in): """Zielony pas odznaki między pierścieniami.""" layer = Image.new("RGBA", (size, size), (0, 0, 0, 0)) ay = np.linspace(0, 1, size, dtype=np.float32)[:, None] c1 = np.array(GREEN, dtype=np.float32) c2 = np.array(GREEN_D, dtype=np.float32) col = (c1[None, None, :] * (1 - ay[..., None]) + c2[None, None, :] * ay[..., None]) grad = Image.fromarray(np.clip(col, 0, 255).astype(np.uint8), "RGB").convert("RGBA") grad = grad.resize((size, size)) layer.paste(grad, (0, 0), ring_mask(size, r_out, r_in)) d = ImageDraw.Draw(layer) c = size / 2 - 0.5 for r, col_ in ((r_out - 3, GOLD), (r_in + 3, GOLD)): d.ellipse([c - r, c - r, c + r, c + r], outline=col_ + (255,), width=7) return layer def arc_text(canvas, cx, cy, radius, text, font, fill, tracking_px, center_deg=-90, upright=True): """Tekst po łuku; -90 deg = szczyt koła.""" widths = [font.getlength(ch) for ch in text] total_px = sum(widths) + tracking_px * (len(text) - 1) total_ang = total_px / radius # radiany start = math.radians(center_deg) - total_ang / 2 ang = start for ch, wch in zip(text, widths): half = (wch / 2 + tracking_px / 2) / radius ang += (wch / 2) / radius x = cx + radius * math.cos(ang) y = cy + radius * math.sin(ang) tile = Image.new("RGBA", (int(wch) + 80, font.size + 120), (0, 0, 0, 0)) td = ImageDraw.Draw(tile) bb = font.getbbox(ch) td.text((40 - bb[0], 60 - bb[1]), ch, font=font, fill=fill + (255,)) rot = math.degrees(ang) + 90 if upright else math.degrees(ang) - 90 tile = tile.rotate(-rot, resample=Image.BICUBIC, expand=True) canvas.alpha_composite( tile, (int(x - tile.width / 2), int(y - tile.height / 2))) ang += (wch / 2 + tracking_px) / radius def photo_disc(path, size): """Portret w kole.""" photo = Image.open(path).convert("RGB") # kadruj na głowę i ramiona (górna część zdjęcia) scale = size / photo.width nw, nh = int(photo.width * scale), int(photo.height * scale) photo = photo.resize((nw, nh), Image.LANCZOS) top = int(nh * 0.02) photo = photo.crop((0, top, size, top + size)) photo = ImageEnhance.Color(photo).enhance(1.06) photo = ImageEnhance.Contrast(photo).enhance(1.04) photo = photo.filter(ImageFilter.UnsharpMask(radius=2, percent=60, threshold=2)) layer = Image.new("RGBA", (size, size), (0, 0, 0, 0)) layer.paste(photo.convert("RGBA"), (0, 0), disc_mask(size, size // 2)) return layer def ribbon(canvas, cx, cy, w, h, text): """Zielona wstęga ze złotą obwódką i ząbkowanymi końcami.""" d = ImageDraw.Draw(canvas) tail = int(h * 1.5) # ogony wstęgi (za korpus) for sgn in (-1, 1): x0 = cx + sgn * (w / 2 - 10) pts = [(x0, cy - h * 0.32), (x0 + sgn * tail, cy - h * 0.10), (x0 + sgn * tail * 0.82, cy + h * 0.30), (x0 + sgn * tail, cy + h * 0.72), (x0, cy + h * 0.42)] d.polygon(pts, fill=GREEN_D + (255,), outline=GOLD + (255,), width=0) d.line(pts + [pts[0]], fill=GOLD + (255,), width=7, joint="curve") # cień + korpus sh = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) ImageDraw.Draw(sh).rectangle( [cx - w / 2 + 10, cy - h / 2 + 16, cx + w / 2 + 10, cy + h / 2 + 16], fill=(0, 0, 0, 170)) canvas.alpha_composite(sh.filter(ImageFilter.GaussianBlur(30))) d = ImageDraw.Draw(canvas) d.rectangle([cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2], fill=GREEN + (255,), outline=GOLD + (255,), width=8) d.line([cx - w / 2, cy - h / 2 + 14, cx + w / 2, cy - h / 2 + 14], fill=(255, 255, 255, 40), width=4) f = fit_font(text, FONT_BEBAS, w - 160, max_size=int(h * 0.62)) text_c(d, cx, cy, text, f, YELLOW) def build(): canvas = make_background() d = ImageDraw.Draw(canvas) cx = W / 2 # ---- nagłówek ---- f_title = fit_font("SKUP GRZYBÓW", FONT_BEBAS, W - 260, max_size=330) ty = 330 bb = f_title.getbbox("SKUP GRZYBÓW") sh = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) ImageDraw.Draw(sh).text((cx - (bb[0] + bb[2]) / 2 + 7, ty - (bb[1] + bb[3]) / 2 + 12), "SKUP GRZYBÓW", font=f_title, fill=(0, 0, 0, 200)) canvas.alpha_composite(sh.filter(ImageFilter.GaussianBlur(16))) text_c(ImageDraw.Draw(canvas), cx, ty, "SKUP GRZYBÓW", f_title, YELLOW) f_sub = ImageFont.truetype(FONT_XBOLD, 62) text_c_tracked(ImageDraw.Draw(canvas), cx, 585, "ODZNAKA HONOROWA", f_sub, SOFT, tracking=22) # ---- odznaka ---- badge_r = 800 badge_cy = 1620 size = badge_r * 2 + 40 bx, by = int(cx - size / 2), int(badge_cy - size / 2) # cień odznaki sh2 = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) ImageDraw.Draw(sh2).ellipse( [bx + 30, by + 55, bx + size + 30, by + size + 55], fill=(0, 0, 0, 190)) canvas.alpha_composite(sh2.filter(ImageFilter.GaussianBlur(70))) badge = Image.new("RGBA", (size, size), (0, 0, 0, 0)) r_out, r_band_in, r_photo = badge_r, badge_r - 60, badge_r - 230 badge.alpha_composite(gold_ring(size, r_out, r_band_in)) badge.alpha_composite(green_band(size, r_band_in, r_photo)) badge.alpha_composite(photo_disc(PORTRAIT, r_photo * 2), (int(size / 2 - r_photo), int(size / 2 - r_photo))) # złota obwódka wokół zdjęcia ImageDraw.Draw(badge).ellipse( [size / 2 - r_photo - 6, size / 2 - r_photo - 6, size / 2 + r_photo + 6, size / 2 + r_photo + 6], outline=GOLD + (255,), width=10) canvas.alpha_composite(badge, (bx, by)) # napis łukiem na zielonym pasie f_arc = ImageFont.truetype(FONT_BEBAS, 128) arc_text(canvas, cx, badge_cy, (r_band_in + r_photo) / 2 + 6, "SZEFOWA SKUPU", f_arc, YELLOW, tracking_px=18) # nity boczne for sgn in (-1, 1): rx = cx + sgn * (r_band_in + r_photo) / 2 d.ellipse([rx - 16, badge_cy - 16, rx + 16, badge_cy + 16], fill=GOLD_L + (255,), outline=GOLD_D + (255,), width=4) # ---- wstęga ---- ribbon(canvas, cx, badge_cy + badge_r - 40, 1500, 190, "GŁÓWNY INSPEKTOR JAKOŚCI") # ---- dedykacja ---- f_ded = fit_font("Wszystkiego najlepszego, Mamo!", FONT_XBOLD, W - 300, max_size=105) text_c(d, 0 + cx, 2890, "Wszystkiego najlepszego, Mamo!", f_ded, WHITE) f_ded2 = ImageFont.truetype(FONT_BOLD, 52) text_c(d, cx, 3005, "z okazji urodzin — od całego Skupu Grzybów", f_ded2, SOFT) # ---- stopka ---- f_foot = ImageFont.truetype(FONT_XBOLD, 46) text_c_tracked(d, cx, 3290, "SKUP GRZYBÓW • TRZCIANKA", f_foot, GOLD, tracking=16) f_foot2 = ImageFont.truetype(FONT_BOLD, 40) text_c(d, cx, 3370, "os. Modrzewiowa 21 • wejście od ul. Łomnickiej", f_foot2, (150, 165, 155)) canvas.convert("RGB").save(OUT, quality=95, dpi=(300, 300)) canvas.convert("RGB").resize((W // 4, H // 4), Image.LANCZOS).save( "reklama_facebook/MAMA_poster_podglad.jpg", quality=88) print("zapisano:", OUT) if __name__ == "__main__": build()