pptx/animation-effect--zoom-out

Attach a <p:timing> tree to a slide with a single animation effect bound to a shape. The manifest expands to one case per effect kind (fade-in, fade-out, fly-in-left, fly-in-right, zoom-in, zoom-out). python-pptx exposes no high-level animation API; the generator builds the timing tree directly via lxml. Each case encodes the effect as an <p:par>/<p:cTn>/.../<p:par> chain whose leaf carries a preset animation attribute combination (presetID, presetClass, presetSubtype). Authoring-side only: the corpus records what was written, not playback.

Library verdicts: python-pptx: — pptxjs: —

Metadata

Feature idpptx/animation-effect--zoom-out
Formatpptx
Categoryanimation
Familypptx/animation-effect
Axis valueseffect=zoom-out
Spececma-376-5-part-1 § 19.5.70 p:timing

XPath assertions

ID / partPredicateXPathpython-pptxpptxjs
timing-present-zoom-out
ppt/slides/slide1.xml
exist
The slide must carry a <p:timing> element holding the animation tree.
//p:timing
preset-id-zoom-out
ppt/slides/slide1.xml
equal = 23
The innermost animation <p:cTn> must carry presetID='23' identifying the effect kind (10=fade, 2=fly, 23=zoom).
//p:timing//p:cTn[@presetID and @presetClass]/@presetID
preset-class-zoom-out
ppt/slides/slide1.xml
equal = exit
The innermost animation <p:cTn> must carry presetClass='exit' (entr for enter, exit for exit).
//p:timing//p:cTn[@presetID and @presetClass]/@presetClass
preset-subtype-zoom-out
ppt/slides/slide1.xml
equal = 0
The innermost animation <p:cTn> must carry presetSubtype='0' (direction/variant code; 0 for plain fade/zoom, 4=left, 8=right for fly-in).
//p:timing//p:cTn[@presetID and @presetClass]/@presetSubtype

Render assertions

No render assertions declared.

Generator source

scripts/gen_animation_effect.py

#!/usr/bin/env python3
"""Generate a single ``fixtures/pptx/animation-effect--<id>.pptx``.

Parameterised generator: the ``features/pptx/animation-effect.json``
manifest expands to one case per effect kind (fade-in, fade-out,
fly-in-left, fly-in-right, zoom-in, zoom-out), and the conformance
runner invokes this script once per case with ``--effect <id> --out
<path>``.

Each invocation builds a one-slide presentation with a single shape,
then appends a <p:timing> tree to the slide element carrying a single
animation effect keyed to the shape by its shape-id. python-pptx
exposes no high-level animation API, so the timing tree is written
directly via lxml. The emitted structure follows the PowerPoint-
authored minimal envelope:

  <p:timing>
    <p:tnLst>
      <p:par>
        <p:cTn id="1" dur="indefinite" restart="never" nodeType="tmRoot">
          <p:childTnLst>
            <p:seq concurrent="1" nextAc="seek">
              <p:cTn id="2" dur="indefinite" nodeType="mainSeq">
                <p:childTnLst>
                  <p:par>
                    <p:cTn id="3" fill="hold">
                      <p:stCondLst>...</p:stCondLst>
                      <p:childTnLst>
                        <p:par>
                          <p:cTn id="4" fill="hold">
                            <p:stCondLst>...</p:stCondLst>
                            <p:childTnLst>
                              <p:par>
                                <p:cTn id="5" presetID="..."
                                       presetClass="..."
                                       presetSubtype="..."
                                       fill="hold" nodeType="clickEffect">
                                  <p:childTnLst>
                                    <p:set>...</p:set>
                                  </p:childTnLst>
                                </p:cTn>
                              </p:par>
                            </p:childTnLst>
                          </p:cTn>
                        </p:par>
                      </p:childTnLst>
                    </p:cTn>
                  </p:par>
                </p:childTnLst>
              </p:cTn>
            </p:seq>
          </p:childTnLst>
        </p:cTn>
      </p:par>
    </p:tnLst>
  </p:timing>

The assertions only look at the innermost cTn's presetID /
presetClass / presetSubtype attributes, so the exact envelope detail
is not load-bearing beyond "exists and parses".
"""

from __future__ import annotations

import argparse
import datetime as _dt
from pathlib import Path

from lxml import etree
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE
from pptx.oxml.ns import qn
from pptx.util import Inches

_REPO_ROOT = Path(__file__).resolve().parent.parent
_REPRODUCIBLE_DT = _dt.datetime(2000, 1, 1, 0, 0, 0)

# Effect-table: id -> (presetID, presetClass, presetSubtype).
# Matches the values enumerated in the manifest parameter list.
_EFFECTS: dict[str, tuple[str, str, str]] = {
    "fade-in":      ("10", "entr", "0"),
    "fade-out":     ("10", "exit", "0"),
    "fly-in-left":  ("2",  "entr", "4"),
    "fly-in-right": ("2",  "entr", "8"),
    "zoom-in":      ("23", "entr", "0"),
    "zoom-out":     ("23", "exit", "0"),
}


