"""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 (8–20 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)