Портал технической службы: наряды и чек-листы, задачи, отказы и неисправности, анализ надёжности, карты, склады, приложение для Android

This commit is contained in:
darkorion
2026-10-13 09:09:53 +03:00
commit 67e411eea8
246 changed files with 23152 additions and 0 deletions
+102
View File
@@ -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")