Download src/postprocess.py from nvikou/vitrina: direct link, hf CLI and curl.
- Browser
- Download file 5.91 kB
-
https://huggingface.co/spaces/nvikou/vitrina/resolve/main/src/postprocess.py
- Command line
-
hf download hf://spaces/nvikou/vitrina/src/postprocess.py
-
curl -L -o postprocess.py https://huggingface.co/spaces/nvikou/vitrina/resolve/main/src/postprocess.py
5.91 kB
| """Постобработка изображений под требования площадок. | |
| Генератор отдаёт картинку в своём размере (1024×1536 и т.п.), а площадкам нужны | |
| свои пропорции и разрешения. Здесь: подгонка холста, плашки с текстом, | |
| водяной знак. Всё на Pillow, без внешних сервисов. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from functools import lru_cache | |
| from pathlib import Path | |
| from PIL import Image, ImageDraw, ImageFont | |
| log = logging.getLogger(__name__) | |
| # Размеры под площадки. Значения — требования/рекомендации на момент сборки, | |
| # площадки их периодически меняют: проверяйте в справке площадки. | |
| SIZE_PRESETS: dict[str, tuple[int, int] | None] = { | |
| "Wildberries — 900×1200": (900, 1200), | |
| "Ozon — 1200×1600": (1200, 1600), | |
| "Яндекс Маркет — 1000×1000": (1000, 1000), | |
| "Квадрат — 1080×1080": (1080, 1080), | |
| "Сторис — 1080×1920": (1080, 1920), | |
| "Баннер — 1920×1080": (1920, 1080), | |
| "Как сгенерировано": None, | |
| } | |
| FONT_CANDIDATES = ( | |
| "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", | |
| "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", | |
| "/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf", | |
| "/System/Library/Fonts/Supplemental/Arial Bold.ttf", | |
| "C:/Windows/Fonts/arialbd.ttf", | |
| ) | |
| BADGE_POSITIONS = ("Слева сверху", "Справа сверху", "Слева снизу", "Справа снизу") | |
| def _font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: | |
| for path in FONT_CANDIDATES: | |
| if Path(path).exists(): | |
| try: | |
| return ImageFont.truetype(path, size) | |
| except OSError: # pragma: no cover | |
| continue | |
| log.warning("Системный шрифт не найден, использую встроенный Pillow") | |
| try: | |
| return ImageFont.load_default(size=size) | |
| except TypeError: # Pillow < 10.1 | |
| return ImageFont.load_default() | |
| def fit_canvas( | |
| image: Image.Image, | |
| target: tuple[int, int] | None, | |
| mode: str = "Обрезать по краям", | |
| background: str = "#FFFFFF", | |
| ) -> Image.Image: | |
| """Приводит картинку к нужному размеру. | |
| «Обрезать по краям» (cover) — кадр заполнен целиком, края срезаются. | |
| «Вписать целиком» (contain) — кадр виден полностью, поля заливаются цветом. | |
| """ | |
| if not target: | |
| return image.convert("RGB") | |
| tw, th = target | |
| img = image.convert("RGB") | |
| scale = max(tw / img.width, th / img.height) | |
| if mode.startswith("Вписать"): | |
| scale = min(tw / img.width, th / img.height) | |
| new_size = (max(1, round(img.width * scale)), max(1, round(img.height * scale))) | |
| img = img.resize(new_size, Image.LANCZOS) | |
| canvas = Image.new("RGB", (tw, th), background) | |
| canvas.paste(img, ((tw - img.width) // 2, (th - img.height) // 2)) | |
| return canvas | |
| def _wrap(draw: ImageDraw.ImageDraw, text: str, font, max_width: int) -> list[str]: | |
| lines: list[str] = [] | |
| for paragraph in text.splitlines(): | |
| words, current = paragraph.split(), "" | |
| for word in words: | |
| probe = f"{current} {word}".strip() | |
| if draw.textlength(probe, font=font) <= max_width or not current: | |
| current = probe | |
| else: | |
| lines.append(current) | |
| current = word | |
| lines.append(current) | |
| return [ln for ln in lines if ln] | |
| def add_badge( | |
| image: Image.Image, | |
| text: str, | |
| *, | |
| position: str = "Слева сверху", | |
| fill: str = "#E4287C", | |
| text_color: str = "#FFFFFF", | |
| scale: float = 0.032, | |
| ) -> Image.Image: | |
| """Плашка с текстом поверх изображения — то, чем живут карточки на площадках.""" | |
| if not text or not text.strip(): | |
| return image | |
| img = image.convert("RGB") | |
| draw = ImageDraw.Draw(img) | |
| font_size = max(14, int(img.height * scale)) | |
| font = _font(font_size) | |
| pad = int(font_size * 0.55) | |
| margin = int(img.width * 0.04) | |
| lines = _wrap(draw, text.strip(), font, int(img.width * 0.5)) | |
| line_h = int(font_size * 1.25) | |
| box_w = int(max(draw.textlength(ln, font=font) for ln in lines)) + pad * 2 | |
| box_h = line_h * len(lines) + pad * 2 | |
| x = margin if position.startswith("Слева") else img.width - margin - box_w | |
| y = margin if "сверху" in position else img.height - margin - box_h | |
| radius = int(min(box_w, box_h) * 0.22) | |
| draw.rounded_rectangle([x, y, x + box_w, y + box_h], radius=radius, fill=fill) | |
| for i, line in enumerate(lines): | |
| draw.text((x + pad, y + pad + i * line_h), line, font=font, fill=text_color) | |
| return img | |
| def add_watermark(image: Image.Image, text: str, opacity: int = 110) -> Image.Image: | |
| """Полупрозрачная подпись в углу — для черновиков и согласований.""" | |
| if not text or not text.strip(): | |
| return image | |
| base = image.convert("RGBA") | |
| layer = Image.new("RGBA", base.size, (0, 0, 0, 0)) | |
| draw = ImageDraw.Draw(layer) | |
| font_size = max(12, int(base.height * 0.022)) | |
| font = _font(font_size) | |
| margin = int(base.width * 0.025) | |
| tw = draw.textlength(text, font=font) | |
| draw.text( | |
| (base.width - tw - margin, base.height - font_size - margin), | |
| text, | |
| font=font, | |
| fill=(255, 255, 255, opacity), | |
| ) | |
| return Image.alpha_composite(base, layer).convert("RGB") | |