379 lines
12 KiB
Markdown
379 lines
12 KiB
Markdown
# 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.
|