Files
Templated-PPT-Master/runtime/ppt-master/scripts/pptx_effects.py
T

144 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""Shared diagnostic contract for unsupported imported object effects."""
from __future__ import annotations
import json
from xml.etree import ElementTree as ET
EFFECT_STATUS_ATTR = "data-pptx-effect-status"
EFFECT_REASON_ATTR = "data-pptx-effect-reason"
UNSUPPORTED_EFFECT_STATUS = "unsupported"
_EFFECT_OBJECT_IDENTITY_ATTRS = (
"data-pptx-object",
"data-pptx-shape-id",
"data-pptx-shape-scope",
)
_DML_NAMESPACE = "http://schemas.openxmlformats.org/drawingml/2006/main"
_TEXT_PROPERTY_TAGS = frozenset({
f"{{{_DML_NAMESPACE}}}defRPr",
f"{{{_DML_NAMESPACE}}}endParaRPr",
f"{{{_DML_NAMESPACE}}}rPr",
})
_RUN_EFFECT_CONTAINER_TAGS = frozenset({
f"{{{_DML_NAMESPACE}}}effectLst",
f"{{{_DML_NAMESPACE}}}effectDag",
})
def project_effect_status_errors(root: ET.Element) -> list[str]:
"""Return blocking diagnostics for invalid or unsupported effect metadata."""
errors: set[str] = set()
parents = {
child: parent
for parent in root.iter()
for child in parent
}
for elem in root.iter():
raw_status = elem.get(EFFECT_STATUS_ATTR)
raw_reason = elem.get(EFFECT_REASON_ATTR)
if raw_status is None and raw_reason is None:
continue
parent = parents.get(elem)
if (
parent is not None
and parent.get(EFFECT_STATUS_ATTR) == raw_status
and parent.get(EFFECT_REASON_ATTR) == raw_reason
and _same_source_object(parent, elem)
):
# Import duplicates the marker on the logical object and carrier
# so stripping either copy cannot erase the block. Report it once.
continue
label = _element_label(elem)
status = (raw_status or "").strip()
if status != UNSUPPORTED_EFFECT_STATUS:
errors.add(
f'{label} {EFFECT_STATUS_ATTR} must equal '
f'{UNSUPPORTED_EFFECT_STATUS!r}; got {raw_status!r}'
)
continue
reason = (raw_reason or "").strip()
if not reason:
errors.add(
f'{label} {EFFECT_REASON_ATTR} requires a non-empty reason'
)
continue
errors.add(f'{label} has unsupported source PPTX effect: {reason}')
return sorted(errors)
def unsupported_effect_metadata(*reasons: str) -> dict[str, str]:
"""Build one canonical import marker without dropping compound reasons."""
normalized: set[str] = set()
for reason in reasons:
reason = reason.strip()
if not reason:
raise ValueError("Unsupported PPTX effect reason must not be empty")
items: object = reason
if reason.startswith("["):
try:
items = json.loads(reason)
except json.JSONDecodeError:
pass
if not isinstance(items, list):
items = [reason]
if not all(isinstance(item, str) and item.strip() for item in items):
raise ValueError("Unsupported PPTX effect reasons must be strings")
normalized.update(item.strip() for item in items)
if not normalized:
raise ValueError("Unsupported PPTX effect reason must not be empty")
ordered = sorted(normalized)
encoded = (
ordered[0]
if len(ordered) == 1
else json.dumps(ordered, separators=(",", ":"))
)
return {
EFFECT_STATUS_ATTR: UNSUPPORTED_EFFECT_STATUS,
EFFECT_REASON_ATTR: encoded,
}
def txbody_has_run_effects(*text_style_roots: ET.Element | None) -> bool:
"""Return whether rebuilding any supplied text style would lose an effect."""
for root in text_style_roots:
if root is None:
continue
for properties in root.iter():
if properties.tag not in _TEXT_PROPERTY_TAGS:
continue
for child in properties:
if child.tag in _RUN_EFFECT_CONTAINER_TAGS and any(
isinstance(effect.tag, str)
for effect in child
):
return True
return False
def _element_label(elem: ET.Element) -> str:
tag = elem.tag.rsplit("}", 1)[-1]
elem_id = elem.get("id") or elem.get("data-name")
if elem_id:
return f'<{tag} id="{elem_id}">'
shape_id = elem.get("data-pptx-shape-id")
if shape_id:
object_kind = elem.get("data-pptx-object") or "object"
scope = elem.get("data-pptx-shape-scope") or "unknown"
return (
f'<{tag} data-pptx-object="{object_kind}" '
f'data-pptx-shape-id="{shape_id}" '
f'data-pptx-shape-scope="{scope}">'
)
return f"<{tag}>"
def _same_source_object(parent: ET.Element, child: ET.Element) -> bool:
"""Return whether a parent/child marker describes one imported object."""
return all(
child.get(attr) is not None
and child.get(attr) == parent.get(attr)
for attr in _EFFECT_OBJECT_IDENTITY_ATTRS
)