import base64 import io import numpy as np from PIL import Image, ImageDraw Image.MAX_IMAGE_PIXELS = None LOGO_BAR_SEARCH_FROM = 0.88 # 하단 12%에서만 밝은 띠를 찾는다 LOGO_BAR_BRIGHTNESS = 200 LOGO_BAR_MIN_ROWS = 20 def content_box(frame: Image.Image) -> tuple[int, int, int, int]: """레터박스(검정·흰 여백)를 걷어낸 실제 포스터 영역.""" brightness = np.asarray(frame.convert("RGB")).astype(np.float32).mean(2) live = (brightness > 14) & (brightness < 244) cols = np.nonzero(live.any(0))[0] rows = np.nonzero(live.any(1))[0] return int(cols.min()), int(rows.min()), int(cols.max()) + 1, int(rows.max()) + 1 def bar_box(poster: Image.Image) -> tuple[int, int, int, int] | None: """하단 주최·주관·후원 로고 띠(밝은 가로 밴드)의 bbox.""" pixels = np.asarray(poster.convert("RGB")).astype(np.float32) height, width = pixels.shape[:2] search_from = int(height * LOGO_BAR_SEARCH_FROM) rows = np.nonzero(pixels[search_from:].mean(axis=(1, 2)) > LOGO_BAR_BRIGHTNESS)[0] if len(rows) < LOGO_BAR_MIN_ROWS: return None return 0, search_from + rows.min() - 4, width, min(height, search_from + rows.max() + 5) def logo_bar(poster: Image.Image) -> Image.Image | None: box = bar_box(poster) return poster.crop(box) if box else None def round_mask(size: tuple[int, int], radius_ratio: float = 0.08) -> Image.Image: """사각 패치를 둥근 카드 위에 얹을 때 모서리가 튀는 것을 막는 알파 마스크.""" width, height = size mask = Image.new("L", size, 0) ImageDraw.Draw(mask).rounded_rectangle( (0, 0, width - 1, height - 1), radius=max(1, round(min(width, height) * radius_ratio)), fill=255) return mask def to_data_uri(image: Image.Image, max_size: tuple[int, int] = (1100, 1600)) -> str: image = image.copy() image.thumbnail(max_size, Image.LANCZOS) buffer = io.BytesIO() image.save(buffer, "JPEG", quality=92) return "data:image/jpeg;base64," + base64.b64encode(buffer.getvalue()).decode()