Files
GostGenerator/web/backend/app/services/pdf_gost_frame.py
T
2026-09-02 15:42:48 +03:00

343 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""GOST title block / frame drawing (ported from controller/pdfcontroller.cpp)."""
from __future__ import annotations
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
FRAME_LINE = 0.5 * mm
M_LEFT = 20 * mm
M_TOP = 5 * mm
M_RIGHT = 5 * mm
M_BOTTOM = 5 * mm
DOC_TYPES = {
"perechen": 1,
"specification_pcb": 2,
"specification": 3,
"vedomost": 4,
}
PRIMARY_APP_FIELD = {1: 251, 2: 252, 3: 253, 4: 254}
def _ins(inscriptions: dict[int, str], num: int) -> str:
return str(inscriptions.get(num, "") or "")
class Inner:
"""Inner document area in ReportLab coordinates (origin bottom-left)."""
def __init__(self, page_w: float, page_h: float):
self.page_w = page_w
self.page_h = page_h
self.left = M_LEFT
self.bottom = M_BOTTOM
self.right = page_w - M_RIGHT
self.top = page_h - M_TOP
def rect(self, x: float, y: float, w: float, h: float) -> None:
pass
def _set_frame_pen(c: canvas.Canvas) -> None:
c.setLineWidth(FRAME_LINE)
c.setStrokeColorRGB(0, 0, 0)
def _txt(
c: canvas.Canvas,
x: float,
y: float,
w: float,
h: float,
text: str,
font: str,
size: float,
align: str = "center",
valign: str = "center",
) -> None:
if not text:
return
c.setFont(font, size)
tw = c.stringWidth(text, font, size)
if align == "center":
tx = x + (w - tw) / 2
elif align == "right":
tx = x + w - tw - 0.5 * mm
else:
tx = x + 0.5 * mm
if valign == "bottom":
ty = y + 1 * mm
elif valign == "top":
ty = y + h - size * 0.35 - 0.5 * mm
else:
ty = y + (h - size * 0.35) / 2
c.drawString(tx, ty, text)
def _txt_multiline(
c: canvas.Canvas,
x: float,
y: float,
w: float,
h: float,
text: str,
font: str,
size: float,
max_lines: int = 3,
) -> None:
if not text:
return
c.setFont(font, size)
words = text.split()
lines: list[str] = []
current = ""
for word in words:
test = f"{current} {word}".strip()
if c.stringWidth(test, font, size) <= w - mm:
current = test
else:
if current:
lines.append(current)
current = word
if current:
lines.append(current)
lines = lines[:max_lines]
step = size * 1.2
start_y = y + h - size * 0.5 - mm
for i, line in enumerate(lines):
tw = c.stringWidth(line, font, size)
c.drawString(x + (w - tw) / 2, start_y - i * step, line)
def _draw_inner_border(c: canvas.Canvas, inner: Inner) -> None:
_set_frame_pen(c)
c.rect(inner.left, inner.bottom, inner.right - inner.left, inner.top - inner.bottom)
def _draw_left_binding_strip(c: canvas.Canvas, inner: Inner) -> tuple[float, float, float, float]:
"""Left vertical strip (12×145 mm) at bottom-left of inner area."""
w = 12 * mm
h = 145 * mm
x = inner.left - w
y = inner.bottom
c.rect(x, y, w, h)
# horizontal dividers (from bottom): 25, 35, 25, 25, 35 mm sections
offsets = [25, 60, 85, 110, 145]
for off in offsets:
yy = y + off * mm
c.line(x, yy, inner.left, yy)
c.line(x + w + 5 * mm, y, x + w + 5 * mm, y + h)
return x, y, w, h
def _draw_left_binding_text(c: canvas.Canvas, strip_x: float, strip_y: float, inscriptions: dict[int, str], font: str) -> None:
size = 12
c.saveState()
c.translate(strip_x + 12 * mm, strip_y + 145 * mm)
c.rotate(-90)
labels = [
(0, 25, "Инв. № подл"),
(25, 35, "Подп. и дата"),
(60, 25, "Взам. инв. №"),
(85, 25, "Инв. № дубл"),
(110, 35, "Подп. и дата"),
]
for ox, ow, label in labels:
_txt(c, ox * mm, 0, ow * mm, 5 * mm, label, font, size)
vals = [(0, 25, 19), (25, 35, 20), (60, 25, 21), (85, 25, 22), (110, 35, 23)]
for ox, ow, field in vals:
_txt(c, ox * mm, 5 * mm, ow * mm, 7 * mm, _ins(inscriptions, field), font, size)
c.restoreState()
def _draw_top_left_strip(c: canvas.Canvas, inner: Inner, inscriptions: dict[int, str], doc_type: int, font: str) -> None:
w = 12 * mm
h = 120 * mm
x = inner.left - w
y = inner.top - h
c.rect(x, y, w, h)
c.line(x + w + 5 * mm, y, x + w + 5 * mm, y + h)
c.line(x, y + 60 * mm, inner.left, y + 60 * mm)
c.saveState()
c.translate(x + w, y)
c.rotate(-90)
_txt(c, 0, 0, 60 * mm, 5 * mm, "Справ. №", font, 12)
_txt(c, 60 * mm, 0, 60 * mm, 5 * mm, "Перв. примен.", font, 12)
_txt(c, 0, 5 * mm, 60 * mm, 7 * mm, _ins(inscriptions, 24), font, 12)
_txt(
c,
60 * mm,
5 * mm,
60 * mm,
7 * mm,
_ins(inscriptions, PRIMARY_APP_FIELD.get(doc_type, 25)),
font,
12,
)
c.restoreState()
def _draw_stamp_first(c: canvas.Canvas, inner: Inner, inscriptions: dict[int, str], doc_type: int, page: int, total: int, font: str) -> None:
mw, mh = 185 * mm, 40 * mm
x = inner.right - mw
y = inner.bottom
c.rect(x, y, mw, mh)
# horizontal lines
for dy in (5, 10, 15, 20, 25, 30, 35):
if dy <= 10 or dy >= 20:
c.line(x, y + dy * mm, x + (7 + 10 + 23 + 15 + 10) * mm, y + dy * mm)
else:
c.line(x, y + dy * mm, x + mw, y + dy * mm)
c.line(x + 65 * mm, y, x + 65 * mm, y + mh)
for dx in (7, 17, 40, 55):
c.line(x + dx * mm, y, x + dx * mm, y + (15 * mm if dx == 7 else mh))
c.line(x + mw - 50 * mm, y + 15 * mm, x + mw - 50 * mm, y + mh)
c.line(x + mw - 45 * mm, y + 20 * mm, x + mw - 45 * mm, y + 25 * mm)
c.line(x + mw - 40 * mm, y + 20 * mm, x + mw - 40 * mm, y + 25 * mm)
c.line(x + mw - 35 * mm, y + 15 * mm, x + mw - 35 * mm, y + 25 * mm)
c.line(x + mw - 20 * mm, y + 15 * mm, x + mw - 20 * mm, y + 25 * mm)
c.line(x + mw - 50 * mm, y + 20 * mm, x + mw, y + 20 * mm)
c.line(x + mw - 50 * mm, y + 25 * mm, x + mw, y + 25 * mm)
# idk block — 22 mm directly above main stamp top
ix = x + mw - 120 * mm
iy = y + mh
iw, ih = 120 * mm, 22 * mm
c.line(ix, iy, ix, iy + ih)
c.line(ix + iw, iy, ix + iw, iy + ih)
c.line(ix, iy + ih, ix + iw, iy + ih)
c.line(ix, iy + 14 * mm, ix + iw, iy + 14 * mm)
c.line(ix + 14 * mm, iy, ix + 14 * mm, iy + 14 * mm)
c.line(ix + 67 * mm, iy, ix + 67 * mm, iy + 14 * mm)
_txt(c, ix, iy, 14 * mm, 14 * mm, _ins(inscriptions, 27), font, 12)
_txt(c, ix + 14 * mm, iy, 53 * mm, 14 * mm, _ins(inscriptions, 28), font, 12)
_txt(c, ix + 67 * mm, iy, 53 * mm, 14 * mm, _ins(inscriptions, 29), font, 12)
_txt(c, ix, iy + 14 * mm, iw, 8 * mm, _ins(inscriptions, 30), font, 12)
# change block headers
_txt(c, x, y + mh - 5 * mm, 7 * mm, 5 * mm, _ins(inscriptions, 14), font, 12)
_txt(c, x + 7 * mm, y + mh - 5 * mm, 10 * mm, 5 * mm, _ins(inscriptions, 15), font, 12)
_txt(c, x + 17 * mm, y + mh - 5 * mm, 23 * mm, 5 * mm, _ins(inscriptions, 16), font, 12)
_txt(c, x + 40 * mm, y + mh - 5 * mm, 15 * mm, 5 * mm, _ins(inscriptions, 17), font, 12)
_txt(c, x + 55 * mm, y + mh - 5 * mm, 10 * mm, 5 * mm, _ins(inscriptions, 18), font, 12)
_txt(c, x, y + mh - 10 * mm, 7 * mm, 5 * mm, "Изм.", font, 12)
_txt(c, x + 7 * mm, y + mh - 10 * mm, 10 * mm, 5 * mm, "Лист", font, 12)
_txt(c, x + 17 * mm, y + mh - 10 * mm, 23 * mm, 5 * mm, "№ докум.", font, 12)
_txt(c, x + 40 * mm, y + mh - 10 * mm, 15 * mm, 5 * mm, "Подп.", font, 12)
_txt(c, x + 55 * mm, y + mh - 10 * mm, 10 * mm, 5 * mm, "Дата", font, 12)
_txt(c, x + 1 * mm, y + mh - 15 * mm, 16 * mm, 5 * mm, "Разраб.", font, 12, "left")
_txt(c, x + 1 * mm, y + mh - 20 * mm, 16 * mm, 5 * mm, "Пров.", font, 12, "left")
_txt(c, x + 1 * mm, y + mh - 25 * mm, 16 * mm, 5 * mm, _ins(inscriptions, 10), font, 12, "left")
_txt(c, x + 18 * mm, y + mh - 25 * mm, 22 * mm, 5 * mm, _ins(inscriptions, 11), font, 12, "left")
_txt(c, x + 18 * mm, y + mh - 15 * mm, 22 * mm, 5 * mm, _ins(inscriptions, 111), font, 12, "left")
_txt(c, x + 18 * mm, y + mh - 20 * mm, 22 * mm, 5 * mm, _ins(inscriptions, 112), font, 12, "left")
_txt(c, x + 18 * mm, y + mh - 30 * mm, 22 * mm, 5 * mm, _ins(inscriptions, 113), font, 12, "left")
_txt(c, x + 18 * mm, y + mh - 35 * mm, 22 * mm, 5 * mm, _ins(inscriptions, 114), font, 12, "left")
_txt(c, x + 41 * mm, y + mh - 25 * mm, 14 * mm, 5 * mm, _ins(inscriptions, 12), font, 12, "left")
_txt(c, x + 56 * mm, y + mh - 25 * mm, 9 * mm, 5 * mm, _ins(inscriptions, 13), font, 12, "left")
_txt(c, x + 1 * mm, y + mh - 30 * mm, 16 * mm, 5 * mm, "Н. контр.", font, 12, "left")
_txt(c, x + 1 * mm, y + mh - 35 * mm, 16 * mm, 5 * mm, "Утв.", font, 12, "left")
# product name / doc title area
if doc_type != 4:
name_text = _ins(inscriptions, 1002 if doc_type == 3 else 1)
_txt_multiline(c, x + 65 * mm, y + mh - 40 * mm, 70 * mm, 25 * mm, name_text, font, 18)
if doc_type == 1:
_txt(c, x + 65 * mm, y + mh - 40 * mm, 70 * mm, 25 * mm, "Перечень элементов", font, 12, valign="bottom")
else:
_txt_multiline(c, x + 65 * mm, y + mh - 35 * mm, 70 * mm, 20 * mm, _ins(inscriptions, 1), font, 18)
_txt(c, x + 65 * mm, y + mh - 35 * mm, 70 * mm, 5 * mm, _ins(inscriptions, 301), font, 12)
_txt(c, x + 65 * mm, y, 70 * mm, 20 * mm, "Ведомость покупных изделий", font, 12, valign="bottom")
_txt(c, x + 65 * mm, y, 70 * mm, 5 * mm, "Копировал", font, 12, "left")
title = _ins(inscriptions, 2)
if doc_type == 1:
title = f"{title} ПЭ3".strip()
elif doc_type == 4:
title = f"{title} ВП".strip()
elif doc_type == 3:
title = _ins(inscriptions, 1001)
_txt(c, x + 65 * mm, y + mh - 15 * mm, 120 * mm, 15 * mm, title, font, 18)
_txt(c, x + 135 * mm, y + mh - 15 * mm, 15 * mm, 5 * mm, "Лит.", font, 12)
_txt(c, x + 150 * mm, y + mh - 15 * mm, 15 * mm, 5 * mm, "Лист", font, 12)
_txt(c, x + 165 * mm, y + mh - 15 * mm, 20 * mm, 5 * mm, "Листов", font, 12)
littera = _ins(inscriptions, 4)
for i in range(min(3, len(littera))):
_txt(c, x + (135 + 5 * i) * mm, y + mh - 20 * mm, 5 * mm, 5 * mm, littera[i], font, 12)
if total > 1:
_txt(c, x + 150 * mm, y + mh - 20 * mm, 15 * mm, 5 * mm, str(page), font, 12)
_txt(c, x + 165 * mm, y + mh - 20 * mm, 20 * mm, 5 * mm, str(total), font, 12)
_txt(c, x + 135 * mm, y + mh - 35 * mm, 50 * mm, 15 * mm, _ins(inscriptions, 9), font, 12)
fmt = "Формат А3" if doc_type == 4 else "Формат А4"
_txt(c, x + 135 * mm, y, 50 * mm, 5 * mm, fmt, font, 12, "left")
def _draw_stamp_other(c: canvas.Canvas, inner: Inner, inscriptions: dict[int, str], doc_type: int, page: int, font: str) -> None:
mw, mh = 185 * mm, 15 * mm
x = inner.right - mw
y = inner.bottom
c.rect(x, y, mw, mh)
for dx in (7, 17, 40, 55, 65, 175):
c.line(x + dx * mm, y, x + dx * mm, y + mh)
c.line(x, y + 5 * mm, x + 65 * mm, y + 5 * mm)
c.line(x, y + 10 * mm, x + 65 * mm, y + 10 * mm)
c.line(x + mw - 10 * mm, y + 7 * mm, x + mw, y + 7 * mm)
_txt(c, x, y, 7 * mm, 5 * mm, "Изм.", font, 12)
_txt(c, x + 7 * mm, y, 10 * mm, 5 * mm, "Лист", font, 12)
_txt(c, x + 17 * mm, y, 23 * mm, 5 * mm, "№ докум", font, 12)
_txt(c, x + 40 * mm, y, 15 * mm, 5 * mm, "Подп.", font, 12)
_txt(c, x + 55 * mm, y, 10 * mm, 5 * mm, "Дата", font, 12)
_txt(c, x, y + 5 * mm, 7 * mm, 5 * mm, _ins(inscriptions, 14), font, 12)
_txt(c, x + 7 * mm, y + 5 * mm, 10 * mm, 5 * mm, _ins(inscriptions, 15), font, 12)
_txt(c, x + 17 * mm, y + 5 * mm, 23 * mm, 5 * mm, _ins(inscriptions, 16), font, 12)
_txt(c, x + 40 * mm, y + 5 * mm, 15 * mm, 5 * mm, _ins(inscriptions, 17), font, 12)
_txt(c, x + 55 * mm, y + 5 * mm, 10 * mm, 5 * mm, _ins(inscriptions, 18), font, 12)
title = _ins(inscriptions, 2)
if doc_type == 1:
title = f"{title} ПЭ3".strip()
elif doc_type == 4:
title = f"{title} ВП".strip()
elif doc_type == 3:
title = _ins(inscriptions, 1001)
_txt(c, x + 65 * mm, y + mh - 15 * mm, 110 * mm, 15 * mm, title, font, 24)
_txt(c, x + 65 * mm, y, 110 * mm, 5 * mm, "Копировал", font, 12, "left")
fmt = "Формат А3" if doc_type == 4 else "Формат А4"
_txt(c, x + 65 * mm, y, 110 * mm, 5 * mm, fmt, font, 12, "right")
_txt(c, x + mw - 10 * mm, y + mh - 7 * mm, 10 * mm, 7 * mm, "Лист", font, 12)
_txt(c, x + mw - 10 * mm, y, 10 * mm, 8 * mm, str(page), font, 12)
def draw_gost_frame(
c: canvas.Canvas,
page_w: float,
page_h: float,
inscriptions: dict[int, str],
table_type: str,
page: int,
total: int,
font: str,
) -> None:
"""Draw GOST A4/A3 frame matching desktop layout."""
doc_type = DOC_TYPES.get(table_type, 1)
display_total = total + 1 # desktop increments for registration sheet
inner = Inner(page_w, page_h)
_draw_inner_border(c, inner)
sx, sy, sw, sh = _draw_left_binding_strip(c, inner)
_draw_left_binding_text(c, sx, sy, inscriptions, font)
if page == 1:
_draw_top_left_strip(c, inner, inscriptions, doc_type, font)
_draw_stamp_first(c, inner, inscriptions, doc_type, page, display_total, font)
else:
_draw_stamp_other(c, inner, inscriptions, doc_type, page, font)