pptx/preserve-extlst-creationid

W6-B tombstone: a slide that Office authored with a `p:extLst` containing a `p:ext uri="{BB962C8B-B14F-4D97-AF65-F5344CB8AC3E}"` wrapping `p14:creationId` must still carry that extension after being loaded and re-saved by python-pptx. Office emits this extension on every slide created in PowerPoint 2010+; if python-pptx ever strips unknown-URI extensions on save it will silently destroy provenance metadata on every re-saved deck. This tombstone catches such a regression: the generator loads `prs-add-slide.pptx` from the python-pptx feature fixtures (an Office-authored file known to carry the extension), re-saves with python-pptx, and the assertion below must still find `p14:creationId` on slide1.

Library verdicts: python-pptx: — pptxjs: —

Metadata

Feature idpptx/preserve-extlst-creationid
Formatpptx
Categoryround-trip-fidelity
Spececma-376-5-part-1 § 19.3.1.12 p:extLst

XPath assertions

ID / partPredicateXPathpython-pptxpptxjs
extension-uri-is-creationid
ppt/slides/slide1.xml
exist
After python-pptx round-trips the fixture, slide1 must still carry a p:ext with the PowerPoint creationId URI.
/p:sld/p:cSld/p:extLst/p:ext[@uri='{BB962C8B-B14F-4D97-AF65-F5344CB8AC3E}']
creationid-child-survives
ppt/slides/slide1.xml
exist
The p14:creationId element inside the extension must still be present.
/p:sld/p:cSld/p:extLst/p:ext/p14:creationId
creationid-val-preserved
ppt/slides/slide1.xml
match = ^[0-9]+$
The @val attribute on p14:creationId must still be a numeric identifier (PowerPoint writes an unsigned 32-bit integer).
/p:sld/p:cSld/p:extLst/p:ext/p14:creationId/@val

Render assertions

No render assertions declared.

Generator source

scripts/gen_preserve_extlst_creationid.py

#!/usr/bin/env python3
"""Generate the ``preserve-extlst-creationid`` tombstone fixture.

Tombstone generator: loads an Office-authored fixture containing
``<p:extLst><p:ext uri="{BB962C8B-...}"><p14:creationId/>`` on a slide,
round-trips it through ``python-pptx`` (``Presentation(...).save(...)``),
and writes the re-saved archive to the output path. The tombstone
manifest then asserts that the ``p14:creationId`` extension element
still resolves on the saved slide.

Input fixture: the generator copies an Office-authored fixture from the
local python-pptx checkout by default. Override with ``--source`` if
you need to test a different starting point.
"""

from __future__ import annotations

import argparse
import datetime as _dt
from pathlib import Path

from pptx import Presentation

_REPO_ROOT = Path(__file__).resolve().parent.parent
_REPRODUCIBLE_DT = _dt.datetime(2000, 1, 1, 0, 0, 0)

# Office-authored input that contains <p:extLst><p:ext uri="{BB962C8B-...}">
# <p14:creationId/>. Verified via the W6-B audit against
# python-pptx/features/steps/test_files.
_DEFAULT_SOURCE = Path(
    "/home/ben/code/python-pptx/features/steps/test_files/prs-add-slide.pptx"
)


