Update 0.4.0
Python application / build (push) Has been cancelled

- Add\Rework UI
- Add Split Table and Listing
- Add Support Customazeble schems
This commit is contained in:
Igor20264
2026-09-04 22:28:39 +03:00
parent 516abe7b83
commit b38661f588
70 changed files with 69532 additions and 413 deletions
+226
View File
@@ -0,0 +1,226 @@
"""Post-conversion heuristic: half-empty pages inside an unfinished section.
Requires Windows + Microsoft Word + pywin32. Findings are ALWAYS heuristic —
false positives are expected (large figures, end of subsection, odd breaks).
Never treat as --strict errors.
"""
from __future__ import annotations
from dataclasses import dataclass
# Unused fraction of the text area below the last content on the page.
EMPTY_FRACTION_THRESHOLD = 0.40
FRONT_MATTER_HINTS = (
"СОДЕРЖАНИЕ",
"ТИТУЛ",
"ЗАДАНИЕ",
)
@dataclass
class PageMetric:
"""Synthetic / measured metrics for one page (CI-friendly)."""
page_index: int # 1-based
content_bottom_frac: float # 0..1, how far down the text area content reaches
section_title: str
next_section_title: str | None
is_landscape: bool = False
is_last_doc_page: bool = False
mostly_figure_or_table: bool = False
@dataclass
class PageFillIssue:
page_index: int
message: str
severity: str = "heuristic"
def evaluate_page_fill(
pages: list[PageMetric],
*,
empty_threshold: float = EMPTY_FRACTION_THRESHOLD,
) -> list[PageFillIssue]:
"""Pure heuristic over page metrics (no Word). Safe for unit tests."""
issues: list[PageFillIssue] = []
for i, page in enumerate(pages):
if page.is_landscape:
continue
if page.mostly_figure_or_table:
continue
if page.is_last_doc_page:
continue
title_u = (page.section_title or "").upper()
if any(h in title_u for h in FRONT_MATTER_HINTS):
continue
# Last page of this section (next page starts a different H1)
if page.next_section_title and page.next_section_title != page.section_title:
continue
empty_frac = 1.0 - page.content_bottom_frac
if empty_frac <= empty_threshold:
continue
# Same section continues on the next page
if i + 1 >= len(pages):
continue
nxt = pages[i + 1]
if nxt.section_title != page.section_title:
continue
issues.append(PageFillIssue(
page_index=page.page_index,
message=(
f"[heuristic] page.fill: на стр. {page.page_index} пустой низ "
f"~{empty_frac:.0%} полосы, а раздел «{page.section_title}» "
f"продолжается на следующей странице. "
f"Проверка эвристическая — возможны ложные срабатывания "
f"(крупный рисунок, конец пункта, нестандартный разрыв)."
),
))
return issues
def check_docx_page_fill(path: str) -> tuple[list[PageFillIssue], str]:
"""
Open DOCX in Word, repaginate, collect metrics, evaluate.
Returns (issues, status_message). On missing Word/pywin32 returns ([], reason).
"""
try:
import win32com.client # type: ignore
except ImportError:
return [], (
"Проверка вёрстки пропущена: нужен пакет pywin32 "
"(pip install pywin32) и Microsoft Word."
)
word = None
doc = None
try:
word = win32com.client.DispatchEx("Word.Application")
word.Visible = False
word.DisplayAlerts = 0
abs_path = str(path)
doc = word.Documents.Open(abs_path, ReadOnly=True)
doc.Repaginate()
pages_metrics: list[PageMetric] = []
page_count = int(doc.ComputeStatistics(2)) # wdStatisticPages
for page_no in range(1, page_count + 1):
try:
metric = _measure_page(doc, page_no, page_count)
except Exception:
continue
if metric is not None:
pages_metrics.append(metric)
issues = evaluate_page_fill(pages_metrics)
if not issues:
return [], (
"Проверка вёрстки (Word): замечаний по полупустым страницам нет "
"(эвристика; возможны пропуски)."
)
return issues, (
f"Проверка вёрстки (Word): найдено замечаний — {len(issues)} "
f"(все эвристические, могут быть ложными)."
)
except Exception as exc:
return [], f"Проверка вёрстки пропущена: не удалось открыть Word ({exc})."
finally:
try:
if doc is not None:
doc.Close(False)
except Exception:
pass
try:
if word is not None:
word.Quit()
except Exception:
pass
def _measure_page(doc, page_no: int, page_count: int) -> PageMetric | None:
"""Best-effort measurement via Word COM selection / page setup."""
selection = doc.Application.Selection
selection.GoTo(What=1, Which=1, Count=page_no) # wdGoToPage, wdGoToAbsolute
section = selection.Sections(1)
ps = section.PageSetup
is_landscape = bool(int(ps.Orientation) == 1) # wdOrientLandscape
page_h = float(ps.PageHeight)
top = float(ps.TopMargin)
bottom = float(ps.BottomMargin)
text_h = max(page_h - top - bottom, 1.0)
start = int(selection.Start)
if page_no < page_count:
selection.GoTo(What=1, Which=1, Count=page_no + 1)
end = int(selection.Start) - 1
else:
end = int(doc.Content.End)
if end < start:
end = start
rng = doc.Range(start, end)
try:
# wdVerticalPositionRelativeToPage = 6
vpos = float(rng.Information(6))
content_bottom = max(0.0, min(1.0, (vpos - top) / text_h))
except Exception:
content_bottom = 1.0
section_title = _heading_near(doc, start)
next_title = None
if page_no < page_count:
try:
selection.GoTo(What=1, Which=1, Count=page_no + 1)
next_title = _heading_near(doc, int(selection.Start))
except Exception:
next_title = None
mostly_object = False
try:
text_len = len((rng.Text or "").strip())
if rng.Tables.Count >= 1 and text_len < 80:
mostly_object = True
if rng.InlineShapes.Count >= 1 and text_len < 80:
mostly_object = True
except Exception:
pass
return PageMetric(
page_index=page_no,
content_bottom_frac=content_bottom,
section_title=section_title or "",
next_section_title=next_title,
is_landscape=is_landscape,
is_last_doc_page=(page_no == page_count),
mostly_figure_or_table=mostly_object,
)
def _heading_near(doc, pos: int) -> str:
"""Walk backwards for nearest Heading 1 style paragraph."""
try:
p = doc.Range(pos, pos).Paragraphs(1)
for _ in range(80):
style = str(p.Style)
if "Heading 1" in style or "Заголовок 1" in style:
return (p.Range.Text or "").strip().replace("\r", "")
if p.Range.Start <= 1:
break
p = p.Previous()
if p is None:
break
except Exception:
pass
return ""
def format_page_fill_report(issues: list[PageFillIssue], status: str) -> str:
lines = [status]
for i in issues:
lines.append(f" [{i.severity}] page:{i.page_index}: {i.message}")
return "\n".join(lines)