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).
| Feature ID | Axis bindings | python-pptx | pptxjs |
|---|---|---|---|
pptx/round-trip-layout-inheritance--inherit-from-master | case=inherit-from-master | — | — |
pptx/round-trip-layout-inheritance--override-present | case=override-present | — | — |
| Library | Pass | Fail | Pending |
|---|---|---|---|
python-pptx | 0 | 0 | 2 |
pptxjs | 0 | 0 | 2 |
case (2 values): override-present, inherit-from-masterscripts/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()
{
"$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."
}
]
}