151 lines
5.0 KiB
Python
151 lines
5.0 KiB
Python
"""apply: page-to-page transitions for cloned slides.
|
|
|
|
Template Fill preserves each source transition unless the CLI or a per-slide
|
|
plan entry requests a replacement. Effects and OOXML mutation come from the
|
|
shared ``pptx_transitions`` core so every PPTX path uses the same writer.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
from xml.etree import ElementTree as ET
|
|
|
|
from pptx_transitions import (
|
|
AdvanceUpdate,
|
|
EnterUpdate,
|
|
apply_slide_motion,
|
|
normalize_transition_effect_request,
|
|
validate_seconds,
|
|
)
|
|
|
|
KEEP_TRANSITION = "keep"
|
|
# Preserve source transitions unless the CLI or a per-slide plan entry selects
|
|
# a replacement. The duration is consumed only when a visual effect is written.
|
|
DEFAULT_TRANSITION = KEEP_TRANSITION
|
|
DEFAULT_TRANSITION_DURATION = 0.5
|
|
TRANSITION_OBJECT_FIELDS = frozenset(
|
|
{
|
|
"effect",
|
|
"effect_options",
|
|
"duration",
|
|
"advance_after",
|
|
}
|
|
)
|
|
|
|
_UNSET = object()
|
|
|
|
|
|
def transition_unknown_fields(raw: dict[str, Any]) -> list[str]:
|
|
"""Return unsupported fields from a per-slide transition object."""
|
|
return sorted(set(raw) - TRANSITION_OBJECT_FIELDS)
|
|
|
|
|
|
def _set_slide_transition(
|
|
slide_root: ET.Element,
|
|
*,
|
|
effect: str | None,
|
|
duration: float,
|
|
effect_options: dict[str, object] | None = None,
|
|
advance_after: float | None = None,
|
|
) -> bool:
|
|
"""Apply a legacy template-fill transition through the shared core.
|
|
|
|
``None`` and ``keep`` preserve the source transition. ``none`` removes the
|
|
visual transition while retaining an explicitly requested auto-advance.
|
|
Legacy ``advance_after`` allowed both click and timed advance, so it maps to
|
|
``both`` rather than the stricter ``after`` mode. The return value reports
|
|
whether the resulting slide contains an automatic advance.
|
|
"""
|
|
if effect is None or effect == KEEP_TRANSITION:
|
|
enter = EnterUpdate(policy="preserve")
|
|
advance = AdvanceUpdate(
|
|
mode="preserve" if advance_after is None else "both",
|
|
after=advance_after,
|
|
)
|
|
elif effect == "none":
|
|
enter = EnterUpdate(policy="none", effect=None, duration=duration)
|
|
advance = AdvanceUpdate(
|
|
mode="click" if advance_after is None else "both",
|
|
after=advance_after,
|
|
)
|
|
else:
|
|
enter = EnterUpdate(
|
|
policy="replace",
|
|
effect=effect,
|
|
duration=duration,
|
|
effect_options=effect_options,
|
|
)
|
|
advance = AdvanceUpdate(
|
|
mode="click" if advance_after is None else "both",
|
|
after=advance_after,
|
|
)
|
|
|
|
try:
|
|
return apply_slide_motion(
|
|
slide_root,
|
|
enter=enter,
|
|
advance=advance,
|
|
)
|
|
except ValueError as exc:
|
|
raise RuntimeError(str(exc)) from exc
|
|
|
|
|
|
def _resolve_slide_transition(
|
|
item: dict[str, Any],
|
|
*,
|
|
default_effect: str | None,
|
|
default_duration: float,
|
|
) -> tuple[str | None, dict[str, object], float, float | None]:
|
|
"""Pick a slide's transition from its plan entry, falling back to CLI defaults."""
|
|
raw = item.get("transition", _UNSET)
|
|
if raw is _UNSET:
|
|
if default_effect in (None, "none", KEEP_TRANSITION):
|
|
return default_effect, {}, default_duration, None
|
|
effect, effect_options = normalize_transition_effect_request(
|
|
default_effect,
|
|
allow_none=False,
|
|
)
|
|
return effect, effect_options, default_duration, None
|
|
if isinstance(raw, dict):
|
|
unknown = transition_unknown_fields(raw)
|
|
if unknown:
|
|
raise RuntimeError(
|
|
"Transition has unknown field(s): " + ", ".join(unknown)
|
|
)
|
|
effect = raw.get("effect", default_effect)
|
|
raw_options = raw.get("effect_options")
|
|
if raw_options is not None and "effect" not in raw:
|
|
raise RuntimeError(
|
|
"Transition effect_options requires an explicit effect"
|
|
)
|
|
duration = raw.get("duration", default_duration)
|
|
advance_after = raw.get("advance_after")
|
|
else:
|
|
effect = None if raw is None else str(raw)
|
|
raw_options = None
|
|
duration = default_duration
|
|
advance_after = None
|
|
effect_options: dict[str, object] = {}
|
|
if effect is not None and effect not in ("none", KEEP_TRANSITION):
|
|
try:
|
|
effect, effect_options = normalize_transition_effect_request(
|
|
effect,
|
|
raw_options,
|
|
allow_none=False,
|
|
)
|
|
except ValueError as exc:
|
|
raise RuntimeError(str(exc)) from exc
|
|
elif raw_options not in (None, {}):
|
|
raise RuntimeError(
|
|
"Transition effect_options requires an explicit native effect"
|
|
)
|
|
try:
|
|
resolved_duration = validate_seconds(
|
|
duration,
|
|
"transition duration",
|
|
allow_zero=False,
|
|
)
|
|
except ValueError as exc:
|
|
raise RuntimeError(str(exc)) from exc
|
|
return effect, effect_options, resolved_duration, advance_after
|