"""Защита заголовков групп от «висячих» переносов на границе страницы (как в десктопе).""" from __future__ import annotations from typing import Any # Совпадает с TABLE_LAYOUTS в pdf_export.py PAGE_LIMITS: dict[str, tuple[int, int]] = { "perechen": (25, 32), "specification_pcb": (26, 32), "specification": (26, 32), "vedomost": (24, 29), } _TEXT_SKIP = frozenset({"id", "row_index", "page_number", "overflow_fields"}) def assign_page_numbers(rows: list[dict[str, Any]], first: int, other: int) -> None: page = 1 used = 0 limit = first for i, row in enumerate(rows): if used >= limit: page += 1 used = 0 limit = other row["row_index"] = i row["page_number"] = page used += 1 def _rows_on_page_before(rows: list[dict[str, Any]], idx: int, page: int) -> int: return sum(1 for j in range(idx) if rows[j].get("page_number") == page) def _group_continues_on_next_page(rows: list[dict[str, Any]], header_idx: int, page: int) -> bool: for i in range(header_idx + 1, len(rows)): r = rows[i] if r.get("is_header"): break if r.get("page_number") == page + 1 and not r.get("is_empty"): return True return False def needs_page_break_protection( rows: list[dict[str, Any]], row_index: int, first: int, other: int ) -> tuple[bool, int]: """Возвращает (нужна_защита, сколько_пустых_вставить_перед_заголовком).""" if row_index < 0 or row_index >= len(rows): return False, 0 row = rows[row_index] if not row.get("is_header"): return False, 0 page = int(row.get("page_number") or 1) max_rows = first if page == 1 else other on_page = _rows_on_page_before(rows, row_index, page) if on_page not in (max_rows - 1, max_rows - 2): return False, 0 if not _group_continues_on_next_page(rows, row_index, page): return False, 0 empty_count = 2 if on_page == max_rows - 2 else 1 return True, empty_count def _make_empty_row(sample: dict[str, Any] | None = None) -> dict[str, Any]: empty: dict[str, Any] = { "is_empty": True, "is_header": False, "is_underline": False, "page_number": 1, "row_index": 0, } if sample: for k, v in sample.items(): if k in _TEXT_SKIP or k in empty: continue if isinstance(v, bool): empty[k] = False elif isinstance(v, (int, float)) and k not in ("id",): empty[k] = "" elif isinstance(v, str): empty[k] = "" elif v is None: empty[k] = "" return empty def protect_page_breaks(rows: list[dict[str, Any]], table_type: str) -> list[dict[str, Any]]: """Вставляет 1–2 пустые строки перед orphan-заголовком; пересчитывает page_number.""" if table_type not in PAGE_LIMITS or not rows: return rows first, other = PAGE_LIMITS[table_type] working = [dict(r) for r in rows] sample = working[0] if working else None # Повторяем, пока есть что защищать (вставки сдвигают границы страниц) for _ in range(len(working) + 2): assign_page_numbers(working, first, other) insert_at: list[tuple[int, int]] = [] for i in range(len(working)): need, count = needs_page_break_protection(working, i, first, other) if need: insert_at.append((i, count)) if not insert_at: break # С конца, чтобы индексы не сбивались for idx, count in reversed(insert_at): for _ in range(count): working.insert(idx, _make_empty_row(sample)) else: assign_page_numbers(working, first, other) assign_page_numbers(working, first, other) return working