Files
GostGenerator/web/backend/app/services/pdf_gost_frame.py
T
2026-09-03 09:55:39 +03:00

395 lines
15 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
BINDING_WIDTH = 12 * mm
# Поле подшивки: 8–20 мм от левого края листа; таблица начинается ровно на inner.left (20 мм).
TABLE_LEFT = M_LEFT
BINDING_LEFT = TABLE_LEFT - BINDING_WIDTH
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 _elide(c: canvas.Canvas, text: str, font: str, size: float, max_w: float) -> str:
if not text:
return text
if c.stringWidth(text, font, size) <= max_w:
return text
ell = ""
lo, hi = 0, len(text)
while lo < hi:
mid = (lo + hi + 1) // 2
if c.stringWidth(text[:mid] + ell, font, size) <= max_w:
lo = mid
else:
hi = mid - 1
return text[:lo] + ell if lo else ell
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)
text = _elide(c, text, font, size, w - mm)
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 _clip_binding_zone(c: canvas.Canvas, inner: Inner) -> None:
"""Ограничить рисование полосы подшивки: 8–20 мм, не заходить в таблицу."""
p = c.beginPath()
p.rect(BINDING_LEFT, inner.bottom, BINDING_WIDTH, inner.top - inner.bottom)
c.clipPath(p, stroke=0, fill=0)
def _draw_left_binding_strip(c: canvas.Canvas, inner: Inner) -> tuple[float, float, float, float]:
"""Left vertical strip (12×145 mm) — strictly left of the table (820 mm)."""
w = BINDING_WIDTH
h = 145 * mm
x = BINDING_LEFT
y = inner.bottom
c.saveState()
_set_frame_pen(c)
_clip_binding_zone(c, inner)
c.rect(x, y, w, h)
offsets = [25, 60, 85, 110, 145]
for off in offsets:
yy = y + off * mm
c.line(x, yy, TABLE_LEFT, yy)
c.restoreState()
return x, y, w, h
def _draw_left_binding_text(
c: canvas.Canvas, inner: Inner, strip_x: float, strip_y: float, inscriptions: dict[int, str], font: str
) -> None:
size = 12
c.saveState()
_clip_binding_zone(c, inner)
c.translate(strip_x + BINDING_WIDTH, 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 = BINDING_WIDTH
h = 120 * mm
x = BINDING_LEFT
y = inner.top - h
c.saveState()
_set_frame_pen(c)
_clip_binding_zone(c, inner)
c.rect(x, y, w, h)
c.line(x, y + 60 * mm, TABLE_LEFT, y + 60 * mm)
c.translate(TABLE_LEFT, 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 _stamp_line_y(stamp_y: float, stamp_h: float, top_mm: float) -> float:
"""Qt top+N mm → ReportLab Y (origin bottom-left)."""
return stamp_y + stamp_h - top_mm * mm
def _stamp_cell_y(stamp_y: float, stamp_h: float, top_mm: float, height_mm: float) -> float:
"""Bottom of text cell for Qt drawText(top+N, height)."""
return stamp_y + stamp_h - (top_mm + height_mm) * mm
def _draw_stamp_first(c: canvas.Canvas, inner: Inner, inscriptions: dict[int, str], doc_type: int, page: int, total: int, font: str) -> None:
# Порт addGostFrameA4FirstPage: Qt Y вниз от top штампа → ReportLab Y вверх от bottom.
mw, mh = 185 * mm, 40 * mm
x = inner.right - mw
y = inner.bottom
left_block = 65 * mm # 7+10+23+15+10
ly = lambda top: _stamp_line_y(y, mh, top)
cy = lambda top, h=5: _stamp_cell_y(y, mh, top, h)
c.rect(x, y, mw, mh)
# Горизонтали (как в Qt: top+5/10 только до 65 мм; top+15 на всю ширину; 20–35 до 65 мм)
for top in (5, 10, 20, 25, 30, 35):
c.line(x, ly(top), x + left_block, ly(top))
c.line(x, ly(15), x + mw, ly(15))
c.line(x + mw - 50 * mm, ly(20), x + mw, ly(20))
c.line(x + mw - 50 * mm, ly(25), x + mw, ly(25))
# Вертикали
c.line(x + 65 * mm, y, x + 65 * mm, y + mh)
c.line(x + 55 * mm, y, x + 55 * mm, y + mh)
c.line(x + 40 * mm, y, x + 40 * mm, y + mh)
c.line(x + 17 * mm, y, x + 17 * mm, y + mh)
c.line(x + 7 * mm, ly(15), x + 7 * mm, y + mh) # только блок Изм./Лист
c.line(x + mw - 50 * mm, y, x + mw - 50 * mm, ly(15))
c.line(x + mw - 45 * mm, ly(25), x + mw - 45 * mm, ly(20))
c.line(x + mw - 40 * mm, ly(25), x + mw - 40 * mm, ly(20))
c.line(x + mw - 35 * mm, ly(25), x + mw - 35 * mm, ly(15))
c.line(x + mw - 20 * mm, ly(25), x + mw - 20 * mm, ly(15))
# Блок над штампом (22 мм)
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 + 8 * mm, ix + iw, iy + 8 * mm) # Qt top+14 → 22-14=8 от низа
c.line(ix + 14 * mm, iy + 8 * mm, ix + 14 * mm, iy + ih)
c.line(ix + 67 * mm, iy + 8 * mm, ix + 67 * mm, iy + ih)
_txt(c, ix, iy + 8 * mm, 14 * mm, 14 * mm, _ins(inscriptions, 27), font, 12)
_txt(c, ix + 14 * mm, iy + 8 * mm, 53 * mm, 14 * mm, _ins(inscriptions, 28), font, 12)
_txt(c, ix + 67 * mm, iy + 8 * mm, 53 * mm, 14 * mm, _ins(inscriptions, 29), font, 12)
_txt(c, ix, iy, iw, 8 * mm, _ins(inscriptions, 30), font, 12)
# Строка значений изменений (Qt top+5) и заголовки Изм./Лист… (Qt top+10)
_txt(c, x, cy(5), 7 * mm, 5 * mm, _ins(inscriptions, 14), font, 12)
_txt(c, x + 7 * mm, cy(5), 10 * mm, 5 * mm, _ins(inscriptions, 15), font, 12)
_txt(c, x + 17 * mm, cy(5), 23 * mm, 5 * mm, _ins(inscriptions, 16), font, 12)
_txt(c, x + 40 * mm, cy(5), 15 * mm, 5 * mm, _ins(inscriptions, 17), font, 12)
_txt(c, x + 55 * mm, cy(5), 10 * mm, 5 * mm, _ins(inscriptions, 18), font, 12)
_txt(c, x, cy(10), 7 * mm, 5 * mm, "Изм.", font, 12)
_txt(c, x + 7 * mm, cy(10), 10 * mm, 5 * mm, "Лист", font, 12)
_txt(c, x + 17 * mm, cy(10), 23 * mm, 5 * mm, "№ докум.", font, 12)
_txt(c, x + 40 * mm, cy(10), 15 * mm, 5 * mm, "Подп.", font, 12)
_txt(c, x + 55 * mm, cy(10), 10 * mm, 5 * mm, "Дата", font, 12)
# Подписи: Разраб./Пров./(поле 10)/Н.контр./Утв. — Qt top+15…35
_txt(c, x + 1 * mm, cy(15), 16 * mm, 5 * mm, "Разраб.", font, 12, "left")
_txt(c, x + 1 * mm, cy(20), 16 * mm, 5 * mm, "Пров.", font, 12, "left")
_txt(c, x + 1 * mm, cy(25), 16 * mm, 5 * mm, _ins(inscriptions, 10), font, 12, "left")
_txt(c, x + 1 * mm, cy(30), 16 * mm, 5 * mm, "Н. контр.", font, 12, "left")
_txt(c, x + 1 * mm, cy(35), 16 * mm, 5 * mm, "Утв.", font, 12, "left")
_txt(c, x + 18 * mm, cy(15), 22 * mm, 5 * mm, _ins(inscriptions, 111), font, 12, "left")
_txt(c, x + 18 * mm, cy(20), 22 * mm, 5 * mm, _ins(inscriptions, 112), font, 12, "left")
_txt(c, x + 18 * mm, cy(25), 22 * mm, 5 * mm, _ins(inscriptions, 11), font, 12, "left")
_txt(c, x + 18 * mm, cy(30), 22 * mm, 5 * mm, _ins(inscriptions, 113), font, 12, "left")
_txt(c, x + 18 * mm, cy(35), 22 * mm, 5 * mm, _ins(inscriptions, 114), font, 12, "left")
_txt(c, x + 41 * mm, cy(25), 14 * mm, 5 * mm, _ins(inscriptions, 12), font, 12, "left")
_txt(c, x + 56 * mm, cy(25), 9 * mm, 5 * mm, _ins(inscriptions, 13), font, 12, "left")
# Наименование изделия (Qt top+15, h=25) и тип документа снизу ячейки
if doc_type != 4:
name_text = _ins(inscriptions, 1002 if doc_type == 3 else 1)
_txt_multiline(c, x + 65 * mm, cy(15, 25), 70 * mm, 25 * mm, name_text, font, 18)
if doc_type == 1:
_txt(c, x + 65 * mm, cy(15, 25), 70 * mm, 25 * mm, "Перечень элементов", font, 12, valign="bottom")
else:
_txt_multiline(c, x + 65 * mm, cy(15, 20), 70 * mm, 20 * mm, _ins(inscriptions, 1), font, 18)
_txt(c, x + 65 * mm, cy(35), 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, cy(0, 15), 120 * mm, 15 * mm, title, font, 18)
_txt(c, x + 135 * mm, cy(15), 15 * mm, 5 * mm, "Лит.", font, 12)
_txt(c, x + 150 * mm, cy(15), 15 * mm, 5 * mm, "Лист", font, 12)
_txt(c, x + 165 * mm, cy(15), 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, cy(20), 5 * mm, 5 * mm, littera[i], font, 12)
if total > 1:
_txt(c, x + 150 * mm, cy(20), 15 * mm, 5 * mm, str(page), font, 12)
_txt(c, x + 165 * mm, cy(20), 20 * mm, 5 * mm, str(total), font, 12)
_txt(c, x + 135 * mm, cy(25, 15), 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, inner, 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)