docx/section-page-size--portrait--ledger

Section <w:pgSz> carries paper dimensions (@w:w, @w:h in twips) and orientation (@w:orient).

Library verdicts: python-docx: — docxjs: —

Metadata

Feature iddocx/section-page-size--portrait--ledger
Formatdocx
Categorypage-layout
Familydocx/section-page-size
Axis valuesorient=portrait, paper=ledger
Spececma-376-5-part-1 § 17.6.13 w:pgSz

XPath assertions

ID / partPredicateXPathpython-docxdocxjs
pgsz-orient-ledger-portrait
word/document.xml
equal = portrait
w:pgSz@w:orient must equal 'portrait' for ledger portrait.
//w:sectPr/w:pgSz/@w:orient

Render assertions

No render assertions declared.

Generator source

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())

Manifest (expanded)

{
  "$schema": "../manifest.schema.json",
  "id": "docx/section-page-size--portrait--ledger",
  "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--ledger",
    "office": "docx/section-page-size--portrait--ledger"
  },
  "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": "ledger"
    }
  },
  "assertions": [
    {
      "id": "pgsz-orient-ledger-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 ledger portrait."
    }
  ]
}

Fixture

Download section-page-size--portrait--ledger.docx (18.6 KB)

Reference preview

Reference (machine, page 1 PNG)

docx/section-page-size--portrait--ledger page 1 reference

Reference PDF


Download PDF Open in PDF.js

Spec 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.