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: —
| Feature id | pptx/preserve-chart-parts |
|---|---|
| Format | pptx |
| Category | round-trip-fidelity |
| Spec | ecma-376-5-part-1 § 14.2.5 c:chartSpace |
| ID / part | Predicate | XPath | python-pptx | pptxjs |
|---|---|---|---|---|
chart1-xml-part-survivesppt/charts/chart1.xml | existThe 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-survivesppt/charts/chartEx1.xml | existThe modern chartEx1.xml part must still be present with a <cx:chartSpace> root. | /cx:chartSpace | — | — |
chart1-rels-to-xlsx-survivesppt/charts/_rels/chart1.xml.rels | existchart1.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-survivesppt/charts/_rels/chartEx1.xml.rels | existchartEx1.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 | existThe [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 | existThe [Content_Types].xml Override for the modern chartEx part must still be emitted. | /ct:Types/ct:Override[@PartName='/ppt/charts/chartEx1.xml'] | — | — |
No render assertions declared.
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())
{
"$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."
}
]
}
No rendered reference is available for this case.