def _build_timing(shape_id: int, preset_id: str, preset_class: str, preset_subtype: str) -> etree._Element:
    """Build the <p:timing> subtree encoding the requested effect.

    The inner <p:cTn id='5'> element is the one carrying the three
    preset-attribute triple asserted by the manifest.
    """
    timing = etree.Element(qn("p:timing"))
    tnLst = etree.SubElement(timing, qn("p:tnLst"))

    par_root = etree.SubElement(tnLst, qn("p:par"))
    cTn_root = etree.SubElement(par_root, qn("p:cTn"))
    cTn_root.set("id", "1")
    cTn_root.set("dur", "indefinite")
    cTn_root.set("restart", "never")
    cTn_root.set("nodeType", "tmRoot")

    child_root = etree.SubElement(cTn_root, qn("p:childTnLst"))
    seq = etree.SubElement(child_root, qn("p:seq"))
    seq.set("concurrent", "1")
    seq.set("nextAc", "seek")

    cTn_seq = etree.SubElement(seq, qn("p:cTn"))
    cTn_seq.set("id", "2")
    cTn_seq.set("dur", "indefinite")
    cTn_seq.set("nodeType", "mainSeq")
    child_seq = etree.SubElement(cTn_seq, qn("p:childTnLst"))

    par_click = etree.SubElement(child_seq, qn("p:par"))
    cTn_click = etree.SubElement(par_click, qn("p:cTn"))
    cTn_click.set("id", "3")
    cTn_click.set("fill", "hold")
    stcl_click = etree.SubElement(cTn_click, qn("p:stCondLst"))
    cond_click = etree.SubElement(stcl_click, qn("p:cond"))
    cond_click.set("delay", "indefinite")
    child_click = etree.SubElement(cTn_click, qn("p:childTnLst"))

    par_group = etree.SubElement(child_click, qn("p:par"))
    cTn_group = etree.SubElement(par_group, qn("p:cTn"))
    cTn_group.set("id", "4")
    cTn_group.set("fill", "hold")
    stcl_group = etree.SubElement(cTn_group, qn("p:stCondLst"))
    cond_group = etree.SubElement(stcl_group, qn("p:cond"))
    cond_group.set("delay", "0")
    child_group = etree.SubElement(cTn_group, qn("p:childTnLst"))

    par_effect = etree.SubElement(child_group, qn("p:par"))
    cTn_effect = etree.SubElement(par_effect, qn("p:cTn"))
    cTn_effect.set("id", "5")
    cTn_effect.set("presetID", preset_id)
    cTn_effect.set("presetClass", preset_class)
    cTn_effect.set("presetSubtype", preset_subtype)
    cTn_effect.set("fill", "hold")
    cTn_effect.set("nodeType", "clickEffect")
    child_effect = etree.SubElement(cTn_effect, qn("p:childTnLst"))

    set_el = etree.SubElement(child_effect, qn("p:set"))
    cBhvr = etree.SubElement(set_el, qn("p:cBhvr"))
    cTn_set = etree.SubElement(cBhvr, qn("p:cTn"))
    cTn_set.set("id", "6")
    cTn_set.set("dur", "1")
    cTn_set.set("fill", "hold")
    tgtEl = etree.SubElement(cBhvr, qn("p:tgtEl"))
    spTgt = etree.SubElement(tgtEl, qn("p:spTgt"))
    spTgt.set("spid", str(shape_id))

    return timing


def _write(effect_id: str, out: Path) -> None:
    preset_id, preset_class, preset_subtype = _EFFECTS[effect_id]

    prs = Presentation()
    # "Title Only" layout; the shape we animate is added below.
    slide = prs.slides.add_slide(prs.slide_layouts[5])
    shape = slide.shapes.add_shape(
        MSO_SHAPE.RECTANGLE, Inches(2), Inches(2), Inches(3), Inches(2)
    )
    # shape.shape_id is the p:cNvPr/@id the timing tree references.
    shape_id = shape.shape_id

    timing = _build_timing(shape_id, preset_id, preset_class, preset_subtype)
    # <p:timing> is a direct child of <p:sld>, positioned after
    # <p:cSld>/<p:clrMapOvr> and before any <p:extLst>. Appending at
    # the end is schema-valid when <p:extLst> is absent (the default).
    slide._element.append(timing)

    out.parent.mkdir(parents=True, exist_ok=True)
    prs.save(out, zip_date_time=_REPRODUCIBLE_DT)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--effect",
        required=True,
        choices=sorted(_EFFECTS.keys()),
        help="Animation effect id (fade-in, fade-out, fly-in-left, fly-in-right, zoom-in, zoom-out).",
    )
    parser.add_argument(
        "--out",
        required=True,
        help="Output fixture path (absolute or repo-relative).",
    )
    args = parser.parse_args()

    out_path = Path(args.out)
    if not out_path.is_absolute():
        out_path = _REPO_ROOT / out_path
    _write(args.effect, out_path)
    print(out_path)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Manifest (expanded)

