pptx/preserve-chart-parts

W6-B tombstone: a slide with both a legacy `<c:chart>` (chart1.xml) and a modern `<cx:chartSpace>` (chartEx1.xml) backed by an embedded Microsoft_Excel_Sheet1.xlsx must keep all chart parts, their .rels files, and the embedded spreadsheet on save. python-pptx 1.0.2 shipped to PyPI silently dropped chart1.xml, its .rels, chartEx1.xml.rels, and the embedded xlsx for the python-pptx feature fixture cht-chartex.pptx. The regression was fixed on the fork's master branch; this tombstone guards against it regressing again. The generator re-saves the cht-chartex fixture via python-pptx and the assertions below verify the chart graph survived the round-trip.

Library verdicts: python-pptx: — pptxjs: —

Metadata

Feature idpptx/preserve-chart-parts
Formatpptx
Categoryround-trip-fidelity
Spececma-376-5-part-1 § 14.2.5 c:chartSpace

XPath assertions

ID / partPredicateXPathpython-pptxpptxjs
chart1-xml-part-survives
ppt/charts/chart1.xml
exist
The legacy chart1.xml part must still be present with a <c:chartSpace> root. (If this part went missing the conformance runner will fail to parse the part, which is equally fatal.)
/c:chartSpace
chartex1-xml-part-survives
ppt/charts/chartEx1.xml
exist
The modern chartEx1.xml part must still be present with a <cx:chartSpace> root.
/cx:chartSpace
chart1-rels-to-xlsx-survives
ppt/charts/_rels/chart1.xml.rels
exist
chart1.xml.rels must still declare a relationship to the embedded .xlsx spreadsheet (without this, PowerPoint cannot edit the chart data).
/pr:Relationships/pr:Relationship[contains(@Target,'.xlsx')]
chartex1-rels-part-survives
ppt/charts/_rels/chartEx1.xml.rels
exist
chartEx1.xml.rels must still exist as a parseable part (even if it declares zero relationships — the Office-authored source file has an empty rels root, and the writer must not drop the empty part because a missing rels part breaks the OPC package structure).
/pr:Relationships
content-types-declares-chart1
[Content_Types].xml
exist
The [Content_Types].xml Override for the legacy chart part must still be emitted; if this disappears PowerPoint flags the archive as corrupt.
/ct:Types/ct:Override[@PartName='/ppt/charts/chart1.xml']
content-types-declares-chartex1
[Content_Types].xml
exist
The [Content_Types].xml Override for the modern chartEx part must still be emitted.
/ct:Types/ct:Override[@PartName='/ppt/charts/chartEx1.xml']

Render assertions

No render assertions declared.

Generator source

scripts/gen_preserve_chart_parts.py

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

Tombstone generator: loads an Office-authored fixture that contains a
modern ``chartEx1`` chart *plus* a sibling legacy ``chart1.xml`` fallback
*plus* the embedded spreadsheet (``Microsoft_Excel_Sheet1.xlsx``) that
backs the chart data. The generator re-saves the deck via
``python-pptx``; the manifest asserts every chart-related part
survives on disk.

