Files
md_to_gost/md2gost/page_geometry.py
T
Igor20264 1a5b35eb54
Python application / build (push) Has been cancelled
BigUpdate 0.5.0
- add Local Render Mermaid
- add page-starе для указания смещения страниц
- add Гиперссылки в документе на списки литературы
2026-09-07 09:54:33 +03:00

194 lines
6.2 KiB
Python

"""A4 page geometry helpers for portrait / landscape sections."""
from __future__ import annotations
from docx.enum.section import WD_ORIENT
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.oxml.ns import qn
from docx.shared import Mm, Pt
from .util import create_element
# GOST-like A4 margins (same as styles._apply_common_page_and_body)
MARGIN_LEFT = Mm(30)
MARGIN_RIGHT = Mm(10)
MARGIN_TOP = Mm(20)
MARGIN_BOTTOM = Mm(20)
# mirea / pis_custom — 30/10/20/20; paco_custom — форма ПАЦО 20/20/20/20
MARGIN_PRESETS = {
"mirea": (Mm(30), Mm(10), Mm(20), Mm(20)),
"pis_custom": (Mm(30), Mm(10), Mm(20), Mm(20)),
}
def apply_margin_preset(preset: str) -> None:
"""Switch active page margins used by apply_section_geometry / content_size."""
global MARGIN_LEFT, MARGIN_RIGHT, MARGIN_TOP, MARGIN_BOTTOM
left, right, top, bottom = MARGIN_PRESETS.get(preset, MARGIN_PRESETS["mirea"])
MARGIN_LEFT = left
MARGIN_RIGHT = right
MARGIN_TOP = top
MARGIN_BOTTOM = bottom
A4_SHORT = Mm(210)
A4_LONG = Mm(297)
def is_landscape_section(section) -> bool:
"""True if section is (or should be treated as) landscape A4."""
try:
if section.orientation == WD_ORIENT.LANDSCAPE:
return True
except Exception:
pass
return int(section.page_width) > int(section.page_height)
def apply_section_geometry(section, *, landscape: bool) -> None:
"""
Set orientation and page size.
Set orientation first (python-docx may swap w/h on change), then force A4 dims.
"""
target = WD_ORIENT.LANDSCAPE if landscape else WD_ORIENT.PORTRAIT
try:
section.orientation = target
except Exception:
pass
if landscape:
section.page_width = A4_LONG
section.page_height = A4_SHORT
else:
section.page_width = A4_SHORT
section.page_height = A4_LONG
section.left_margin = MARGIN_LEFT
section.right_margin = MARGIN_RIGHT
section.top_margin = MARGIN_TOP
section.bottom_margin = MARGIN_BOTTOM
# Explicit orient on pgSz for Word.
pg_sz = section._sectPr.find(qn("w:pgSz"))
if pg_sz is None:
pg_sz = section._sectPr._add_pgSz()
if landscape:
pg_sz.set(qn("w:orient"), "landscape")
# Re-assert after XML tweak (some builds reshuffle).
section.page_width = A4_LONG
section.page_height = A4_SHORT
else:
if pg_sz.get(qn("w:orient")) is not None:
del pg_sz.attrib[qn("w:orient")]
section.page_width = A4_SHORT
section.page_height = A4_LONG
# Vertical align: center on landscape (figures/tables only); top on portrait
sect_pr = section._sectPr
for el in list(sect_pr.findall(qn("w:vAlign"))):
sect_pr.remove(el)
if landscape:
sect_pr.append(create_element("w:vAlign", {"w:val": "center"}))
def content_size(*, landscape: bool) -> tuple:
"""Return (max_height, max_width) usable content area for LayoutTracker."""
if landscape:
page_w, page_h = A4_LONG, A4_SHORT
else:
page_w, page_h = A4_SHORT, A4_LONG
max_height = page_h - MARGIN_TOP - MARGIN_BOTTOM
max_width = page_w - MARGIN_LEFT - MARGIN_RIGHT
return max_height, max_width
def apply_centered_page_footer(section) -> None:
"""Centered PAGE field, Times New Roman 12 (body section style)."""
footer = section.footer
footer.is_linked_to_previous = False
if not footer.paragraphs:
footer.add_paragraph()
paragraph = footer.paragraphs[0]
paragraph.clear()
paragraph.paragraph_format.first_line_indent = 0
paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
run = paragraph.add_run()
run.font.name = "Times New Roman"
run.font.size = Pt(12)
paragraph._p.append(create_element("w:fldSimple", {
"w:instr": "PAGE \\* MERGEFORMAT",
}))
def clear_section_footer(section) -> None:
"""Empty footer (no PAGE) — for title / assignment / TOC sections."""
footer = section.footer
footer.is_linked_to_previous = False
for p in footer.paragraphs:
p.clear()
if not footer.paragraphs:
footer.add_paragraph()
def ensure_continuous_page_numbers(document) -> None:
"""
After docxcompose of title/assignment: no PAGE on early sections,
continuous numbering (do not restart at 1 on body section).
"""
from docx.oxml.ns import qn
sections = list(document.sections)
if not sections:
return
# Heuristic: sections before the first that already has a PAGE field /
# or first N-1 if title was prepended — clear footer on all but keep
# continuous pgNumType. Body renderer already puts PAGE on body section;
# after compose, early sections may inherit footers — clear empty ones
# that belong to front matter (no Heading-like body content is hard to
# detect), so: clear footer on every section that has no PAGE field, and
# strip w:pgNumType start=1 everywhere.
for section in sections:
sect_pr = section._sectPr
for child in list(sect_pr):
if child.tag == qn("w:pgNumType"):
# Keep continuous: remove start attribute if present
if child.get(qn("w:start")) is not None:
del child.attrib[qn("w:start")]
def set_section_page_start(section, start: int) -> None:
"""Set w:pgNumType/@w:start so PAGE field begins at ``start`` in this section."""
if start < 1:
return
sect_pr = section._sectPr
pg = sect_pr.find(qn("w:pgNumType"))
if pg is None:
sect_pr.append(create_element("w:pgNumType", {"w:start": str(int(start))}))
else:
pg.set(qn("w:start"), str(int(start)))
def apply_page_number_start(
document,
start: int | None,
*,
front_sections: int = 0,
) -> None:
"""
Apply page-number offset for the body.
``start`` is None / <1 — leave continuous numbering (strip explicit starts).
``start`` >= 1 — first section after front matter restarts PAGE at that number;
later body sections stay continuous from there.
"""
ensure_continuous_page_numbers(document)
if start is None or int(start) < 1:
return
sections = list(document.sections)
idx = max(0, int(front_sections))
if idx >= len(sections):
return
set_section_page_start(sections[idx], int(start))