Release standalone PPT template workflow 0.5.0
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"""Read native PowerPoint chart display caches for slide-library analysis.
|
||||
|
||||
The template-fill workflow edits chart data from explicit fill plans. This
|
||||
module only reads the data currently visible in a chart XML part, keeping
|
||||
workbook parsing out of the analyzer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from .ooxml import NS
|
||||
|
||||
|
||||
def empty_chart_data() -> dict[str, Any]:
|
||||
return {
|
||||
"chart_type": None,
|
||||
"category_count": 0,
|
||||
"series_count": 0,
|
||||
"categories": [],
|
||||
"series": [],
|
||||
}
|
||||
|
||||
|
||||
def _local_name(tag: str) -> str:
|
||||
return tag.rsplit("}", 1)[-1]
|
||||
|
||||
|
||||
def _chart_types_with_series(chart_root: ET.Element) -> list[ET.Element]:
|
||||
plot_area = chart_root.find(".//c:plotArea", NS)
|
||||
if plot_area is None:
|
||||
return []
|
||||
chart_types: list[ET.Element] = []
|
||||
for child in list(plot_area):
|
||||
if _local_name(child.tag).endswith("Chart") and child.findall("c:ser", NS):
|
||||
chart_types.append(child)
|
||||
return chart_types
|
||||
|
||||
|
||||
def _coerce_number(value: str) -> int | float | str:
|
||||
try:
|
||||
number = float(value)
|
||||
except ValueError:
|
||||
return value
|
||||
if number.is_integer():
|
||||
return int(number)
|
||||
return number
|
||||
|
||||
|
||||
def _cache_values(parent: ET.Element | None, *, numeric: bool = False) -> list[Any]:
|
||||
if parent is None:
|
||||
return []
|
||||
cache = parent.find(".//c:strCache", NS)
|
||||
if cache is None:
|
||||
cache = parent.find(".//c:numCache", NS)
|
||||
if cache is None:
|
||||
return []
|
||||
values: list[Any] = []
|
||||
for point in cache.findall("c:pt", NS):
|
||||
value = point.findtext("c:v", default="", namespaces=NS)
|
||||
values.append(_coerce_number(value) if numeric else value)
|
||||
return values
|
||||
|
||||
|
||||
def _series_name(series: ET.Element, fallback: str) -> str:
|
||||
tx = series.find("c:tx", NS)
|
||||
if tx is None:
|
||||
return fallback
|
||||
values = _cache_values(tx)
|
||||
if values:
|
||||
return str(values[0])
|
||||
direct = tx.findtext("c:v", default="", namespaces=NS)
|
||||
return direct or fallback
|
||||
|
||||
|
||||
def read_chart_data(chart_root: ET.Element) -> dict[str, Any]:
|
||||
"""Return a compact summary of chart type, categories, series, and values."""
|
||||
chart_types = _chart_types_with_series(chart_root)
|
||||
if not chart_types:
|
||||
return empty_chart_data()
|
||||
|
||||
first_series = chart_types[0].find("c:ser", NS)
|
||||
categories = _cache_values(first_series.find("c:cat", NS)) if first_series is not None else []
|
||||
series_payload: list[dict[str, Any]] = []
|
||||
plot_types: list[str] = []
|
||||
for chart_type in chart_types:
|
||||
plot_name = _local_name(chart_type.tag)
|
||||
plot_types.append(plot_name)
|
||||
for series in chart_type.findall("c:ser", NS):
|
||||
index = len(series_payload) + 1
|
||||
series_categories = _cache_values(series.find("c:cat", NS))
|
||||
if not categories and series_categories:
|
||||
categories = series_categories
|
||||
series_payload.append(
|
||||
{
|
||||
"name": _series_name(series, f"系列{index}"),
|
||||
"values": _cache_values(series.find("c:val", NS), numeric=True),
|
||||
"chart_type": plot_name,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"chart_type": plot_types[0] if len(set(plot_types)) == 1 else "comboChart",
|
||||
"plot_types": plot_types,
|
||||
"category_count": len(categories),
|
||||
"series_count": len(series_payload),
|
||||
"categories": categories,
|
||||
"series": series_payload,
|
||||
}
|
||||
Reference in New Issue
Block a user