"""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) 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")]