返回 ppt-master
pptx_effects.py
根目录 / skills / ppt-master / scripts / pptx_effects.py
1 #!/usr/bin/env python3
2 """Shared diagnostic contract for unsupported imported object effects."""
3
4 from __future__ import annotations
5
6 import json
7 from xml.etree import ElementTree as ET
8
9
10 EFFECT_STATUS_ATTR = "data-pptx-effect-status"
11 EFFECT_REASON_ATTR = "data-pptx-effect-reason"
12 UNSUPPORTED_EFFECT_STATUS = "unsupported"
13 _EFFECT_OBJECT_IDENTITY_ATTRS = (
14 "data-pptx-object",
15 "data-pptx-shape-id",
16 "data-pptx-shape-scope",
17 )
18 _DML_NAMESPACE = "http://schemas.openxmlformats.org/drawingml/2006/main"
19 _TEXT_PROPERTY_TAGS = frozenset({
20 f"{{{_DML_NAMESPACE}}}defRPr",
21 f"{{{_DML_NAMESPACE}}}endParaRPr",
22 f"{{{_DML_NAMESPACE}}}rPr",
23 })
24 _RUN_EFFECT_CONTAINER_TAGS = frozenset({
25 f"{{{_DML_NAMESPACE}}}effectLst",
26 f"{{{_DML_NAMESPACE}}}effectDag",
27 })
28
29
30 def project_effect_status_errors(root: ET.Element) -> list[str]:
31 """Return blocking diagnostics for invalid or unsupported effect metadata."""
32 errors: set[str] = set()
33 parents = {
34 child: parent
35 for parent in root.iter()
36 for child in parent
37 }
38 for elem in root.iter():
39 raw_status = elem.get(EFFECT_STATUS_ATTR)
40 raw_reason = elem.get(EFFECT_REASON_ATTR)
41 if raw_status is None and raw_reason is None:
42 continue
43 parent = parents.get(elem)
44 if (
45 parent is not None
46 and parent.get(EFFECT_STATUS_ATTR) == raw_status
47 and parent.get(EFFECT_REASON_ATTR) == raw_reason
48 and _same_source_object(parent, elem)
49 ):
50 # Import duplicates the marker on the logical object and carrier
51 # so stripping either copy cannot erase the block. Report it once.
52 continue
53 label = _element_label(elem)
54 status = (raw_status or "").strip()
55 if status != UNSUPPORTED_EFFECT_STATUS:
56 errors.add(
57 f'{label} {EFFECT_STATUS_ATTR} must equal '
58 f'{UNSUPPORTED_EFFECT_STATUS!r}; got {raw_status!r}'
59 )
60 continue
61 reason = (raw_reason or "").strip()
62 if not reason:
63 errors.add(
64 f'{label} {EFFECT_REASON_ATTR} requires a non-empty reason'
65 )
66 continue
67 errors.add(f'{label} has unsupported source PPTX effect: {reason}')
68 return sorted(errors)
69
70
71 def unsupported_effect_metadata(*reasons: str) -> dict[str, str]:
72 """Build one canonical import marker without dropping compound reasons."""
73 normalized: set[str] = set()
74 for reason in reasons:
75 reason = reason.strip()
76 if not reason:
77 raise ValueError("Unsupported PPTX effect reason must not be empty")
78 items: object = reason
79 if reason.startswith("["):
80 try:
81 items = json.loads(reason)
82 except json.JSONDecodeError:
83 pass
84 if not isinstance(items, list):
85 items = [reason]
86 if not all(isinstance(item, str) and item.strip() for item in items):
87 raise ValueError("Unsupported PPTX effect reasons must be strings")
88 normalized.update(item.strip() for item in items)
89 if not normalized:
90 raise ValueError("Unsupported PPTX effect reason must not be empty")
91 ordered = sorted(normalized)
92 encoded = (
93 ordered[0]
94 if len(ordered) == 1
95 else json.dumps(ordered, separators=(",", ":"))
96 )
97 return {
98 EFFECT_STATUS_ATTR: UNSUPPORTED_EFFECT_STATUS,
99 EFFECT_REASON_ATTR: encoded,
100 }
101
102
103 def txbody_has_run_effects(*text_style_roots: ET.Element | None) -> bool:
104 """Return whether rebuilding any supplied text style would lose an effect."""
105 for root in text_style_roots:
106 if root is None:
107 continue
108 for properties in root.iter():
109 if properties.tag not in _TEXT_PROPERTY_TAGS:
110 continue
111 for child in properties:
112 if child.tag in _RUN_EFFECT_CONTAINER_TAGS and any(
113 isinstance(effect.tag, str)
114 for effect in child
115 ):
116 return True
117 return False
118
119
120 def _element_label(elem: ET.Element) -> str:
121 tag = elem.tag.rsplit("}", 1)[-1]
122 elem_id = elem.get("id") or elem.get("data-name")
123 if elem_id:
124 return f'<{tag} id="{elem_id}">'
125 shape_id = elem.get("data-pptx-shape-id")
126 if shape_id:
127 object_kind = elem.get("data-pptx-object") or "object"
128 scope = elem.get("data-pptx-shape-scope") or "unknown"
129 return (
130 f'<{tag} data-pptx-object="{object_kind}" '
131 f'data-pptx-shape-id="{shape_id}" '
132 f'data-pptx-shape-scope="{scope}">'
133 )
134 return f"<{tag}>"
135
136
137 def _same_source_object(parent: ET.Element, child: ET.Element) -> bool:
138 """Return whether a parent/child marker describes one imported object."""
139 return all(
140 child.get(attr) is not None
141 and child.get(attr) == parent.get(attr)
142 for attr in _EFFECT_OBJECT_IDENTITY_ATTRS
143 )
144
144 lines PYTHON