Section <w:pgSz> carries paper dimensions (@w:w, @w:h in twips) and orientation (@w:orient).
Library verdicts: python-docx: — docxjs: —
| Feature id | docx/section-page-size--portrait--b4 |
|---|---|
| Format | docx |
| Category | page-layout |
| Family | docx/section-page-size |
| Axis values | orient=portrait, paper=b4 |
| Spec | ecma-376-5-part-1 § 17.6.13 w:pgSz |
| ID / part | Predicate | XPath | python-docx | docxjs |
|---|---|---|---|---|
pgsz-orient-b4-portraitword/document.xml | equal = portraitw:pgSz@w:orient must equal 'portrait' for b4 portrait. | //w:sectPr/w:pgSz/@w:orient | — | — |
No render assertions declared.
scripts/gen_section_page_size.py
#!/usr/bin/env python3
"""Generate a ``fixtures/docx/section-page-size*.docx`` fixture.
Parameterised: called with ``--paper <id> --orient portrait|landscape
--out <path>``. Writes a one-paragraph document whose root section's
``<w:pgSz>`` carries the specified paper dimensions (in twips, from the
built-in ``PAPER_SIZES`` table) and orientation flag.
The ``features/docx/section-page-size.json`` manifest's
``generator.arg_template`` drives these args at expansion time; the
Cartesian product of 10 paper sizes x 2 orientations yields 20 fixtures.
Word stores page size on ``<w:sectPr>/<w:pgSz>`` with ``@w:w``, ``@w:h``
(in twips) and an optional ``@w:orient`` of ``portrait`` or
``landscape``. For landscape, Word expects the LONG edge as ``@w:w`` --
the orientation flag alone is not sufficient; many renderers will draw
a portrait page if the dimensions don't match the flag. This generator
swaps ``w``/``h`` for landscape so ``@w:w > @w:h``.
ECMA-376 Part 1 clause 17.6.13.
"""
from __future__ import annotations
import argparse
from pathlib import Path
from docx import Document
from docx.enum.section import WD_ORIENTATION
from docx.oxml.ns import qn
from docx.shared import Emu
_REPO_ROOT = Path(__file__).resolve().parent.parent
# Paper dimensions as (portrait_width_twips, portrait_height_twips).
# 1 inch = 1440 twips. Values match the w:pgSz @w:w/@w:h attributes
# that Word emits for each named paper size.
PAPER_SIZES: dict[str, tuple[int, int]] = {
"letter": (12240, 15840),
"legal": (12240, 20160),
"tabloid": (15840, 25920),
"ledger": (25920, 15840),
"executive": (10440, 15120),
"a3": (16838, 23814),
"a4": (11906, 16838),
"a5": (8391, 11906),
"b4": (14572, 20639),
"b5": (10319, 14572),
}
# 1 twip = 635 EMU. python-docx stores page_width / page_height in EMU.
_TWIP_TO_EMU = 635
def _write(paper: str, orient: str, out: Path) -> None:
if paper not in PAPER_SIZES:
raise ValueError(f"unknown paper {paper!r}; known: {sorted(PAPER_SIZES)}")
w_twips, h_twips = PAPER_SIZES[paper]
doc = Document()
section = doc.sections[-1]
# Recent python-docx auto-swaps @w:w/@w:h when the orientation
# setter observes an actual change — so set orientation BEFORE the
# dimensions, otherwise the newly-set values get rotated underneath
# us. Write the target (@w:w, @w:h) last so the on-disk shape
# matches what we intend.
if orient == "portrait":
section.orientation = WD_ORIENTATION.PORTRAIT
section.page_width = Emu(w_twips * _TWIP_TO_EMU)
section.page_height = Emu(h_twips * _TWIP_TO_EMU)
elif orient == "landscape":
section.orientation = WD_ORIENTATION.LANDSCAPE
# Word expects the long edge as @w:w for landscape; the portrait
# table stores (short, long), so we swap here so the final
# @w:w/@w:h agree with the orient flag.
section.page_width = Emu(h_twips * _TWIP_TO_EMU)
section.page_height = Emu(w_twips * _TWIP_TO_EMU)
else:
raise ValueError(f"unknown orient {orient!r}; expected 'portrait' or 'landscape'")
# python-docx's OptionalAttribute treats PORTRAIT as the default and
# omits @w:orient from the XML. For a conformance fixture we want the
# attribute present on every case so XPath-string equality works
# uniformly for both orientations — write it directly to the element.
section._sectPr.pgSz.set(qn("w:orient"), orient)
doc.add_paragraph(f"{paper} {orient}")
out.parent.mkdir(parents=True, exist_ok=True)
doc.save(out, reproducible=True)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--paper",
required=True,
choices=sorted(PAPER_SIZES),
help="Paper size id (e.g. letter, a4).",
)
parser.add_argument(
"--orient",
required=True,
choices=("portrait", "landscape"),
help="Page orientation.",
)
parser.add_argument(
"--out",
required=True,
help="Output .docx path (absolute, or repo-relative).",
)
args = parser.parse_args()
out_path = Path(args.out)
if not out_path.is_absolute():
out_path = _REPO_ROOT / out_path
_write(args.paper, args.orient, out_path)
print(out_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/section-page-size--portrait--b4",
"kind": "literal",
"title": "Section page size",
"format": "docx",
"category": "page-layout",
"summary": "Section <w:pgSz> carries paper dimensions (@w:w, @w:h in twips) and orientation (@w:orient).",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "17.6.13",
"element": "w:pgSz",
"rnc_reference": "spec/ecma-376-5/part-1/rnc/WordprocessingML.rnc",
"xsd_reference": "spec/ecma-376-5/part-1/xsd/wml.xsd",
"notes": "Page size is carried by <w:pgSz w:w=\"...\" w:h=\"...\" w:orient=\"portrait|landscape\"/> inside <w:sectPr>. Dimensions are in twips (1 inch = 1440 twips). The @w:orient flag alone is not sufficient — Word expects @w:w > @w:h for landscape, otherwise renderers draw a portrait page regardless of the flag. This manifest is a parameterised family: 10 paper sizes x 2 orientations = 20 cases. Each expanded case targets a distinct fixture docx/section-page-size--<paper-id>--<orient-id>.docx. For landscape, the generator swaps the portrait-dimension table values so the long edge lands on @w:w."
},
"fixtures": {
"machine": "docx/section-page-size--portrait--b4",
"office": "docx/section-page-size--portrait--b4"
},
"generator": {
"python": "scripts/gen_section_page_size.py",
"arg_template": "--paper {paper.id} --orient {orient.val} --out fixtures/docx/section-page-size--{orient.id}--{paper.id}.docx"
},
"_expansion": {
"parent_id": "docx/section-page-size",
"bindings": {
"orient": "portrait",
"paper": "b4"
}
},
"assertions": [
{
"id": "pgsz-orient-b4-portrait",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:sectPr/w:pgSz/@w:orient",
"must": "equal",
"value": "portrait",
"description": "w:pgSz@w:orient must equal 'portrait' for b4 portrait."
}
]
}
