# Advanced Image and Motion Maintenance Smoke

Run this manual smoke from the repository root after changing nested image
crop finalization, native picture export, deterministic Morph pairing, authored
PowerPoint presets, or their planning contracts. It builds one temporary
two-slide project under the gitignored `projects/_smoke_*` namespace and follows
the inline-smoke convention from
[`code-style.md`](../../../../docs/rules/code-style.md) §11; do not turn it into
a test file or public example deck.

The fixture deliberately closes the full planning and execution chain:

- `design_spec.md` carries `Motion suggestion`, `Native shape suggestion`, one
  current §VIII image row, and `Crop Policy`;
- `spec_lock.md` projects that row with optional layout pattern `#M1-11`;
- both pages reuse one raster through ordinary, ellipse-preset, and custom-path
  independent nested crops;
- `animations.json` pairs the main crop across adjacent Morph pages;
- one helper-authored `rightArrow` verifies native preset discovery and export.

```bash
python3 - <<'PY'
import base64
import io
import json
import re
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path
from xml.etree import ElementTree as ET

from PIL import Image, ImageDraw

project = Path(
    tempfile.mkdtemp(
        prefix="_smoke_advanced_image_motion_",
        dir="projects",
    )
)
scripts = Path("skills/ppt-master/scripts")
images = project / "images"
svg_output = project / "svg_output"
images.mkdir()
svg_output.mkdir()
(project / "README.md").write_text(
    "# Advanced image and motion maintenance smoke\n",
    encoding="utf-8",
)


def run_tool(script, *args):
    result = subprocess.run(
        [sys.executable, str(scripts / script), *map(str, args)],
        capture_output=True,
        text=True,
    )
    assert result.returncode == 0, result.stderr or result.stdout
    return result.stdout.strip()


scene = Image.new("RGB", (1280, 720), "#E2E8F0")
draw = ImageDraw.Draw(scene)
draw.rectangle((0, 0, 420, 720), fill="#2563EB")
draw.rectangle((420, 0, 860, 720), fill="#F97316")
draw.rectangle((860, 0, 1280, 720), fill="#0F172A")
draw.ellipse((460, 150, 820, 510), fill="#F8FAFC")
scene.save(images / "scene.png")

preset = run_tool(
    "preset_shape_svg.py",
    "render",
    "rightArrow",
    "--id",
    "native-arrow",
    "--frame",
    "500",
    "570",
    "280",
    "80",
    "--fill",
    "#2563EB",
    "--stroke",
    "none",
)
preset = (
    '<g id="native-arrow-module" data-pptx-bounds="500 570 280 80">'
    + preset
    + "</g>"
)

(project / "design_spec.md").write_text(
    """<!-- ppt-master-schema: design-spec/v1 -->
# Advanced Image and Motion Smoke - Design Spec

## I. Project Information

| Item | Value |
| --- | --- |
| Project Name | advanced-image-motion-smoke |
| Canvas Format | PPT 16:9 |
| Page Count | 2 |

## II. Canvas Specification

- ViewBox: `0 0 1280 720`
- Format: `ppt169`

## III. Visual Theme

- Direction: restrained technical maintenance fixture
- Colors: blue, orange, slate, and white

## IV. Typography System

- Title: Arial, sans-serif at 36px
- Body: Arial, sans-serif at 20px

## V. Layout Principles

- Reuse one source image through independently editable crop objects.
- Keep every visible crop inside the slide canvas.

## VI. Icon Usage Specification

- No icons.

## VIII. Image Resource List

| Filename | Dimensions | Ratio | Purpose | Type | Layout pattern | Crop Policy | Acquire Via | Status | Reference | text_policy | page_role |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| scene.png | 1280x720 | 16:9 | Morph crop continuity | Photo | #M1-11 same-source independent crops with a shaped detail | adaptive | user | Existing | Synthetic three-band scene for crop and Morph verification | none | local |

## IX. Content Outline

### Part 1: Crop continuity

#### Slide 01 - Overview

- **Audience move**: See separate source views as one editable image system.
- **Layout**: Ordinary crop, shaped crop, and one native directional preset.
- **Title**: Overview crop state
- **Core message**: One source can support independent native picture objects.
- **Content**: Show the wide crop and shaped detail together.
- **Images**: Use `scene.png` for both visible crop objects.
- **Native shape suggestion**: Use the PowerPoint `rightArrow` preset to indicate continuation.
- **Motion suggestion**: Continue the primary crop into Slide 02 as one deterministic Morph object.

#### Slide 02 - Detail

- **Audience move**: Recognize the same image after a controlled crop and position change.
- **Layout**: Enlarged primary crop plus a second shaped detail.
- **Title**: Detail crop state
- **Core message**: Morph identity is independent from the picture crop geometry.
- **Content**: Move and zoom the primary crop while preserving the shared source.
- **Images**: Reuse `scene.png`; do not generate or replace the source.
- **Motion suggestion**: Pair the primary crop with Slide 01 and use Morph by object.

## X. Speaker Notes Requirements

- Notes are disabled for this maintenance smoke.
""",
    encoding="utf-8",
)

(project / "spec_lock.md").write_text(
    """<!-- ppt-master-schema: spec-lock/v1 -->
# Execution Lock

## canvas
- viewBox: 0 0 1280 720
- format: ppt169
## communication
- audience: PPT Master maintainers
- objective: Verify advanced image and motion contracts end to end.
- core_message: One raster remains editable across native crops and Morph.
## mode
- mode: showcase
## visual_style
- visual_style: swiss-minimal
## colors
- bg: #FFFFFF
- primary: #2563EB
- accent: #F97316
- text: #0F172A
## typography
- font_family: Arial, sans-serif
- title_family: Arial, sans-serif
- body_family: Arial, sans-serif
- title: 36
- body: 20
## icons
- library: none
- inventory: none
## images
- scene: images/scene.png | source=user | pattern=#M1-11 same-source independent crops with a shaped detail | crop=adaptive
## page_rhythm
- P01: dense
- P02: dense
## pptx_structure
- mode: flat
## forbidden
- Unsupported SVG constructs
""",
    encoding="utf-8",
)

slide_1 = f"""<svg xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 1280 720" data-pptx-page-role="content">
  <defs>
    <clipPath id="overview-shape" clipPathUnits="userSpaceOnUse">
      <ellipse cx="0.725" cy="0.5" rx="0.275" ry="0.5"/>
    </clipPath>
  </defs>
  <rect id="background" data-pptx-role="background"
    x="0" y="0" width="1280" height="720" fill="#FFFFFF"/>
  <g id="hero-overview" data-pptx-bounds="80 140 448 360">
    <svg id="overview-crop" x="80" y="140" width="448" height="360"
      viewBox="0 0 0.7 1" preserveAspectRatio="none" overflow="hidden">
      <image href="../images/scene.png" x="0" y="0" width="1" height="1"
        preserveAspectRatio="none"/>
    </svg>
  </g>
  <g id="shaped-overview" data-pptx-bounds="760 170 330 340">
    <svg id="overview-shaped-crop" data-pptx-crop="1"
      x="760" y="170" width="330" height="340"
      viewBox="0.45 0 0.55 1" preserveAspectRatio="none" overflow="hidden">
      <image href="../images/scene.png" x="0" y="0" width="1" height="1"
        preserveAspectRatio="none" clip-path="url(#overview-shape)"/>
    </svg>
  </g>
  {preset}
</svg>
"""

slide_2 = """<svg xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 1280 720" data-pptx-page-role="content">
  <defs>
    <clipPath id="detail-shape" clipPathUnits="userSpaceOnUse">
      <path d="M 0.5 1 L 0.75 0 L 1 1 Z"/>
    </clipPath>
  </defs>
  <rect id="background" data-pptx-role="background"
    x="0" y="0" width="1280" height="720" fill="#FFFFFF"/>
  <g id="hero-detail" data-pptx-bounds="0 0 1280 720">
    <svg id="detail-crop" x="0" y="0" width="1280" height="720"
      viewBox="0.15 0.1 0.36 0.36"
      preserveAspectRatio="none" overflow="hidden">
      <image href="../images/scene.png" x="0" y="0" width="1" height="1"
        preserveAspectRatio="none"/>
    </svg>
  </g>
  <g id="shaped-detail" data-pptx-bounds="820 160 320 360">
    <svg id="detail-shaped-crop" data-pptx-crop="1"
      x="820" y="160" width="320" height="360"
      viewBox="0.5 0 0.5 1" preserveAspectRatio="none" overflow="hidden">
      <image href="../images/scene.png" x="0" y="0" width="1" height="1"
        preserveAspectRatio="none" clip-path="url(#detail-shape)"/>
    </svg>
  </g>
</svg>
"""

(svg_output / "01_overview.svg").write_text(slide_1, encoding="utf-8")
(svg_output / "02_detail.svg").write_text(slide_2, encoding="utf-8")
(project / "animations.json").write_text(
    json.dumps(
        {
            "version": 1,
            "slides": {
                "02_detail": {
                    "transition": {
                        "effect": "morph",
                        "effect_options": {"morph_by": "object"},
                        "duration": 0.8,
                    },
                    "morph": {
                        "from": "01_overview",
                        "pairs": {
                            "hero-image": {
                                "from": "hero-overview",
                                "to": "hero-detail",
                            }
                        },
                    },
                }
            },
        },
        ensure_ascii=False,
        indent=2,
    )
    + "\n",
    encoding="utf-8",
)

run_tool("project_manager.py", "validate", project)
run_tool("svg_quality_checker.py", project, "--format", "ppt169")
run_tool("animation_config.py", "validate", project)


def finalized_image_sizes():
    sizes = []
    for svg_path in sorted((project / "svg_final").glob("*.svg")):
        root = ET.parse(svg_path).getroot()
        for image in root.iter("{http://www.w3.org/2000/svg}image"):
            href = image.get("href") or image.get(
                "{http://www.w3.org/1999/xlink}href"
            )
            assert href and href.startswith("data:image/"), href
            payload = base64.b64decode(href.split(",", 1)[1], validate=True)
            with Image.open(io.BytesIO(payload)) as embedded:
                sizes.append(embedded.size)
    return sizes


finalized_sizes = []
for finalize_args in ((), ("--no-compress",)):
    run_tool("finalize_svg.py", project, *finalize_args)
    embedded_sizes = finalized_image_sizes()
    assert len(embedded_sizes) == 4, embedded_sizes
    assert all(width > 100 and height > 100 for width, height in embedded_sizes)
    finalized_sizes.append(embedded_sizes)

pptx = project / "advanced-image-motion.pptx"
run_tool("svg_to_pptx.py", project, "--no-notes", "-o", pptx)
with zipfile.ZipFile(pptx) as archive:
    slide_names = sorted(
        name
        for name in archive.namelist()
        if re.fullmatch(r"ppt/slides/slide[12]\.xml", name)
    )
    slide_xml = [archive.read(name) for name in slide_names]
assert len(slide_xml) == 2, slide_names
assert all(b'!!hero-image' in xml for xml in slide_xml)
assert b'morph' in slide_xml[1]
assert sum(xml.count(b"<a:srcRect") for xml in slide_xml) >= 4
assert b'<a:prstGeom prst="ellipse">' in slide_xml[0]
assert b'<a:prstGeom prst="rightArrow">' in slide_xml[0]
assert b"<a:custGeom>" in slide_xml[1]

readback = project / "readback"
run_tool(
    "pptx_to_svg.py",
    pptx,
    "-o",
    readback,
    "--inheritance-mode",
    "flat",
    "--strict",
)
readback_svg = "\n".join(
    path.read_text(encoding="utf-8")
    for path in sorted((readback / "svg").glob("slide_*.svg"))
)
assert readback_svg.count('data-pptx-crop="1"') >= 2
assert readback_svg.count('data-pptx-shape-name="!!hero-image"') == 2

print(
    "Advanced image and motion smoke: passed "
    f"(embedded sizes={finalized_sizes}; project={project})"
)
PY
```

Every command must exit successfully. The finalized data URIs must retain
useful source pixels rather than collapsing to the nested child’s unit
coordinates; the package must contain four native picture crops, the authored
`rightArrow`, one Morph transition, and exactly one `!!hero-image` target on
each adjacent slide. The strict PPTX-to-SVG readback must restore both shaped
crop markers and both forced-Morph object names.

The printed project is intentionally retained for manual Microsoft PowerPoint
inspection. Morph must continue the primary picture without a visible
cross-fade, sharpness jump, or media re-decoding flash. That Office-only visual
check is evidence gathering; to stress differing media payloads, replace
`scene.png` with a `7680×4320` source and re-export. Do not trigger media
deduplication changes from package structure alone.
