| 1 | # Advanced Image and Motion Maintenance Smoke |
| 2 | |
| 3 | Run this manual smoke from the repository root after changing nested image |
| 4 | crop finalization, native picture export, deterministic Morph pairing, authored |
| 5 | PowerPoint presets, or their planning contracts. It builds one temporary |
| 6 | two-slide project under the gitignored `projects/_smoke_*` namespace and follows |
| 7 | the inline-smoke convention from |
| 8 | [`code-style.md`](../../../../docs/rules/code-style.md) §11; do not turn it into |
| 9 | a test file or public example deck. |
| 10 | |
| 11 | The fixture deliberately closes the full planning and execution chain: |
| 12 | |
| 13 | - `design_spec.md` carries `Motion suggestion`, `Native shape suggestion`, one |
| 14 | current §VIII image row, and `Crop Policy`; |
| 15 | - `spec_lock.md` projects that row with optional layout pattern `#M1-11`; |
| 16 | - both pages reuse one raster through ordinary, ellipse-preset, and custom-path |
| 17 | independent nested crops; |
| 18 | - `animations.json` pairs the main crop across adjacent Morph pages; |
| 19 | - one helper-authored `rightArrow` verifies native preset discovery and export. |
| 20 | |
| 21 | ```bash |
| 22 | python3 - <<'PY' |
| 23 | import base64 |
| 24 | import io |
| 25 | import json |
| 26 | import re |
| 27 | import subprocess |
| 28 | import sys |
| 29 | import tempfile |
| 30 | import zipfile |
| 31 | from pathlib import Path |
| 32 | from xml.etree import ElementTree as ET |
| 33 | |
| 34 | from PIL import Image, ImageDraw |
| 35 | |
| 36 | project = Path( |
| 37 | tempfile.mkdtemp( |
| 38 | prefix="_smoke_advanced_image_motion_", |
| 39 | dir="projects", |
| 40 | ) |
| 41 | ) |
| 42 | scripts = Path("skills/ppt-master/scripts") |
| 43 | images = project / "images" |
| 44 | svg_output = project / "svg_output" |
| 45 | images.mkdir() |
| 46 | svg_output.mkdir() |
| 47 | (project / "README.md").write_text( |
| 48 | "# Advanced image and motion maintenance smoke\n", |
| 49 | encoding="utf-8", |
| 50 | ) |
| 51 | |
| 52 | |
| 53 | def run_tool(script, *args): |
| 54 | result = subprocess.run( |
| 55 | [sys.executable, str(scripts / script), *map(str, args)], |
| 56 | capture_output=True, |
| 57 | text=True, |
| 58 | ) |
| 59 | assert result.returncode == 0, result.stderr or result.stdout |
| 60 | return result.stdout.strip() |
| 61 | |
| 62 | |
| 63 | scene = Image.new("RGB", (1280, 720), "#E2E8F0") |
| 64 | draw = ImageDraw.Draw(scene) |
| 65 | draw.rectangle((0, 0, 420, 720), fill="#2563EB") |
| 66 | draw.rectangle((420, 0, 860, 720), fill="#F97316") |
| 67 | draw.rectangle((860, 0, 1280, 720), fill="#0F172A") |
| 68 | draw.ellipse((460, 150, 820, 510), fill="#F8FAFC") |
| 69 | scene.save(images / "scene.png") |
| 70 | |
| 71 | preset = run_tool( |
| 72 | "preset_shape_svg.py", |
| 73 | "render", |
| 74 | "rightArrow", |
| 75 | "--id", |
| 76 | "native-arrow", |
| 77 | "--frame", |
| 78 | "500", |
| 79 | "570", |
| 80 | "280", |
| 81 | "80", |
| 82 | "--fill", |
| 83 | "#2563EB", |
| 84 | "--stroke", |
| 85 | "none", |
| 86 | ) |
| 87 | preset = ( |
| 88 | '<g id="native-arrow-module" data-pptx-bounds="500 570 280 80">' |
| 89 | + preset |
| 90 | + "</g>" |
| 91 | ) |
| 92 | |
| 93 | (project / "design_spec.md").write_text( |
| 94 | """<!-- ppt-master-schema: design-spec/v1 --> |
| 95 | # Advanced Image and Motion Smoke - Design Spec |
| 96 | |
| 97 | ## I. Project Information |
| 98 | |
| 99 | | Item | Value | |
| 100 | | --- | --- | |
| 101 | | Project Name | advanced-image-motion-smoke | |
| 102 | | Canvas Format | PPT 16:9 | |
| 103 | | Page Count | 2 | |
| 104 | |
| 105 | ## II. Canvas Specification |
| 106 | |
| 107 | - ViewBox: `0 0 1280 720` |
| 108 | - Format: `ppt169` |
| 109 | |
| 110 | ## III. Visual Theme |
| 111 | |
| 112 | - Direction: restrained technical maintenance fixture |
| 113 | - Colors: blue, orange, slate, and white |
| 114 | |
| 115 | ## IV. Typography System |
| 116 | |
| 117 | - Title: Arial, sans-serif at 36px |
| 118 | - Body: Arial, sans-serif at 20px |
| 119 | |
| 120 | ## V. Layout Principles |
| 121 | |
| 122 | - Reuse one source image through independently editable crop objects. |
| 123 | - Keep every visible crop inside the slide canvas. |
| 124 | |
| 125 | ## VI. Icon Usage Specification |
| 126 | |
| 127 | - No icons. |
| 128 | |
| 129 | ## VIII. Image Resource List |
| 130 | |
| 131 | | Filename | Dimensions | Ratio | Purpose | Type | Layout pattern | Crop Policy | Acquire Via | Status | Reference | text_policy | page_role | |
| 132 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | |
| 133 | | 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 | |
| 134 | |
| 135 | ## IX. Content Outline |
| 136 | |
| 137 | ### Part 1: Crop continuity |
| 138 | |
| 139 | #### Slide 01 - Overview |
| 140 | |
| 141 | - **Audience move**: See separate source views as one editable image system. |
| 142 | - **Layout**: Ordinary crop, shaped crop, and one native directional preset. |
| 143 | - **Title**: Overview crop state |
| 144 | - **Core message**: One source can support independent native picture objects. |
| 145 | - **Content**: Show the wide crop and shaped detail together. |
| 146 | - **Images**: Use `scene.png` for both visible crop objects. |
| 147 | - **Native shape suggestion**: Use the PowerPoint `rightArrow` preset to indicate continuation. |
| 148 | - **Motion suggestion**: Continue the primary crop into Slide 02 as one deterministic Morph object. |
| 149 | |
| 150 | #### Slide 02 - Detail |
| 151 | |
| 152 | - **Audience move**: Recognize the same image after a controlled crop and position change. |
| 153 | - **Layout**: Enlarged primary crop plus a second shaped detail. |
| 154 | - **Title**: Detail crop state |
| 155 | - **Core message**: Morph identity is independent from the picture crop geometry. |
| 156 | - **Content**: Move and zoom the primary crop while preserving the shared source. |
| 157 | - **Images**: Reuse `scene.png`; do not generate or replace the source. |
| 158 | - **Motion suggestion**: Pair the primary crop with Slide 01 and use Morph by object. |
| 159 | |
| 160 | ## X. Speaker Notes Requirements |
| 161 | |
| 162 | - Notes are disabled for this maintenance smoke. |
| 163 | """, |
| 164 | encoding="utf-8", |
| 165 | ) |
| 166 | |
| 167 | (project / "spec_lock.md").write_text( |
| 168 | """<!-- ppt-master-schema: spec-lock/v1 --> |
| 169 | # Execution Lock |
| 170 | |
| 171 | ## canvas |
| 172 | - viewBox: 0 0 1280 720 |
| 173 | - format: ppt169 |
| 174 | ## communication |
| 175 | - audience: PPT Master maintainers |
| 176 | - objective: Verify advanced image and motion contracts end to end. |
| 177 | - core_message: One raster remains editable across native crops and Morph. |
| 178 | ## mode |
| 179 | - mode: showcase |
| 180 | ## visual_style |
| 181 | - visual_style: swiss-minimal |
| 182 | ## colors |
| 183 | - bg: #FFFFFF |
| 184 | - primary: #2563EB |
| 185 | - accent: #F97316 |
| 186 | - text: #0F172A |
| 187 | ## typography |
| 188 | - font_family: Arial, sans-serif |
| 189 | - title_family: Arial, sans-serif |
| 190 | - body_family: Arial, sans-serif |
| 191 | - title: 36 |
| 192 | - body: 20 |
| 193 | ## icons |
| 194 | - library: none |
| 195 | - inventory: none |
| 196 | ## images |
| 197 | - scene: images/scene.png | source=user | pattern=#M1-11 same-source independent crops with a shaped detail | crop=adaptive |
| 198 | ## page_rhythm |
| 199 | - P01: dense |
| 200 | - P02: dense |
| 201 | ## pptx_structure |
| 202 | - mode: flat |
| 203 | ## forbidden |
| 204 | - Unsupported SVG constructs |
| 205 | """, |
| 206 | encoding="utf-8", |
| 207 | ) |
| 208 | |
| 209 | slide_1 = f"""<svg xmlns="http://www.w3.org/2000/svg" |
| 210 | viewBox="0 0 1280 720" data-pptx-page-role="content"> |
| 211 | <defs> |
| 212 | <clipPath id="overview-shape" clipPathUnits="userSpaceOnUse"> |
| 213 | <ellipse cx="0.725" cy="0.5" rx="0.275" ry="0.5"/> |
| 214 | </clipPath> |
| 215 | </defs> |
| 216 | <rect id="background" data-pptx-role="background" |
| 217 | x="0" y="0" width="1280" height="720" fill="#FFFFFF"/> |
| 218 | <g id="hero-overview" data-pptx-bounds="80 140 448 360"> |
| 219 | <svg id="overview-crop" x="80" y="140" width="448" height="360" |
| 220 | viewBox="0 0 0.7 1" preserveAspectRatio="none" overflow="hidden"> |
| 221 | <image href="../images/scene.png" x="0" y="0" width="1" height="1" |
| 222 | preserveAspectRatio="none"/> |
| 223 | </svg> |
| 224 | </g> |
| 225 | <g id="shaped-overview" data-pptx-bounds="760 170 330 340"> |
| 226 | <svg id="overview-shaped-crop" data-pptx-crop="1" |
| 227 | x="760" y="170" width="330" height="340" |
| 228 | viewBox="0.45 0 0.55 1" preserveAspectRatio="none" overflow="hidden"> |
| 229 | <image href="../images/scene.png" x="0" y="0" width="1" height="1" |
| 230 | preserveAspectRatio="none" clip-path="url(#overview-shape)"/> |
| 231 | </svg> |
| 232 | </g> |
| 233 | {preset} |
| 234 | </svg> |
| 235 | """ |
| 236 | |
| 237 | slide_2 = """<svg xmlns="http://www.w3.org/2000/svg" |
| 238 | viewBox="0 0 1280 720" data-pptx-page-role="content"> |
| 239 | <defs> |
| 240 | <clipPath id="detail-shape" clipPathUnits="userSpaceOnUse"> |
| 241 | <path d="M 0.5 1 L 0.75 0 L 1 1 Z"/> |
| 242 | </clipPath> |
| 243 | </defs> |
| 244 | <rect id="background" data-pptx-role="background" |
| 245 | x="0" y="0" width="1280" height="720" fill="#FFFFFF"/> |
| 246 | <g id="hero-detail" data-pptx-bounds="0 0 1280 720"> |
| 247 | <svg id="detail-crop" x="0" y="0" width="1280" height="720" |
| 248 | viewBox="0.15 0.1 0.36 0.36" |
| 249 | preserveAspectRatio="none" overflow="hidden"> |
| 250 | <image href="../images/scene.png" x="0" y="0" width="1" height="1" |
| 251 | preserveAspectRatio="none"/> |
| 252 | </svg> |
| 253 | </g> |
| 254 | <g id="shaped-detail" data-pptx-bounds="820 160 320 360"> |
| 255 | <svg id="detail-shaped-crop" data-pptx-crop="1" |
| 256 | x="820" y="160" width="320" height="360" |
| 257 | viewBox="0.5 0 0.5 1" preserveAspectRatio="none" overflow="hidden"> |
| 258 | <image href="../images/scene.png" x="0" y="0" width="1" height="1" |
| 259 | preserveAspectRatio="none" clip-path="url(#detail-shape)"/> |
| 260 | </svg> |
| 261 | </g> |
| 262 | </svg> |
| 263 | """ |
| 264 | |
| 265 | (svg_output / "01_overview.svg").write_text(slide_1, encoding="utf-8") |
| 266 | (svg_output / "02_detail.svg").write_text(slide_2, encoding="utf-8") |
| 267 | (project / "animations.json").write_text( |
| 268 | json.dumps( |
| 269 | { |
| 270 | "version": 1, |
| 271 | "slides": { |
| 272 | "02_detail": { |
| 273 | "transition": { |
| 274 | "effect": "morph", |
| 275 | "effect_options": {"morph_by": "object"}, |
| 276 | "duration": 0.8, |
| 277 | }, |
| 278 | "morph": { |
| 279 | "from": "01_overview", |
| 280 | "pairs": { |
| 281 | "hero-image": { |
| 282 | "from": "hero-overview", |
| 283 | "to": "hero-detail", |
| 284 | } |
| 285 | }, |
| 286 | }, |
| 287 | } |
| 288 | }, |
| 289 | }, |
| 290 | ensure_ascii=False, |
| 291 | indent=2, |
| 292 | ) |
| 293 | + "\n", |
| 294 | encoding="utf-8", |
| 295 | ) |
| 296 | |
| 297 | run_tool("project_manager.py", "validate", project) |
| 298 | run_tool("svg_quality_checker.py", project, "--format", "ppt169") |
| 299 | run_tool("animation_config.py", "validate", project) |
| 300 | |
| 301 | |
| 302 | def finalized_image_sizes(): |
| 303 | sizes = [] |
| 304 | for svg_path in sorted((project / "svg_final").glob("*.svg")): |
| 305 | root = ET.parse(svg_path).getroot() |
| 306 | for image in root.iter("{http://www.w3.org/2000/svg}image"): |
| 307 | href = image.get("href") or image.get( |
| 308 | "{http://www.w3.org/1999/xlink}href" |
| 309 | ) |
| 310 | assert href and href.startswith("data:image/"), href |
| 311 | payload = base64.b64decode(href.split(",", 1)[1], validate=True) |
| 312 | with Image.open(io.BytesIO(payload)) as embedded: |
| 313 | sizes.append(embedded.size) |
| 314 | return sizes |
| 315 | |
| 316 | |
| 317 | finalized_sizes = [] |
| 318 | for finalize_args in ((), ("--no-compress",)): |
| 319 | run_tool("finalize_svg.py", project, *finalize_args) |
| 320 | embedded_sizes = finalized_image_sizes() |
| 321 | assert len(embedded_sizes) == 4, embedded_sizes |
| 322 | assert all(width > 100 and height > 100 for width, height in embedded_sizes) |
| 323 | finalized_sizes.append(embedded_sizes) |
| 324 | |
| 325 | pptx = project / "advanced-image-motion.pptx" |
| 326 | run_tool("svg_to_pptx.py", project, "--no-notes", "-o", pptx) |
| 327 | with zipfile.ZipFile(pptx) as archive: |
| 328 | slide_names = sorted( |
| 329 | name |
| 330 | for name in archive.namelist() |
| 331 | if re.fullmatch(r"ppt/slides/slide[12]\.xml", name) |
| 332 | ) |
| 333 | slide_xml = [archive.read(name) for name in slide_names] |
| 334 | assert len(slide_xml) == 2, slide_names |
| 335 | assert all(b'!!hero-image' in xml for xml in slide_xml) |
| 336 | assert b'morph' in slide_xml[1] |
| 337 | assert sum(xml.count(b"<a:srcRect") for xml in slide_xml) >= 4 |
| 338 | assert b'<a:prstGeom prst="ellipse">' in slide_xml[0] |
| 339 | assert b'<a:prstGeom prst="rightArrow">' in slide_xml[0] |
| 340 | assert b"<a:custGeom>" in slide_xml[1] |
| 341 | |
| 342 | readback = project / "readback" |
| 343 | run_tool( |
| 344 | "pptx_to_svg.py", |
| 345 | pptx, |
| 346 | "-o", |
| 347 | readback, |
| 348 | "--inheritance-mode", |
| 349 | "flat", |
| 350 | "--strict", |
| 351 | ) |
| 352 | readback_svg = "\n".join( |
| 353 | path.read_text(encoding="utf-8") |
| 354 | for path in sorted((readback / "svg").glob("slide_*.svg")) |
| 355 | ) |
| 356 | assert readback_svg.count('data-pptx-crop="1"') >= 2 |
| 357 | assert readback_svg.count('data-pptx-shape-name="!!hero-image"') == 2 |
| 358 | |
| 359 | print( |
| 360 | "Advanced image and motion smoke: passed " |
| 361 | f"(embedded sizes={finalized_sizes}; project={project})" |
| 362 | ) |
| 363 | PY |
| 364 | ``` |
| 365 | |
| 366 | Every command must exit successfully. The finalized data URIs must retain |
| 367 | useful source pixels rather than collapsing to the nested child’s unit |
| 368 | coordinates; the package must contain four native picture crops, the authored |
| 369 | `rightArrow`, one Morph transition, and exactly one `!!hero-image` target on |
| 370 | each adjacent slide. The strict PPTX-to-SVG readback must restore both shaped |
| 371 | crop markers and both forced-Morph object names. |
| 372 | |
| 373 | The printed project is intentionally retained for manual Microsoft PowerPoint |
| 374 | inspection. Morph must continue the primary picture without a visible |
| 375 | cross-fade, sharpness jump, or media re-decoding flash. That Office-only visual |
| 376 | check is evidence gathering; to stress differing media payloads, replace |
| 377 | `scene.png` with a `7680×4320` source and re-export. Do not trigger media |
| 378 | deduplication changes from package structure alone. |
| 379 |