Портал технической службы: наряды и чек-листы, задачи, отказы и неисправности, анализ надёжности, карты, склады, приложение для Android
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
"""Разметка скриншотов: рамка вокруг элемента, номерной значок и стрелка к нему."""
|
||||
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")
|
||||
Reference in New Issue
Block a user