pptx/round-trip-layout-inheritance

A slide that overrides its inherited master theme via a themeOverride part is a common inheritance-chain trap: the slide holds a relationship to themeOverride1.xml, the Content_Types entry registers it, and both must survive a load/save cycle. This family covers the positive case (override present) and the negative case (inherit from master — no override must materialise out of thin air).

Cases (2)

Feature IDAxis bindingspython-pptxpptxjs
pptx/round-trip-layout-inheritance--inherit-from-mastercase=inherit-from-master
pptx/round-trip-layout-inheritance--override-presentcase=override-present

Aggregate

LibraryPassFailPending
python-pptx002
pptxjs002

Parameter axes

Spec notes

Borrows the existing slide-master-theme-override fixtures from W2-E. The override case asserts both the part and the relationship survive; the inherit case asserts they remain absent (a naive save that always emits a themeOverride would break the negative case).

Generator source

scripts/gen_round_trip_layout_inheritance_pptx.py — runs with --arg_template --source fixtures/{case.source}.pptx --out fixtures/pptx/round-trip-layout-inheritance--{case.id}.pptx

#!/usr/bin/env python3
"""Generate a pptx layout-inheritance round-trip fixture.

Load the source (a slide-master-theme-override W2-E fixture) via
python-pptx and save it to the named output. Deliberately shares its
body with :mod:`gen_round_trip_identity_pptx` — the distinguishing
behaviour is asserted by the feature manifest, not by the script.
"""

from __future__ import annotations

import argparse
import zipfile
from pathlib import Path

from pptx import Presentation  # type: ignore[import-not-found]


def _rezip_with_epoch_timestamps(path: Path) -> None:
    tmp = path.with_suffix(path.suffix + ".tmp")
    with zipfile.ZipFile(path, "r") as src, zipfile.ZipFile(
        tmp, "w", zipfile.ZIP_DEFLATED
    ) as dst:
        for info in src.infolist():
            data = src.read(info.filename)
            info.date_time = (1980, 1, 1, 0, 0, 0)
            dst.writestr(info, data)
    tmp.replace(path)


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--source", required=True)
    ap.add_argument("--out", required=True)
    args = ap.parse_args()

    out = Path(args.out)
    out.parent.mkdir(parents=True, exist_ok=True)
    Presentation(args.source).save(str(out))
    _rezip_with_epoch_timestamps(out)


if __name__ == "__main__":
    main()

Manifest (parent, unexpanded)

{
  "$schema": "../manifest.schema.json",
  "id": "pptx/round-trip-layout-inheritance",
  "kind": "parameterised",
  "title": "Round-trip preserves themeOverride and layout inheritance chain",
  "format": "pptx",
  "category": "round-trip",
  "summary": "A slide that overrides its inherited master theme via a themeOverride part is a common inheritance-chain trap: the slide holds a relationship to themeOverride1.xml, the Content_Types entry registers it, and both must survive a load/save cycle. This family covers the positive case (override present) and the negative case (inherit from master — no override must materialise out of thin air).",
  "spec": {
    "source": "ecma-376-5-part-1",
    "clause": "19.3.1.45",
    "element": "a:themeOverride",
    "xsd_reference": "spec/ecma-376-5/part-1/xsd/pml.xsd",
    "notes": "Borrows the existing slide-master-theme-override fixtures from W2-E. The override case asserts both the part and the relationship survive; the inherit case asserts they remain absent (a naive save that always emits a themeOverride would break the negative case)."
  },
  "fixtures": {
    "machine": "pptx/round-trip-layout-inheritance"
  },
  "round_trip": {
    "library": "python-pptx",
    "source_fixture": "{case.source}"
  },
  "parameters": {
    "case": [
      {
        "id": "override-present",
        "source": "pptx/slide-master-theme-override--override-via-themeOverride",
        "expect_override": "exist"
      },
      {
        "id": "inherit-from-master",
        "source": "pptx/slide-master-theme-override--inherit-from-master",
        "expect_override": "absent"
      }
    ]
  },
  "generator": {
    "python": "scripts/gen_round_trip_layout_inheritance_pptx.py",
    "arg_template": "--source fixtures/{case.source}.pptx --out fixtures/pptx/round-trip-layout-inheritance--{case.id}.pptx"
  },
  "assertions_template": [
    {
      "id": "slide-1-present",
      "part": "ppt/slides/slide1.xml",
      "namespaces": {
        "p": "http://schemas.openxmlformats.org/presentationml/2006/main"
      },
      "xpath": "//p:sld",
      "must": "exist",
      "description": "The slide itself must still serialise after the round trip — baseline sanity."
    },
    {
      "id": "slide-layout-rel-preserved",
      "part": "ppt/slides/_rels/slide1.xml.rels",
      "namespaces": {
        "r": "http://schemas.openxmlformats.org/package/2006/relationships"
      },
      "xpath": "//r:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout']",
      "must": "exist",
      "description": "The slide -> slideLayout relationship is the first link in the inheritance chain and must survive."
    },
    {
      "id": "layout-master-rel-preserved",
      "part": "ppt/slideLayouts/_rels/slideLayout6.xml.rels",
      "namespaces": {
        "r": "http://schemas.openxmlformats.org/package/2006/relationships"
      },
      "xpath": "//r:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster']",
      "must": "exist",
      "description": "The slideLayout -> slideMaster relationship is the second link. A load/save that rebuilds parts but drops this rel breaks inheritance silently."
    },
    {
      "id": "theme-override-relationship",
      "part": "ppt/slides/_rels/slide1.xml.rels",
      "namespaces": {
        "r": "http://schemas.openxmlformats.org/package/2006/relationships"
      },
      "xpath": "//r:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/themeOverride']",
      "must": "{case.expect_override}",
      "description": "Positive case: the themeOverride rel must be present after round-trip. Negative case: it must NOT have materialised."
    }
  ]
}