Motivation: python-pptx 1.0.2 (released on PyPI) silently dropped
``chart1.xml``, its ``.rels``, the ``.rels`` for ``chartEx1``, and the
embedded xlsx on save for this fixture. The drop was fixed on the
fork's ``master`` branch during Wave 24. This tombstone prevents the
regression from coming back — if anybody ever tears out the transparent
pass-through for ``chart`` / ``chartEx`` relationship targets again,
this test case will fail loudly.
"""

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 both a chart1 (legacy .chart) and
# a chartEx1 (modern extended chart) on slide1, with the backing xlsx.
_DEFAULT_SOURCE = Path(
    "/home/ben/code/python-pptx/features/steps/test_files/cht-chartex.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 both chart1.xml and chartEx1.xml.",
    )
    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-chart-parts",
  "kind": "literal",
  "title": "Preserve chart + chartEx + embedded xlsx through python-pptx load+save",
  "format": "pptx",
  "category": "round-trip-fidelity",
  "summary": "W6-B tombstone: a slide with both a legacy `<c:chart>` (chart1.xml) and a modern `<cx:chartSpace>` (chartEx1.xml) backed by an embedded Microsoft_Excel_Sheet1.xlsx must keep all chart parts, their .rels files, and the embedded spreadsheet on save. python-pptx 1.0.2 shipped to PyPI silently dropped chart1.xml, its .rels, chartEx1.xml.rels, and the embedded xlsx for the python-pptx feature fixture cht-chartex.pptx. The regression was fixed on the fork's master branch; this tombstone guards against it regressing again. The generator re-saves the cht-chartex fixture via python-pptx and the assertions below verify the chart graph survived the round-trip.",
  "spec": {
    "source": "ecma-376-5-part-1",
    "clause": "14.2.5",
    "element": "c:chartSpace",
    "notes": "ECMA-376 Part 1 clause 14.2 defines the DrawingML chart content type and its relationship to the package (chart-rels point at an embedded xlsx via oleObject or chartSpace/externalData). Modern `<cx:chartSpace>` (Office 2014+ Extended Charts — waterfall, funnel, treemap, sunburst, histogram, box-and-whisker) lives in a sibling content type `application/vnd.ms-office.chartex+xml` under the `cx:` namespace. PowerPoint frequently emits both a legacy `<c:chart>` (chart1.xml) — renderable by pre-2016 PowerPoint via mc:Fallback — and a `<cx:chartSpace>` (chartEx1.xml) — rendered by 2016+ via mc:Choice. Dropping either one breaks a deck in at least half of the deployed PowerPoint population."
  },
  "fixtures": {
    "machine": "pptx/preserve-chart-parts"
  },
  "generator": {
    "python": "scripts/gen_preserve_chart_parts.py",
    "arg_template": "--out fixtures/pptx/preserve-chart-parts.pptx"
  },
  "assertions": [
    {
      "id": "chart1-xml-part-survives",
      "part": "ppt/charts/chart1.xml",
      "namespaces": {
        "c": "http://schemas.openxmlformats.org/drawingml/2006/chart"
      },
      "xpath": "/c:chartSpace",
      "must": "exist",
      "description": "The legacy chart1.xml part must still be present with a <c:chartSpace> root. (If this part went missing the conformance runner will fail to parse the part, which is equally fatal.)"
    },
    {
      "id": "chartex1-xml-part-survives",
      "part": "ppt/charts/chartEx1.xml",
      "namespaces": {
        "cx": "http://schemas.microsoft.com/office/drawing/2014/chartex"
      },
      "xpath": "/cx:chartSpace",
      "must": "exist",
      "description": "The modern chartEx1.xml part must still be present with a <cx:chartSpace> root."
    },
    {
      "id": "chart1-rels-to-xlsx-survives",
      "part": "ppt/charts/_rels/chart1.xml.rels",
      "namespaces": {
        "pr": "http://schemas.openxmlformats.org/package/2006/relationships"
      },
      "xpath": "/pr:Relationships/pr:Relationship[contains(@Target,'.xlsx')]",
      "must": "exist",
      "description": "chart1.xml.rels must still declare a relationship to the embedded .xlsx spreadsheet (without this, PowerPoint cannot edit the chart data)."
    },
    {
      "id": "chartex1-rels-part-survives",
      "part": "ppt/charts/_rels/chartEx1.xml.rels",
      "namespaces": {
        "pr": "http://schemas.openxmlformats.org/package/2006/relationships"
      },
      "xpath": "/pr:Relationships",
      "must": "exist",
      "description": "chartEx1.xml.rels must still exist as a parseable part (even if it declares zero relationships — the Office-authored source file has an empty rels root, and the writer must not drop the empty part because a missing rels part breaks the OPC package structure)."
    },
    {
      "id": "content-types-declares-chart1",
      "part": "[Content_Types].xml",
      "namespaces": {
        "ct": "http://schemas.openxmlformats.org/package/2006/content-types"
      },
      "xpath": "/ct:Types/ct:Override[@PartName='/ppt/charts/chart1.xml']",
      "must": "exist",
      "description": "The [Content_Types].xml Override for the legacy chart part must still be emitted; if this disappears PowerPoint flags the archive as corrupt."
    },
    {
      "id": "content-types-declares-chartex1",
      "part": "[Content_Types].xml",
      "namespaces": {
        "ct": "http://schemas.openxmlformats.org/package/2006/content-types"
      },
      "xpath": "/ct:Types/ct:Override[@PartName='/ppt/charts/chartEx1.xml']",
      "must": "exist",
      "description": "The [Content_Types].xml Override for the modern chartEx part must still be emitted."
    }
  ]
}

Fixture

Download preserve-chart-parts.pptx (64.3 KB)

Reference preview

No rendered reference is available for this case.

Spec notes

ECMA-376 Part 1 clause 14.2 defines the DrawingML chart content type and its relationship to the package (chart-rels point at an embedded xlsx via oleObject or chartSpace/externalData). Modern `<cx:chartSpace>` (Office 2014+ Extended Charts — waterfall, funnel, treemap, sunburst, histogram, box-and-whisker) lives in a sibling content type `application/vnd.ms-office.chartex+xml` under the `cx:` namespace. PowerPoint frequently emits both a legacy `<c:chart>` (chart1.xml) — renderable by pre-2016 PowerPoint via mc:Fallback — and a `<cx:chartSpace>` (chartEx1.xml) — rendered by 2016+ via mc:Choice. Dropping either one breaks a deck in at least half of the deployed PowerPoint population.