vitrina / src /postprocess.py
nvikou's picture
Витрина: первый деплой
4448e70 verified
Raw History Blame Contribute Delete
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 = ("Слева сверху", "Справа сверху", "Слева снизу", "Справа снизу")
@lru_cache(maxsize=16)
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")