103 lines
5.3 KiB
Python
103 lines
5.3 KiB
Python
"""Разметка скриншотов: рамка вокруг элемента, номерной значок и стрелка к нему."""
|
|
import math
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
RED = (225, 29, 72)
|
|
FONT = Path(__file__).resolve().parents[2] / "app" / "fonts" / "DejaVuSans-Bold.ttf"
|
|
|
|
|
|
def _arrow(d: ImageDraw.ImageDraw, p0, p1, w, head):
|
|
d.line([p0, p1], fill=RED, width=w)
|
|
ang = math.atan2(p1[1] - p0[1], p1[0] - p0[0])
|
|
tip = p1
|
|
a1, a2 = ang + math.radians(150), ang - math.radians(150)
|
|
d.polygon([tip, (tip[0] + head * math.cos(a1), tip[1] + head * math.sin(a1)),
|
|
(tip[0] + head * math.cos(a2), tip[1] + head * math.sin(a2))], fill=RED)
|
|
|
|
|
|
def annotate(img: Image.Image, marks, scale: float = 1.0) -> Image.Image:
|
|
"""marks: [(номер, (x, y, w, h) в пикселях изображения, направление up|down|left|right|auto)]"""
|
|
img = img.convert("RGBA")
|
|
over = Image.new("RGBA", img.size, (0, 0, 0, 0))
|
|
d = ImageDraw.Draw(over)
|
|
W, H = img.size
|
|
S = max(scale, 1.0)
|
|
font = ImageFont.truetype(str(FONT), int(17 * S))
|
|
pad, r, gap, lw = int(5 * S), int(15 * S), int(44 * S), max(2, int(3 * S))
|
|
for n, (x, y, w, h), direction in marks:
|
|
box = (x - pad, y - pad, x + w + pad, y + h + pad)
|
|
d.rounded_rectangle(box, radius=int(8 * S), fill=RED + (28,), outline=RED + (255,), width=lw)
|
|
cx, cy = x + w / 2, y + h / 2
|
|
if direction == "auto": # где больше свободного места
|
|
room = {"up": box[1], "down": H - box[3], "left": box[0], "right": W - box[2]}
|
|
direction = "up" if room["up"] > 70 * S and room["up"] >= room["left"] * 0.6 else max(room, key=room.get)
|
|
if direction == "up":
|
|
bc, a0, a1 = (cx, box[1] - gap), (cx, box[1] - gap + r), (cx, box[1])
|
|
elif direction == "down":
|
|
bc, a0, a1 = (cx, box[3] + gap), (cx, box[3] + gap - r), (cx, box[3])
|
|
elif direction == "left":
|
|
bc, a0, a1 = (box[0] - gap, cy), (box[0] - gap + r, cy), (box[0], cy)
|
|
else:
|
|
bc, a0, a1 = (box[2] + gap, cy), (box[2] + gap - r, cy), (box[2], cy)
|
|
# значок не должен выходить за край кадра
|
|
nx = min(max(bc[0], r + 4), W - r - 4)
|
|
ny = min(max(bc[1], r + 4), H - r - 4)
|
|
if direction in ("up", "down"):
|
|
a0, bc = (nx, a0[1]), (nx, bc[1])
|
|
a1 = (min(max(nx, box[0] + 6), box[2] - 6), a1[1])
|
|
else:
|
|
a0, bc = (a0[0], ny), (bc[0], ny)
|
|
a1 = (a1[0], min(max(ny, box[1] + 6), box[3] - 6))
|
|
_arrow(d, a0, a1, lw, int(11 * S))
|
|
bx, by = bc
|
|
d.ellipse((bx - r - 2, by - r - 2, bx + r + 2, by + r + 2), fill=(255, 255, 255, 255))
|
|
d.ellipse((bx - r, by - r, bx + r, by + r), fill=RED + (255,))
|
|
tw = d.textlength(str(n), font=font)
|
|
d.text((bx - tw / 2, by - font.size * 0.62), str(n), font=font, fill=(255, 255, 255, 255))
|
|
return Image.alpha_composite(img, over).convert("RGB")
|
|
|
|
|
|
def annotate_side(img: Image.Image, marks, margin: int, scale: float = 1.0, bg=(238, 241, 246)) -> Image.Image:
|
|
"""Вариант для узких экранов: скриншот на светлом фоне с полями, номера стоят в полях и стрелками указывают внутрь."""
|
|
img = img.convert("RGB")
|
|
W, H = img.size
|
|
S = max(scale, 1.0)
|
|
canvas = Image.new("RGB", (W + 2 * margin, H), bg)
|
|
canvas.paste(img, (margin, 0))
|
|
over = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
|
|
d = ImageDraw.Draw(over)
|
|
d.rectangle((margin - 1, 0, margin + W, H - 1), outline=(190, 196, 208, 255), width=max(2, int(2 * S)))
|
|
font = ImageFont.truetype(str(FONT), int(17 * S))
|
|
pad, r, lw = int(4 * S), int(15 * S), max(2, int(3 * S))
|
|
last = {"left": -10**6, "right": -10**6}
|
|
items = []
|
|
for idx, (n, (x, y, w, h), direction) in enumerate(marks):
|
|
x += margin
|
|
cx = x + w / 2
|
|
if direction in ("left", "right"):
|
|
side = direction
|
|
elif abs(w) > W * 0.6:
|
|
side = "left" if idx % 2 == 0 else "right"
|
|
else:
|
|
side = "left" if cx < margin + W / 2 else "right"
|
|
items.append((y + h / 2, idx, n, (x, y, w, h), side))
|
|
for cy, idx, n, (x, y, w, h), side in sorted(items):
|
|
box = (x - pad, y - pad, x + w + pad, y + h + pad)
|
|
d.rounded_rectangle(box, radius=int(8 * S), fill=RED + (28,), outline=RED + (255,), width=lw)
|
|
by = min(max(cy, r + 4), H - r - 4)
|
|
by = max(by, last[side] + 2 * r + int(10 * S))
|
|
last[side] = by
|
|
bx = margin / 2 if side == "left" else margin + W + margin / 2
|
|
a_end_y = min(max(by, box[1] + 6), box[3] - 6)
|
|
if side == "left":
|
|
_arrow(d, (bx + r, by), (box[0], a_end_y), lw, int(11 * S))
|
|
else:
|
|
_arrow(d, (bx - r, by), (box[2], a_end_y), lw, int(11 * S))
|
|
d.ellipse((bx - r - 2, by - r - 2, bx + r + 2, by + r + 2), fill=(255, 255, 255, 255))
|
|
d.ellipse((bx - r, by - r, bx + r, by + r), fill=RED + (255,))
|
|
tw = d.textlength(str(n), font=font)
|
|
d.text((bx - tw / 2, by - font.size * 0.62), str(n), font=font, fill=(255, 255, 255, 255))
|
|
return Image.alpha_composite(canvas.convert("RGBA"), over).convert("RGB")
|