# -*- coding: utf-8 -*- """ Grafika na post Facebook — SKUP GRZYBÓW Trzcianka. Baza: REALNE zdjęcie siedziby skupu (napisy z oryginału usunięte). Format: 1080 x 1350 (4:5) — optymalny zasięg w feedzie. Dane wg oryginalnego ogłoszenia: SKUP GRZYBÓW / TRZCIANKA / os. Modrzewiowa 21 / wejście od strony Łomnickiej. """ import numpy as np from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance # ---------- parametry ---------- W, H = 1080, 1350 GRAPHITE = (20, 33, 26) YELLOW = (255, 208, 0) # #FFD000 WHITE = (255, 255, 255) SOFT = (208, 220, 211) DARK = (8, 14, 11) FONT_BEBAS = "fonts/BebasNeue-Regular.ttf" FONT_XBOLD = "fonts/Montserrat-ExtraBold.ttf" FONT_BOLD = "fonts/Montserrat-Bold.ttf" PHOTO = "reklama_facebook/skup_bez_napisow.jpg" OUT = "reklama_facebook/FB_post_SKUP_GRZYBOW_1080x1350.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): """Wyśrodkowany tekst z letter-spacing.""" 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 cover_photo(canvas, photo_path): """Realne zdjęcie full-bleed na cały canvas.""" photo = Image.open(photo_path).convert("RGB") scale = max(W / photo.width, H / photo.height) nw, nh = int(photo.width * scale + 0.5), int(photo.height * scale + 0.5) photo = photo.resize((nw, nh), Image.LANCZOS) left = (nw - W) // 2 top = (nh - H) // 2 photo = photo.crop((left, top, left + W, top + H)) photo = ImageEnhance.Color(photo).enhance(1.12) photo = ImageEnhance.Contrast(photo).enhance(1.07) photo = ImageEnhance.Brightness(photo).enhance(1.05) photo = photo.filter(ImageFilter.UnsharpMask(radius=2, percent=70, threshold=2)) canvas.paste(photo.convert("RGBA"), (0, 0)) def vignette(canvas, strength=0.30): x = np.linspace(0, 1, W, dtype=np.float32) y = np.linspace(0, 1, H, dtype=np.float32) X, Y = np.meshgrid(x, y) vx = np.clip((X - 0.5) / 0.62, -1, 1) vy = np.clip((Y - 0.5) / 0.66, -1, 1) v = 1 - strength * np.clip(np.sqrt(vx ** 2 + vy ** 2) - 0.25, 0, 1) ** 1.4 dark = Image.fromarray( (np.clip((1 - v) * 255, 0, 255)).astype(np.uint8), "L") canvas.alpha_composite(Image.composite( Image.new("RGBA", (W, H), DARK + (255,)), Image.new("RGBA", (W, H), (0, 0, 0, 0)), dark)) def top_scrim(canvas, h, max_alpha=235, plateau=0.72): """Grafitowa poświata od góry — pod blok tekstu na koronie drzewa. Pełne krycie do `plateau` wysokości, potem wygaszanie do zera.""" ay = np.linspace(0, 1, h, dtype=np.float32)[:, None] a = np.clip((1 - ay) / (1 - plateau), 0, 1) ** 1.4 * max_alpha scrim = Image.fromarray( (np.broadcast_to(a, (h, W))).astype(np.uint8), "L") dark = Image.new("RGBA", (W, h), GRAPHITE + (255,)) canvas.alpha_composite(Image.composite( dark, Image.new("RGBA", (W, h), (0, 0, 0, 0)), scrim), (0, 0)) def bottom_scrim(canvas, y0, max_alpha=170): h = H - y0 ay = np.linspace(0, 1, h, dtype=np.float32)[:, None] a = ay ** 1.7 * max_alpha scrim = Image.fromarray( (np.broadcast_to(a, (h, W))).astype(np.uint8), "L") dark = Image.new("RGBA", (W, h), DARK + (255,)) canvas.alpha_composite(Image.composite( dark, Image.new("RGBA", (W, h), (0, 0, 0, 0)), scrim), (0, y0)) def build(): canvas = Image.new("RGBA", (W, H), GRAPHITE + (255,)) cx = W / 2 cover_photo(canvas, PHOTO) vignette(canvas, strength=0.22) top_scrim(canvas, 720, plateau=0.70) bottom_scrim(canvas, 1180, max_alpha=140) # ---- tytuł na koronie drzewa ---- title = "SKUP GRZYBÓW" f_title = fit_font(title, FONT_BEBAS, W - 110, max_size=185) ty = 165 bb = f_title.getbbox(title) sh = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) ImageDraw.Draw(sh).text((cx - (bb[0] + bb[2]) / 2 + 5, ty - (bb[1] + bb[3]) / 2 + 9), title, font=f_title, fill=(0, 0, 0, 200)) canvas.alpha_composite(sh.filter(ImageFilter.GaussianBlur(12))) text_c(ImageDraw.Draw(canvas), cx, ty, title, f_title, YELLOW) # ---- miasto ---- f_city = ImageFont.truetype(FONT_XBOLD, 40) text_c_tracked(ImageDraw.Draw(canvas), cx, 285, "TRZCIANKA", f_city, WHITE, tracking=14) # ---- złoty separator ---- d = ImageDraw.Draw(canvas) lw = int(f_title.getlength(title) * 0.42) d.rounded_rectangle([cx - lw / 2, 336, cx + lw / 2, 344], radius=4, fill=YELLOW + (255,)) # ---- adres ---- f_adr = fit_font("os. Modrzewiowa 21", FONT_XBOLD, W - 160, max_size=56) text_c(d, cx, 412, "os. Modrzewiowa 21", f_adr, WHITE) f_ent = ImageFont.truetype(FONT_BOLD, 33) text_c(d, cx, 478, "wejście od strony ul. Łomnickiej", f_ent, SOFT) # ---- engagement na dole ---- f_sm = ImageFont.truetype(FONT_BOLD, 26) text_c(d, cx, 1305, "Udostępnij znajomemu grzybiarzowi!", f_sm, (210, 220, 212)) canvas.convert("RGB").save(OUT, quality=93) canvas.convert("RGB").resize((W // 2, H // 2), Image.LANCZOS).save( "reklama_facebook/FB_post_podglad.jpg", quality=88) print("zapisano:", OUT) if __name__ == "__main__": build()