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