返回 ppt-master
transitions.py
1 """apply: page-to-page transitions for cloned slides.
2
3 Template Fill preserves each source transition unless the CLI or a per-slide
4 plan entry requests a replacement. Effects and OOXML mutation come from the
5 shared ``pptx_transitions`` core so every PPTX path uses the same writer.
6 """
7
8 from __future__ import annotations
9
10 from typing import Any
11 from xml.etree import ElementTree as ET
12
13 from pptx_transitions import (
14 AdvanceUpdate,
15 EnterUpdate,
16 apply_slide_motion,
17 normalize_transition_effect_request,
18 validate_seconds,
19 )
20
21 KEEP_TRANSITION = "keep"
22 # Preserve source transitions unless the CLI or a per-slide plan entry selects
23 # a replacement. The duration is consumed only when a visual effect is written.
24 DEFAULT_TRANSITION = KEEP_TRANSITION
25 DEFAULT_TRANSITION_DURATION = 0.5
26 TRANSITION_OBJECT_FIELDS = frozenset(
27 {
28 "effect",
29 "effect_options",
30 "duration",
31 "advance_after",
32 }
33 )
34
35 _UNSET = object()
36
37
38 def transition_unknown_fields(raw: dict[str, Any]) -> list[str]:
39 """Return unsupported fields from a per-slide transition object."""
40 return sorted(set(raw) - TRANSITION_OBJECT_FIELDS)
41
42
43 def _set_slide_transition(
44 slide_root: ET.Element,
45 *,
46 effect: str | None,
47 duration: float,
48 effect_options: dict[str, object] | None = None,
49 advance_after: float | None = None,
50 ) -> bool:
51 """Apply a legacy template-fill transition through the shared core.
52
53 ``None`` and ``keep`` preserve the source transition. ``none`` removes the
54 visual transition while retaining an explicitly requested auto-advance.
55 Legacy ``advance_after`` allowed both click and timed advance, so it maps to
56 ``both`` rather than the stricter ``after`` mode. The return value reports
57 whether the resulting slide contains an automatic advance.
58 """
59 if effect is None or effect == KEEP_TRANSITION:
60 enter = EnterUpdate(policy="preserve")
61 advance = AdvanceUpdate(
62 mode="preserve" if advance_after is None else "both",
63 after=advance_after,
64 )
65 elif effect == "none":
66 enter = EnterUpdate(policy="none", effect=None, duration=duration)
67 advance = AdvanceUpdate(
68 mode="click" if advance_after is None else "both",
69 after=advance_after,
70 )
71 else:
72 enter = EnterUpdate(
73 policy="replace",
74 effect=effect,
75 duration=duration,
76 effect_options=effect_options,
77 )
78 advance = AdvanceUpdate(
79 mode="click" if advance_after is None else "both",
80 after=advance_after,
81 )
82
83 try:
84 return apply_slide_motion(
85 slide_root,
86 enter=enter,
87 advance=advance,
88 )
89 except ValueError as exc:
90 raise RuntimeError(str(exc)) from exc
91
92
93 def _resolve_slide_transition(
94 item: dict[str, Any],
95 *,
96 default_effect: str | None,
97 default_duration: float,
98 ) -> tuple[str | None, dict[str, object], float, float | None]:
99 """Pick a slide's transition from its plan entry, falling back to CLI defaults."""
100 raw = item.get("transition", _UNSET)
101 if raw is _UNSET:
102 if default_effect in (None, "none", KEEP_TRANSITION):
103 return default_effect, {}, default_duration, None
104 effect, effect_options = normalize_transition_effect_request(
105 default_effect,
106 allow_none=False,
107 )
108 return effect, effect_options, default_duration, None
109 if isinstance(raw, dict):
110 unknown = transition_unknown_fields(raw)
111 if unknown:
112 raise RuntimeError(
113 "Transition has unknown field(s): " + ", ".join(unknown)
114 )
115 effect = raw.get("effect", default_effect)
116 raw_options = raw.get("effect_options")
117 if raw_options is not None and "effect" not in raw:
118 raise RuntimeError(
119 "Transition effect_options requires an explicit effect"
120 )
121 duration = raw.get("duration", default_duration)
122 advance_after = raw.get("advance_after")
123 else:
124 effect = None if raw is None else str(raw)
125 raw_options = None
126 duration = default_duration
127 advance_after = None
128 effect_options: dict[str, object] = {}
129 if effect is not None and effect not in ("none", KEEP_TRANSITION):
130 try:
131 effect, effect_options = normalize_transition_effect_request(
132 effect,
133 raw_options,
134 allow_none=False,
135 )
136 except ValueError as exc:
137 raise RuntimeError(str(exc)) from exc
138 elif raw_options not in (None, {}):
139 raise RuntimeError(
140 "Transition effect_options requires an explicit native effect"
141 )
142 try:
143 resolved_duration = validate_seconds(
144 duration,
145 "transition duration",
146 allow_zero=False,
147 )
148 except ValueError as exc:
149 raise RuntimeError(str(exc)) from exc
150 return effect, effect_options, resolved_duration, advance_after
151
151 lines PYTHON