# -*- coding: utf-8 -*- """ Banery wielkoformatowe SKUP GRZYBÓW — 3000 x 1200 mm @ 150 DPI. Canvas: 17717 x 7087 px. Wyjście: PDF 1:1 (bezstratny) + podgląd JPG 1:10. """ import io import numpy as np from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance import img2pdf Image.MAX_IMAGE_PIXELS = None # ---------- parametry ---------- DPI = 150 MM = DPI / 25.4 W, H = round(3000 * MM), round(1200 * MM) # 17717 x 7087 SAFE = round(100 * MM) # strefa bezpieczna 100 mm GRAPHITE_2 = (20, 33, 26) YELLOW = (255, 208, 0) # #FFD000 WHITE = (255, 255, 255) BLACK = (5, 8, 6) FONT_BEBAS = "fonts/BebasNeue-Regular.ttf" FONT_BLACK = "fonts/Montserrat-Black.ttf" FONT_XBOLD = "fonts/Montserrat-ExtraBold.ttf" FONT_BOLD = "fonts/Montserrat-Bold.ttf" PHOTO = "assets/borowiki_final.jpg" def fit_font(text, font_path, target_w, max_size=6000): lo, hi = 50, 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): """Tekst wyśrodkowany optycznie w (cx, cy).""" bb = font.getbbox(text) draw.text((cx - (bb[0] + bb[2]) / 2, cy - (bb[1] + bb[3]) / 2), text, font=font, fill=fill + (255,)) def text_l(draw, x, cy, text, font, fill): """Tekst do lewej krawędzi x, wyśrodkowany pionowo na cy.""" bb = font.getbbox(text) draw.text((x, cy - (bb[1] + bb[3]) / 2), text, font=font, fill=fill + (255,)) def draw_3d_text(base, x_top, text, font, fill, depth=85): """Ekstruzja 3D + miękki cień. x_top = (x, y) punktu anchor 'la'.""" x, y = x_top soft = Image.new("RGBA", base.size, (0, 0, 0, 0)) ImageDraw.Draw(soft).text((x + depth + 30, y + depth + 45), text, font=font, fill=(0, 0, 0, 200)) base.alpha_composite(soft.filter(ImageFilter.GaussianBlur(60))) ext = Image.new("RGBA", base.size, (0, 0, 0, 0)) ed = ImageDraw.Draw(ext) for i in range(depth // 3, 0, -1): ed.text((x + i * 3, y + i * 2), text, font=font, fill=BLACK + (255,)) base.alpha_composite(ext) ImageDraw.Draw(base).text((x, y), text, font=font, fill=fill + (255,)) def rounded_tile(size_wh, label, label_h_ratio=0.32): """Biały kafel z ciemnym nagłówkiem; wnętrze 100% puste i białe.""" w, h = size_wh tile = Image.new("RGBA", (w, h), (0, 0, 0, 0)) d = ImageDraw.Draw(tile) rad = int(h * 0.14) d.rounded_rectangle([0, 0, w - 1, h - 1], radius=rad, fill=WHITE + (255,)) band_h = int(h * label_h_ratio) band = Image.new("RGBA", (w, band_h + rad), (0, 0, 0, 0)) ImageDraw.Draw(band).rounded_rectangle( [0, 0, w - 1, band_h + rad], radius=rad, fill=GRAPHITE_2 + (255,)) tile.alpha_composite(band, (0, 0)) f = fit_font(label, FONT_XBOLD, int(w * 0.84), max_size=int(band_h * 0.60)) d = ImageDraw.Draw(tile) text_c(d, w // 2, band_h // 2, label, f, WHITE) return tile def paste_with_shadow(canvas, tile, pos, blur=55, offset=(18, 30), alpha=150): sh = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) mask = tile.split()[3].point(lambda a: int(a * alpha / 255)) sh.paste(Image.new("RGBA", tile.size, (0, 0, 0, 255)), (pos[0] + offset[0], pos[1] + offset[1]), mask) canvas.alpha_composite(sh.filter(ImageFilter.GaussianBlur(blur))) canvas.alpha_composite(tile, pos) 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([26, 40, 32], dtype=np.float32) bot = np.array([9, 14, 11], dtype=np.float32) grad = top[None, None, :] * (1 - Y[..., None]) + \ bot[None, None, :] * Y[..., None] glow = np.exp(-(((X - 0.82) ** 2) / 0.06 + ((Y - 0.75) ** 2) / 0.10)) grad += glow[..., None] * np.array([14, 26, 16], 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.32 * 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") def composite_photo(canvas, photo_path, x_start, feather): photo = Image.open(photo_path).convert("RGB") pw = W - x_start scale = max(pw / 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 = max(0, (nw - pw) // 2) top = max(0, nh - H) photo = photo.crop((left, top, left + pw, top + H)) photo = ImageEnhance.Color(photo).enhance(1.08) photo = ImageEnhance.Contrast(photo).enhance(1.05) photo = photo.filter(ImageFilter.UnsharpMask(radius=3, percent=60, threshold=2)) ax = np.linspace(0, 1, pw, dtype=np.float32) ay = np.linspace(0, 1, H, dtype=np.float32) AX, AY = np.meshgrid(ax, ay) a = np.clip(AX / (feather / pw), 0, 1) ** 1.5 a *= (0.25 + 0.75 * np.clip(AY / 0.32, 0, 1)) a *= (0.55 + 0.45 * np.clip((1 - AY) / 0.18, 0, 1)) mask = Image.fromarray((a * 255).astype(np.uint8), "L") layer = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) layer.paste(photo, (x_start, 0), mask) canvas.alpha_composite(layer) ay_full = np.linspace(0, 1, H, dtype=np.float32)[:, None] scr = np.clip(1 - ay_full / (2700 / H), 0, 1) ** 1.2 * 0.55 scrim = Image.fromarray( (np.broadcast_to(scr, (H, W)) * 255).astype(np.uint8), "L") dark = Image.new("RGBA", canvas.size, (0, 0, 0, 255)) canvas.alpha_composite(Image.composite( dark, Image.new("RGBA", canvas.size, (0, 0, 0, 0)), scrim)) def draw_header(canvas, head_w): """Nagłówek SKUP GRZYBÓW + złota linia akcentowa. Zwraca dół nagłówka.""" d = ImageDraw.Draw(canvas) f_head = fit_font("SKUP GRZYBÓW", FONT_BEBAS, head_w) bb = f_head.getbbox("SKUP GRZYBÓW") y_draw = SAFE + 30 - bb[1] draw_3d_text(canvas, (SAFE + 60, y_draw), "SKUP GRZYBÓW", f_head, YELLOW) head_bottom = y_draw + bb[3] # złota linia pod nagłówkiem — subtelny akcent lw = int(f_head.getlength("SKUP GRZYBÓW") * 0.55) ly = head_bottom + 130 d.rounded_rectangle([SAFE + 60, ly, SAFE + 60 + lw, ly + 26], radius=13, fill=YELLOW + (255,)) return ly + 26 def arrow_poly(x0, x1, yc, shaft_ratio=0.42, head_ratio=0.45): head_w = (x1 - x0) * head_ratio sh = (x1 - x0) * shaft_ratio / 2 return [(x0, yc - sh), (x1 - head_w, yc - sh), (x1 - head_w, yc - sh * 2.1), (x1, yc), (x1 - head_w, yc + sh * 2.1), (x1 - head_w, yc + sh), (x0, yc + sh)] def draw_arrow(canvas, x0, x1, yc): d = ImageDraw.Draw(canvas) pts = arrow_poly(x0, x1, yc) sh = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) ImageDraw.Draw(sh).polygon([(x + 30, y + 45) for x, y in pts], fill=(0, 0, 0, 190)) canvas.alpha_composite(sh.filter(ImageFilter.GaussianBlur(40))) d.polygon(pts, fill=YELLOW + (255,)) d.line(pts + [pts[0]], fill=BLACK + (255,), width=30, joint="curve") def save_outputs(canvas, stem): rgb = canvas.convert("RGB") prev = rgb.resize((W // 10, H // 10), Image.LANCZOS) prev.save(f"{stem}_podglad_1-10.jpg", quality=92, dpi=(15, 15)) buf = io.BytesIO() rgb.save(buf, format="PNG", optimize=False) layout = img2pdf.get_fixed_dpi_layout_fun((DPI, DPI)) with open(f"{stem}_300x120cm_150dpi.pdf", "wb") as f: f.write(img2pdf.convert(buf.getvalue(), layout_fun=layout)) print("zapisano:", stem, flush=True) # ================= PROJEKT 1 — LOKALNY ================= def banner1(): canvas = make_background().convert("RGBA") composite_photo(canvas, PHOTO, x_start=W - 6800, feather=3100) head_bottom = draw_header(canvas, 15700) tile_w = 5100 y0 = head_bottom + 380 tile_h = (H - SAFE) - y0 x1 = SAFE + 60 x2 = x1 + tile_w + 380 paste_with_shadow(canvas, rounded_tile((tile_w, tile_h), "GODZINY OTWARCIA"), (x1, y0)) paste_with_shadow(canvas, rounded_tile((tile_w, tile_h), "NUMER TELEFONU"), (x2, y0)) save_outputs(canvas, "Baner1_LOKALNY") # ================= PROJEKT 2 — DOJAZDOWY ================= def banner2(): canvas = make_background().convert("RGBA") composite_photo(canvas, PHOTO, x_start=W - 5300, feather=2400) head_bottom = draw_header(canvas, 13500) d = ImageDraw.Draw(canvas) content_top = head_bottom + 260 bottom = H - SAFE - 30 # ---- kolumna kafli (prawa część strefy tekstów, na ciemnym tle) ---- tile_w = 2800 tile_h = (bottom - content_top - 100) // 2 tx0 = 9700 ty1 = content_top ty2 = content_top + tile_h + 100 paste_with_shadow(canvas, rounded_tile((tile_w, tile_h), "GODZINY OTWARCIA"), (tx0, ty1)) paste_with_shadow(canvas, rounded_tile((tile_w, tile_h), "TELEFON"), (tx0, ty2)) # ---- moduł manewrowy: żółty panel drogowy [850 M ➔] ---- pan_y0 = content_top pan_h = 1300 pan_y1 = pan_y0 + pan_h pan_x0, pan_x1 = SAFE + 60, 6000 ay_c = (pan_y0 + pan_y1) // 2 sh = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) ImageDraw.Draw(sh).rounded_rectangle( [pan_x0 + 28, pan_y0 + 45, pan_x1 + 28, pan_y1 + 45], radius=160, fill=(0, 0, 0, 180)) canvas.alpha_composite(sh.filter(ImageFilter.GaussianBlur(50))) d.rounded_rectangle([pan_x0, pan_y0, pan_x1, pan_y1], radius=160, fill=YELLOW + (255,), outline=BLACK + (255,), width=34) ix = pan_x0 + 400 f850 = fit_font("850 M", FONT_BEBAS, 2400, max_size=int(pan_h * 0.88)) text_l(d, ix, ay_c, "850 M", f850, BLACK) ax0 = ix + f850.getlength("850 M") + 380 ax1 = pan_x1 - 400 d.polygon(arrow_poly(ax0, ax1, ay_c), fill=BLACK + (255,)) # podpis manewru — dwie linie, wyśrodkowane na wysokości panelu tx = pan_x1 + 420 f_turn = fit_font("W PRAWO", FONT_BEBAS, 3100, max_size=850) text_l(d, tx, ay_c - int(f_turn.size * 0.58), "SKRĘĆ", f_turn, WHITE) text_l(d, tx, ay_c + int(f_turn.size * 0.58), "W PRAWO", f_turn, YELLOW) # ---- adres ---- adr_cy = pan_y1 + 580 f_adr = fit_font("os. Modrzewiowe 21", FONT_BLACK, 8800, max_size=880) sh3 = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) ImageDraw.Draw(sh3).text((SAFE + 74, adr_cy + 20), "os. Modrzewiowe 21", font=f_adr, fill=(0, 0, 0, 200)) canvas.alpha_composite(sh3.filter(ImageFilter.GaussianBlur(18))) text_l(d, SAFE + 60, adr_cy, "os. Modrzewiowe 21", f_adr, YELLOW) # ---- pigułka nawigacyjna ---- pill_txt = "WEJŚCIE I WJAZD: od strony ul. Łomnickiej" f_pill = fit_font(pill_txt, FONT_BOLD, 8400, max_size=400) pw = f_pill.getlength(pill_txt) px0 = SAFE + 60 py0 = adr_cy + int(f_adr.size * 0.55) + 120 py1 = min(bottom, py0 + f_pill.size + 220) py0 = py1 - (f_pill.size + 220) ph = py1 - py0 py0 = py1 - ph px1 = px0 + pw + 560 sh4 = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) ImageDraw.Draw(sh4).rounded_rectangle( [px0 + 18, py0 + 28, px1 + 18, py1 + 28], radius=(py1 - py0) // 2, fill=(0, 0, 0, 170)) canvas.alpha_composite(sh4.filter(ImageFilter.GaussianBlur(35))) d.rounded_rectangle([px0, py0, px1, py1], radius=(py1 - py0) // 2, fill=(16, 26, 21, 255), outline=YELLOW + (255,), width=16) text_c(d, (px0 + px1) / 2, (py0 + py1) / 2, pill_txt, f_pill, WHITE) save_outputs(canvas, "Baner2_DOJAZDOWY") if __name__ == "__main__": print("canvas:", W, "x", H) banner1() banner2()