Files
md_to_gost/tests/test_landscape.py
T
Igor20264 510f7e7adf
Python application / build (push) Waiting to run
v0.5.2
Что то сделал
2026-09-08 19:37:54 +03:00

333 lines
12 KiB
Python

"""Tests for landscape section around wide figures/tables."""
from __future__ import annotations
import os
from pathlib import Path
from docx import Document
from docx.enum.section import WD_ORIENT
from PIL import Image as PILImage
from md2gost import package_dir
from md2gost.page_geometry import content_size
from md2gost.parser_ import Parser
from md2gost.renderable.caption import CaptionInfo
from md2gost.renderable.image import Image
from md2gost.renderable.table import Table
from md2gost.renderer import Renderer
from docx.oxml.ns import qn
def _section_page_start(section):
pg = section._sectPr.find(qn("w:pgNumType"))
if pg is None:
return None
return pg.get(qn("w:start"))
def test_content_size_landscape_wider():
h_p, w_p = content_size(landscape=False)
h_l, w_l = content_size(landscape=True)
assert w_l > w_p
assert h_l < h_p
def test_renderer_landscape_section_for_table():
doc = Document(os.path.join(package_dir(), "Template.docx"))
body = doc._body._element
for child in list(body):
if child.tag.endswith("}sectPr"):
continue
body.remove(child)
renderer = Renderer(doc, skip_numbering=False)
table = Table(doc._body, 2, 4, CaptionInfo("t1", "Wide", landscape=True))
for r in range(2):
for c in range(4):
table.add_paragraph_to_cell(r, c).add_run(f"{r},{c}")
before = len(doc.sections)
renderer.process([table])
after = len(doc.sections)
assert after >= before + 2
land = [s for s in doc.sections if s.orientation == WD_ORIENT.LANDSCAPE]
assert land, "no landscape section"
assert int(land[0].page_width) > int(land[0].page_height)
from md2gost.styles import apply_document_styles
apply_document_styles(doc, "mirea")
land2 = [s for s in doc.sections if s.orientation == WD_ORIENT.LANDSCAPE]
assert land2
assert int(land2[0].page_width) > int(land2[0].page_height)
assert doc.sections[-1].orientation == WD_ORIENT.PORTRAIT
assert int(doc.sections[-1].page_width) < int(doc.sections[-1].page_height)
# Landscape and return-to-portrait must not restart PAGE (no w:start).
for section in land2:
assert _section_page_start(section) is None
assert _section_page_start(doc.sections[-1]) is None
def test_landscape_sections_have_no_page_restart():
"""+landscape must not leave w:pgNumType/@w:start on album / following portrait."""
doc = Document(os.path.join(package_dir(), "Template.docx"))
body = doc._body._element
for child in list(body):
if child.tag.endswith("}sectPr"):
continue
body.remove(child)
from md2gost.renderable.heading import Heading
from md2gost.renderable.paragraph import Paragraph
from md2gost.renderable.toc import ToC
h = Heading(doc._body, 1, False)
h.add_run("СОДЕРЖАНИЕ")
toc = ToC(doc._body, toc_mode="native")
para = Paragraph(doc._body)
para.add_run("Текст перед широкой таблицей.")
table = Table(doc._body, 2, 3, CaptionInfo("wide", "Карта", landscape=True))
for r in range(2):
for c in range(3):
table.add_paragraph_to_cell(r, c).add_run(f"{r}{c}")
after = Paragraph(doc._body)
after.add_run("Текст после альбома.")
Renderer(doc, skip_numbering=True).process([h, toc, para, table, after])
land = [s for s in doc.sections if s.orientation == WD_ORIENT.LANDSCAPE]
assert land
for section in land:
assert _section_page_start(section) is None, "landscape restarted PAGE"
# Body portrait after landscape also continuous.
assert doc.sections[-1].orientation == WD_ORIENT.PORTRAIT
assert _section_page_start(doc.sections[-1]) is None
# Non-first sections must not restart (first may still carry template start).
for section in list(doc.sections)[1:]:
assert _section_page_start(section) is None
def test_parser_propagates_landscape_to_table():
doc = Document(os.path.join(package_dir(), "Template.docx"))
md = "%t1 Широкая +landscape\n\n|A|B|C|\n|---|---|---|\n|1|2|3|\n"
renderables = list(Parser(doc, md).parse())
assert len(renderables) == 1
assert isinstance(renderables[0], Table)
assert renderables[0].landscape is True
def test_landscape_image_not_queued_for_soft_pagebreak(tmp_path, monkeypatch):
"""Portrait fit-check must not queue +landscape images (empty page before figure)."""
monkeypatch.chdir(tmp_path)
png = tmp_path / "wide.png"
# Wide image that is tall when forced into portrait width
PILImage.new("RGB", (2000, 1400), color=(0, 128, 255)).save(png)
os.environ["WORKING_DIR"] = str(tmp_path)
doc = Document(os.path.join(package_dir(), "Template.docx"))
body = doc._body._element
for child in list(body):
if child.tag.endswith("}sectPr"):
continue
body.remove(child)
from md2gost.renderable.paragraph import Paragraph as RParagraph
from md2gost.sub_renderable import SubRenderable
para = RParagraph(doc._body)
para.add_run("Before text that fills some space. " * 20)
img = Image(
doc._body,
str(png),
CaptionInfo("deploy", "Диаграмма", landscape=True),
)
# Mimic factory: image attached to paragraph
para._images.append(img)
from md2gost.layout_tracker import LayoutState
from md2gost.page_geometry import content_size
from docx.shared import Length
max_h, max_w = content_size(landscape=False)
state = LayoutState(max_h, max_w)
# Pretend most of the page is already used so a portrait fit-check would fail
state.add_height(Length(int(max_h) - 100000))
infos = list(para.render(None, state))
subs = [i for i in infos if isinstance(i, SubRenderable)]
assert len(subs) == 1
assert subs[0].renderable is img
assert subs[0].add_to_new_page is False
def test_landscape_places_figure_inside_landscape_section(tmp_path, monkeypatch):
"""Content must sit BEFORE body sectPr or Word ignores landscape."""
monkeypatch.chdir(tmp_path)
png = tmp_path / "x.png"
PILImage.new("RGB", (40, 20), color=(0, 128, 255)).save(png)
doc = Document(os.path.join(package_dir(), "Template.docx"))
body = doc._body._element
for child in list(body):
if child.tag.endswith("}sectPr"):
continue
body.remove(child)
os.environ["WORKING_DIR"] = str(tmp_path)
img = Image(
doc._body,
str(png),
CaptionInfo("fig", "wide", landscape=True),
)
renderer = Renderer(doc, skip_numbering=False)
renderer.process([img])
A = "{http://schemas.openxmlformats.org/drawingml/2006/main}"
W = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
section_idx = 0
image_section = None
for child in doc.element.body:
tag = child.tag.split("}")[-1]
if child.findall(f".//{A}blip"):
image_section = section_idx
sect = None
if tag == "sectPr":
sect = child
elif tag == "p":
pPr = child.find(f"{W}pPr")
if pPr is not None:
sect = pPr.find(f"{W}sectPr")
if sect is not None:
section_idx += 1
assert any(s.orientation == WD_ORIENT.LANDSCAPE for s in doc.sections)
assert image_section is not None
assert image_section == 1
W_NS = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
def _is_break_paragraph(el) -> bool:
"""Empty paragraph that is a page break and/or hosts a section break."""
if el.tag != f"{W_NS}p":
return False
texts = [t.text or "" for t in el.findall(f".//{W_NS}t")]
if any(t.strip() for t in texts):
return False
pPr = el.find(f"{W_NS}pPr")
has_sect = pPr is not None and pPr.find(f"{W_NS}sectPr") is not None
has_br = any(br.get(f"{W_NS}type") == "page" for br in el.findall(f".//{W_NS}br"))
return has_sect or has_br
def _breaks_between_last_text_and_table(doc) -> int:
"""Empty page/section-break paras between the last body sentence and the table."""
children = list(doc.element.body)
table_i = next(i for i, el in enumerate(children) if el.tag == f"{W_NS}tbl")
j = table_i - 1
breaks = 0
while j >= 0:
el = children[j]
if _is_break_paragraph(el):
breaks += 1
j -= 1
continue
if el.tag == f"{W_NS}p":
text = "".join(t.text or "" for t in el.findall(f".//{W_NS}t")).strip()
if text.startswith("Таблица") or text.startswith("Продолжение"):
j -= 1
continue
break
return breaks
def test_portrait_to_landscape_is_single_section_break():
"""Text then +landscape: one next-page section, no extra blank portrait page."""
doc = Document(os.path.join(package_dir(), "Template.docx"))
body = doc._body._element
for child in list(body):
if child.tag.endswith("}sectPr"):
continue
body.remove(child)
from md2gost.renderable.heading import Heading
from md2gost.renderable.paragraph import Paragraph
from md2gost.renderable.toc import ToC
h = Heading(doc._body, 1, False)
h.add_run("СОДЕРЖАНИЕ")
toc = ToC(doc._body, toc_mode="native")
para = Paragraph(doc._body)
para.add_run("Текст перед широкой таблицей.")
table = Table(doc._body, 2, 3, CaptionInfo("wide", "Карта", landscape=True))
for r in range(2):
for c in range(3):
table.add_paragraph_to_cell(r, c).add_run(f"{r}{c}")
Renderer(doc, skip_numbering=True).process([h, toc, para, table])
assert _breaks_between_last_text_and_table(doc) == 1
# Same without TOC — still a single break, not page-break + section-break.
doc2 = Document(os.path.join(package_dir(), "Template.docx"))
body2 = doc2._body._element
for child in list(body2):
if child.tag.endswith("}sectPr"):
continue
body2.remove(child)
para2 = Paragraph(doc2._body)
para2.add_run("Текст перед широкой таблицей.")
table2 = Table(doc2._body, 2, 3, CaptionInfo("wide2", "Карта", landscape=True))
for r in range(2):
for c in range(3):
table2.add_paragraph_to_cell(r, c).add_run(f"{r}{c}")
Renderer(doc2, skip_numbering=True).process([para2, table2])
assert _breaks_between_last_text_and_table(doc2) == 1
def test_landscape_listing_deferred_after_section(tmp_path, monkeypatch):
"""+landscape +listing → listing must not sit in the landscape section."""
from unittest.mock import patch
from md2gost.renderable.caption import CaptionInfo
from md2gost.renderable.diagram import DiagramFigure
from md2gost.renderer import Renderer
from md2gost.styles import apply_mirea_styles
monkeypatch.chdir(tmp_path)
png = tmp_path / "d.png"
PILImage.new("RGB", (40, 20), color=(0, 128, 255)).save(png)
doc = Document(os.path.join(package_dir(), "Template.docx"))
body = doc._body._element
for child in list(body):
if child.tag.endswith("}sectPr"):
continue
body.remove(child)
apply_mirea_styles(doc)
class FakeResult:
png_path = str(png)
svg_path = None
pixel_scale = 1.0
fig = DiagramFigure(
doc._body,
"uml",
"@startuml\nA->B\n@enduml",
CaptionInfo("d1", "Схема", with_listing=True, landscape=True),
with_listing=True,
)
assert fig.listing is not None
renderer = Renderer(doc, skip_numbering=False)
with patch("md2gost.renderable.diagram.render_diagram", return_value=FakeResult()):
renderer.process([fig])
assert any(s.orientation == WD_ORIENT.LANDSCAPE for s in doc.sections)
texts = [p.text for p in doc.paragraphs]
listing_caps = [t for t in texts if t.startswith("Листинг")]
assert listing_caps, f"no listing caption in {texts!r}"