{
  "$schema": "../manifest.schema.json",
  "id": "pptx/animation-effect--zoom-out",
  "kind": "literal",
  "title": "Animation effect",
  "format": "pptx",
  "category": "animation",
  "roles": [
    "authoring"
  ],
  "summary": "Attach a <p:timing> tree to a slide with a single animation effect bound to a shape. The manifest expands to one case per effect kind (fade-in, fade-out, fly-in-left, fly-in-right, zoom-in, zoom-out). python-pptx exposes no high-level animation API; the generator builds the timing tree directly via lxml. Each case encodes the effect as an <p:par>/<p:cTn>/.../<p:par> chain whose leaf carries a preset animation attribute combination (presetID, presetClass, presetSubtype). Authoring-side only: the corpus records what was written, not playback.",
  "spec": {
    "source": "ecma-376-5-part-1",
    "clause": "19.5.70",
    "element": "p:timing",
    "rnc_reference": "spec/ecma-376-5/part-1/rnc/pml.rnc",
    "xsd_reference": "spec/ecma-376-5/part-1/xsd/pml.xsd",
    "notes": "<p:timing> carries the slide's animation tree: a hierarchy of <p:par> (parallel) and <p:seq> (sequence) time nodes, each with a <p:cTn> (common timing) descriptor. Effects are identified by presetID / presetClass / presetSubtype attributes on <p:cTn> of the innermost <p:par>. The six effects asserted here are the best-known PowerPoint defaults: fade (presetID=10, class=entr/exit), fly (presetID=2 with presetSubtype for direction), zoom (presetID=23, class=entr/exit). Since python-pptx has no high-level animation API, the XML is appended directly — the assertions check for the presence of <p:timing>//p:par with a <p:cTn> carrying the expected presetID + presetClass + presetSubtype attributes."
  },
  "fixtures": {
    "machine": "pptx/animation-effect--zoom-out"
  },
  "generator": {
    "python": "scripts/gen_animation_effect.py",
    "arg_template": "--effect {effect.id} --out fixtures/pptx/animation-effect--{effect.id}.pptx"
  },
  "_expansion": {
    "parent_id": "pptx/animation-effect",
    "bindings": {
      "effect": "zoom-out"
    }
  },
  "assertions": [
    {
      "id": "timing-present-zoom-out",
      "part": "ppt/slides/slide1.xml",
      "namespaces": {
        "p": "http://schemas.openxmlformats.org/presentationml/2006/main"
      },
      "xpath": "//p:timing",
      "must": "exist",
      "description": "The slide must carry a <p:timing> element holding the animation tree."
    },
    {
      "id": "preset-id-zoom-out",
      "part": "ppt/slides/slide1.xml",
      "namespaces": {
        "p": "http://schemas.openxmlformats.org/presentationml/2006/main"
      },
      "xpath": "//p:timing//p:cTn[@presetID and @presetClass]/@presetID",
      "must": "equal",
      "value": "23",
      "description": "The innermost animation <p:cTn> must carry presetID='23' identifying the effect kind (10=fade, 2=fly, 23=zoom)."
    },
    {
      "id": "preset-class-zoom-out",
      "part": "ppt/slides/slide1.xml",
      "namespaces": {
        "p": "http://schemas.openxmlformats.org/presentationml/2006/main"
      },
      "xpath": "//p:timing//p:cTn[@presetID and @presetClass]/@presetClass",
      "must": "equal",
      "value": "exit",
      "description": "The innermost animation <p:cTn> must carry presetClass='exit' (entr for enter, exit for exit)."
    },
    {
      "id": "preset-subtype-zoom-out",
      "part": "ppt/slides/slide1.xml",
      "namespaces": {
        "p": "http://schemas.openxmlformats.org/presentationml/2006/main"
      },
      "xpath": "//p:timing//p:cTn[@presetID and @presetClass]/@presetSubtype",
      "must": "equal",
      "value": "0",
      "description": "The innermost animation <p:cTn> must carry presetSubtype='0' (direction/variant code; 0 for plain fade/zoom, 4=left, 8=right for fly-in)."
    }
  ]
}

Fixture

Download animation-effect--zoom-out.pptx (27.8 KB)

Reference preview

Reference (machine, page 1 PNG)

pptx/animation-effect--zoom-out page 1 reference

Reference PDF


Download PDF Open in PDF.js

Spec notes

<p:timing> carries the slide's animation tree: a hierarchy of <p:par> (parallel) and <p:seq> (sequence) time nodes, each with a <p:cTn> (common timing) descriptor. Effects are identified by presetID / presetClass / presetSubtype attributes on <p:cTn> of the innermost <p:par>. The six effects asserted here are the best-known PowerPoint defaults: fade (presetID=10, class=entr/exit), fly (presetID=2 with presetSubtype for direction), zoom (presetID=23, class=entr/exit). Since python-pptx has no high-level animation API, the XML is appended directly — the assertions check for the presence of <p:timing>//p:par with a <p:cTn> carrying the expected presetID + presetClass + presetSubtype attributes.