def _roundtrip(source: Path, out: Path) -> None:
    prs = Presentation(str(source))
    out.parent.mkdir(parents=True, exist_ok=True)
    prs.save(out, zip_date_time=_REPRODUCIBLE_DT)


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--source",
        type=Path,
        default=_DEFAULT_SOURCE,
        help="Input .pptx containing a p14:creationId extension on slide1.",
    )
    parser.add_argument(
        "--out",
        required=True,
        help="Output fixture 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
    _roundtrip(args.source, out_path)
    print(out_path)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Manifest (expanded)

{
  "$schema": "../manifest.schema.json",
  "id": "pptx/preserve-extlst-creationid",
  "kind": "literal",
  "title": "Preserve <p:extLst>/<p14:creationId> through python-pptx load+save",
  "format": "pptx",
  "category": "round-trip-fidelity",
  "summary": "W6-B tombstone: a slide that Office authored with a `p:extLst` containing a `p:ext uri=\"{BB962C8B-B14F-4D97-AF65-F5344CB8AC3E}\"` wrapping `p14:creationId` must still carry that extension after being loaded and re-saved by python-pptx. Office emits this extension on every slide created in PowerPoint 2010+; if python-pptx ever strips unknown-URI extensions on save it will silently destroy provenance metadata on every re-saved deck. This tombstone catches such a regression: the generator loads `prs-add-slide.pptx` from the python-pptx feature fixtures (an Office-authored file known to carry the extension), re-saves with python-pptx, and the assertion below must still find `p14:creationId` on slide1.",
  "spec": {
    "source": "ecma-376-5-part-1",
    "clause": "19.3.1.12",
    "element": "p:extLst",
    "rnc_reference": "spec/ecma-376-5/part-1/rnc/pml.rnc",
    "xsd_reference": "spec/ecma-376-5/part-1/xsd/pml.xsd",
    "notes": "p:ext is the generic spec-compliant container for future-or-vendor extensions. PowerPoint 2010+ populates it with <p14:creationId val=\"...\"/> (Microsoft's per-slide provenance ID) gated by the URI {BB962C8B-B14F-4D97-AF65-F5344CB8AC3E}. A conformant consumer must preserve p:ext subtrees it does not understand (ECMA-376 Part 3 'markup compatibility'). Dropping them is a silent fidelity bug."
  },
  "fixtures": {
    "machine": "pptx/preserve-extlst-creationid"
  },
  "generator": {
    "python": "scripts/gen_preserve_extlst_creationid.py",
    "arg_template": "--out fixtures/pptx/preserve-extlst-creationid.pptx"
  },
  "assertions": [
    {
      "id": "extension-uri-is-creationid",
      "part": "ppt/slides/slide1.xml",
      "namespaces": {
        "p": "http://schemas.openxmlformats.org/presentationml/2006/main"
      },
      "xpath": "/p:sld/p:cSld/p:extLst/p:ext[@uri='{BB962C8B-B14F-4D97-AF65-F5344CB8AC3E}']",
      "must": "exist",
      "description": "After python-pptx round-trips the fixture, slide1 must still carry a p:ext with the PowerPoint creationId URI."
    },
    {
      "id": "creationid-child-survives",
      "part": "ppt/slides/slide1.xml",
      "namespaces": {
        "p": "http://schemas.openxmlformats.org/presentationml/2006/main",
        "p14": "http://schemas.microsoft.com/office/powerpoint/2010/main"
      },
      "xpath": "/p:sld/p:cSld/p:extLst/p:ext/p14:creationId",
      "must": "exist",
      "description": "The p14:creationId element inside the extension must still be present."
    },
    {
      "id": "creationid-val-preserved",
      "part": "ppt/slides/slide1.xml",
      "namespaces": {
        "p": "http://schemas.openxmlformats.org/presentationml/2006/main",
        "p14": "http://schemas.microsoft.com/office/powerpoint/2010/main"
      },
      "xpath": "/p:sld/p:cSld/p:extLst/p:ext/p14:creationId/@val",
      "must": "match",
      "value": "^[0-9]+$",
      "description": "The @val attribute on p14:creationId must still be a numeric identifier (PowerPoint writes an unsigned 32-bit integer)."
    }
  ]
}

Fixture

Download preserve-extlst-creationid.pptx (13.8 KB)

Reference preview

No rendered reference is available for this case.

Spec notes

p:ext is the generic spec-compliant container for future-or-vendor extensions. PowerPoint 2010+ populates it with <p14:creationId val="..."/> (Microsoft's per-slide provenance ID) gated by the URI {BB962C8B-B14F-4D97-AF65-F5344CB8AC3E}. A conformant consumer must preserve p:ext subtrees it does not understand (ECMA-376 Part 3 'markup compatibility'). Dropping them is a silent fidelity bug.