Files
GostGenerator/web/backend/app/services/page_breaks.py
T
2026-09-08 13:47:07 +03:00

120 lines
4.0 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.
"""Защита заголовков групп от «висячих» переносов на границе страницы (как в десктопе)."""
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