返回 ppt-master
svg-pipeline.md
根目录 / skills / ppt-master / scripts / docs / svg-pipeline.md
1 # SVG Pipeline Tools
2
3 > **Maintenance boundary**: post-processing modules serve both the on-disk
4 > `svg_final/` preview and in-memory native PPTX conversion. Check both
5 > consumers before changing or removing a step.
6
7 These tools cover post-processing, SVG validation, speaker notes, recorded narration, and PPTX export.
8
9 The supported delivery contract has one PPTX path: `svg_output/` → the project SVG-to-DrawingML converter → native PPTX. The mandatory `finalize_svg.py` step separately creates self-contained `svg_final/` visual previews, which may be opened directly or inserted into PowerPoint as SVG pictures. There is no SVG-image PPTX output, and PowerPoint's manual Convert-to-Shape operation is unsupported.
10
11 ## `svg_authoring_view.py`
12
13 Create a lightweight editable authoring IR bundle from one PPTX-imported SVG or
14 a directory of imported SVGs:
15
16 ```bash
17 python3 scripts/svg_authoring_view.py <svg-file-or-directory> -o <output-dir> \
18 --projection-kind layered
19 ```
20
21 The operation is non-destructive and refuses existing output files unless
22 `--force` is explicit. It never writes back to the source SVG. The JSON report
23 on stdout records original/projected byte counts and removals by category. The
24 output directory contains the editable SVGs, one model-readable
25 `authoring_summary.json`, and one tool-only `authoring_manifest.json`.
26
27 The projected copy:
28
29 - removes embedded `txbody` metadata;
30 - removes hidden native geometry carriers while retaining and unwrapping their
31 visible preview geometry;
32 - removes source-object identity/style/hash attributes that are only useful to
33 an exact import round trip;
34 - keeps visible paths, text, images, stable ids, Master/Layout root markers,
35 selected native-shape intent, and a document-local `data-pptx-source-ref` on
36 each imported logical object;
37 - rewrites relative local asset references for the projection's new location;
38 - compacts imported model-facing frames and safe transform page coordinates to
39 at most two decimals.
40
41 The summary stores the current SVG roster plus compact per-file canvas, size,
42 text, image, vector, placeholder, icon, and source-ref counts. Models read the
43 summary and editable SVGs; they do not read the machine manifest. The manifest
44 stores relative source/authoring filenames, source and initial authoring hashes,
45 and source element paths. It deliberately does not copy the opaque payload.
46 The authoring bundle is the editable source for template creation; the complete
47 imported SVG remains immutable native-payload backing. Final
48 `templates/*.svg` files are materialized and validated from that pair. The IR
49 directory itself is not a supported direct input to `svg_to_pptx.py`.
50
51 Regenerate the summary after direct edits that do not pass through one of the
52 in-place normalization tools:
53
54 ```bash
55 python3 scripts/svg_authoring_view.py <authoring-dir> --refresh-summary
56 ```
57
58 This projection is separate from canonical preset authoring. New project SVGs
59 and project-owned templates use the compact authored form: one atomic
60 `<g data-pptx-authoring="preset">` owns the preset intent and base paint, with
61 the registry-generated visible `<path>` layers as direct children. Quality
62 check and export rerender the locked registry to validate that group, so the
63 compact form has no hidden carrier, preview wrapper, or serialized preview
64 fingerprint. `pptx_to_svg.py` continues to emit the expanded carrier/preview
65 evidence required for import and round-trip decisions. The normative boundary
66 is owned by [`shared-standards-core.md`](../../references/shared-standards-core.md) §1.5, with
67 authoring guidance in
68 [`native-shape-authoring.md`](../../references/native-shape-authoring.md).
69
70 ## Shape Boolean maintenance smoke
71
72 Run this manual smoke from the repository root after changing
73 `shape_boolean_svg.py`, preset geometry, path conversion, or custom-geometry
74 import/export. It uses only a gitignored `projects/_smoke_*` workspace and the
75 inline-smoke convention from [`code-style.md`](../../../../docs/rules/code-style.md)
76 §11; do not turn it into a test file or example deck.
77
78 ```bash
79 python3 - <<'PY'
80 import re
81 import subprocess
82 import sys
83 import tempfile
84 import zipfile
85 from pathlib import Path
86 from xml.etree import ElementTree as ET
87
88 import pathops
89
90 project = Path(tempfile.mkdtemp(prefix="_smoke_shape_boolean_", dir="projects"))
91 scripts = Path("skills/ppt-master/scripts")
92 svg_output = project / "svg_output"
93 svg_output.mkdir()
94 (project / "spec_lock.md").write_text(
95 """<!-- ppt-master-schema: spec-lock/v1 -->
96 # Execution Lock
97
98 ## canvas
99 - viewBox: 0 0 1280 720
100 - format: ppt169
101 ## communication
102 - audience:
103 - objective:
104 - core_message:
105 ## mode
106 - mode: briefing
107 ## visual_style
108 - visual_style: Boolean maintenance smoke
109 ## colors
110 - bg: #FFFFFF
111 - primary: #2563EB
112 - accent: #F97316
113 - text: #0F172A
114 ## typography
115 - font_family: Arial, sans-serif
116 - title_family: Arial, sans-serif
117 - body_family: Arial, sans-serif
118 - title: 36
119 - body: 20
120 ## icons
121 - library: none
122 - inventory: none
123 ## page_rhythm
124 - P01: dense
125 ## pptx_structure
126 - mode: flat
127 ## forbidden
128 - Unsupported SVG constructs
129 """,
130 encoding="utf-8",
131 )
132
133
134 def run_tool(script, *args):
135 result = subprocess.run(
136 [sys.executable, str(scripts / script), *map(str, args)],
137 capture_output=True, text=True,
138 )
139 assert result.returncode == 0, result.stderr or result.stdout
140 return result.stdout.strip()
141
142 preset = run_tool(
143 "preset_shape_svg.py", "render", "rightArrow",
144 "--id", "preset-source", "--frame", "500", "120", "240", "120",
145 "--fill", "#2563EB", "--stroke", "none",
146 )
147 source = project / "operands.svg"
148 source.write_text(
149 f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
150 <defs><clipPath id="clip"><rect width="80" height="80"/></clipPath></defs>
151 <g transform="translate(100 80) scale(1.2)">
152 <rect id="body" x="40" y="40" width="400" height="240" rx="20"
153 fill="#2563EB" stroke="#0F172A" stroke-width="5"
154 stroke-dasharray="10 4"/>
155 <circle id="cutout" cx="240" cy="160" r="70" fill="#F97316"/>
156 <text id="text-cutout" x="240" y="235" text-anchor="middle"
157 font-family="sans-serif" font-size="180" font-weight="700">01</text>
158 <text id="missing-font" x="240" y="235"
159 font-family="Definitely Missing Font" font-size="80">X</text>
160 <text id="nested-text" x="240" y="235"
161 font-family="sans-serif" font-size="80"><tspan>X</tspan></text>
162 <path id="open" d="M 40 320 L 260 320 L 260 420" fill="#2563EB"/>
163 <rect id="clipped" x="40" y="320" width="160" height="100"
164 clip-path="url(#clip)" fill="#2563EB"/>
165 <path id="imported" d="M 240 320 H 400 V 420 H 240 Z"
166 data-pptx-geometry-kind="custom" fill="#2563EB"/>
167 <rect id="dashoffset" x="440" y="320" width="120" height="100"
168 fill="#2563EB" stroke="#0F172A" stroke-dashoffset="2"/>
169 <rect id="non-scaling" x="500" y="40" width="150" height="120"
170 fill="#2563EB" stroke="#0F172A" stroke-width="5"
171 stroke-dasharray="10 4" vector-effect="non-scaling-stroke"/>
172 <circle id="non-scaling-cut" cx="625" cy="100" r="45" fill="#F97316"/>
173 <rect id="far" x="800" y="320" width="100" height="80" fill="#2563EB"/>
174 </g>
175 {preset}
176 <circle id="preset-cut" cx="690" cy="180" r="52" fill="#F97316"/>
177 </svg>
178 """,
179 encoding="utf-8",
180 )
181
182 operations = [
183 ("union", "union", "preset-source", "preset-cut"),
184 ("combine", "combine", "body", "cutout"),
185 ("fragment", "fragment", "body", "cutout"),
186 ("intersect", "intersect", "body", "cutout"),
187 ("subtract", "subtract", "body", "cutout"),
188 ("text-subtract", "subtract", "body", "text-cutout"),
189 ]
190 expected_custom_shapes = 0
191 for index, (name, operation, first, second) in enumerate(operations, start=1):
192 fragment = run_tool(
193 "shape_boolean_svg.py", "render", source, "--operation", operation,
194 "--source", first, "--source", second, "--id", f"result-{name}",
195 )
196 paths = list(
197 ET.fromstring(
198 f'<svg xmlns="http://www.w3.org/2000/svg">{fragment}</svg>'
199 )
200 )
201 assert paths and all(path.tag.endswith("}path") for path in paths)
202 assert all(
203 token not in fragment
204 for token in ("clip-path=", "fill-rule=", "mask=", "transform=")
205 )
206 if operation == "fragment":
207 assert len(paths) > 1
208 assert [path.get("id") for path in paths] == [
209 f"result-{name}-{piece}"
210 for piece in range(1, len(paths) + 1)
211 ]
212 else:
213 assert len(paths) == 1
214 assert paths[0].get("id") == f"result-{name}"
215 if operation == "combine":
216 assert all(path.get("stroke-width") == "6" for path in paths)
217 assert all(path.get("stroke-dasharray") == "12 4.8" for path in paths)
218 if operation == "subtract":
219 assert (paths[0].get("d") or "").count("M ") >= 2
220
221 expected_custom_shapes += len(paths)
222 (svg_output / f"{index:02d}_{name}.svg").write_text(
223 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" '
224 'data-pptx-page-role="content">'
225 '<rect x="0" y="0" width="1280" height="720" fill="#FFFFFF"/>'
226 f"{fragment}</svg>\n",
227 encoding="utf-8",
228 )
229
230 non_scaling = ET.fromstring(
231 run_tool(
232 "shape_boolean_svg.py", "render", source, "--operation", "union",
233 "--source", "non-scaling", "--source", "non-scaling-cut",
234 "--id", "result-non-scaling",
235 )
236 )
237 assert non_scaling.get("stroke-width") == "5"
238 assert non_scaling.get("stroke-dasharray") == "10 4"
239 assert non_scaling.get("vector-effect") == "non-scaling-stroke"
240
241 rejections = [
242 ("union", "body", "open", "open subpath"),
243 ("union", "body", "clipped", "uses clip-path"),
244 ("union", "body", "imported", "PPTX import/round-trip metadata"),
245 ("union", "body", "dashoffset", "stroke-dashoffset"),
246 ("intersect", "body", "far", "produced no filled area"),
247 ("subtract", "body", "missing-font", "cannot resolve an installed font"),
248 ("subtract", "body", "nested-text", "child content is unsupported"),
249 ]
250 for operation, first, second, expected_error in rejections:
251 rejected = subprocess.run(
252 [
253 sys.executable,
254 str(scripts / "shape_boolean_svg.py"),
255 "render", str(source), "--operation", operation,
256 "--source", first, "--source", second,
257 "--id", f"reject-{second}",
258 ],
259 capture_output=True, text=True,
260 )
261 assert rejected.returncode != 0
262 assert expected_error in rejected.stderr, rejected.stderr
263
264 run_tool(
265 "svg_quality_checker.py", project,
266 "--quick-generate", "--format", "ppt169",
267 "--stage", "final", "--json",
268 )
269 pptx = project / "boolean-smoke.pptx"
270 run_tool("svg_to_pptx.py", project, "--quick-generate", "-o", pptx)
271 with zipfile.ZipFile(pptx) as archive:
272 slides = [
273 name
274 for name in archive.namelist()
275 if re.fullmatch(r"ppt/slides/slide\d+\.xml", name)
276 ]
277 custom_shapes = sum(
278 archive.read(name).count(b"<a:custGeom>")
279 for name in slides
280 )
281 assert len(slides) == len(operations), slides
282 assert custom_shapes == expected_custom_shapes
283
284 readback = project / "readback"
285 run_tool(
286 "pptx_to_svg.py", pptx, "-o", readback,
287 "--inheritance-mode", "flat", "--strict",
288 )
289 slides = sorted((readback / "svg").glob("slide_*.svg"))
290 readback_custom_shapes = sum(
291 slide.read_text(encoding="utf-8").count('data-pptx-custgeom="')
292 for slide in slides
293 )
294 assert len(slides) == len(operations), slides
295 assert readback_custom_shapes == expected_custom_shapes
296 print(
297 f"Shape Boolean smoke: passed "
298 f"({expected_custom_shapes} custom shapes; {project})"
299 )
300 PY
301 ```
302
303 The seven inline negative cases must return nonzero and match their expected
304 errors; every other command must pass. Open the printed
305 `boolean-smoke.pptx` path in PowerPoint: both shape-cut and text-cut Subtract
306 results must have real holes, and every Fragment sibling must remain separately
307 selectable.
308
309 ## `compact_svg_coordinates.py`
310
311 Compact safe model-facing page-space coordinates without rewriting unrelated
312 SVG formatting:
313
314 ```bash
315 python3 scripts/compact_svg_coordinates.py <svg-file-or-directory>
316 python3 scripts/compact_svg_coordinates.py <template-directory> \
317 --inplace --keep-native-frames
318 ```
319
320 The default run is a dry-run JSON report. `--inplace` atomically replaces only
321 changed SVG files. The shared create-template final pass uses
322 `--keep-native-frames`: it compacts `data-pptx-bounds`, translation values,
323 rotation centers, and
324 matrix `e/f`, while preserving canonical
325 authored-preset or inline native frames. `svg_authoring_view.py` separately
326 compacts imported model-facing frames because unchanged mirror refs can recover
327 their exact coordinates from immutable lossless backing.
328
329 The compactor never rounds path/points geometry, normalized crop or nested
330 `viewBox` ratios, gradient offsets, opacity, scale arguments, rotation angles,
331 or matrix `a/b/c/d` coefficients. Type A mirror materialization invokes the
332 same compactor before native-record externalization; `standard` and `fidelity`
333 use the shared final pass before template validation.
334
335 ## `extract_svg_assets.py`
336
337 Factor large vector subtrees out of lightweight authoring IR documents and
338 replace them with compact `<use data-icon>` references:
339
340 ```bash
341 python3 scripts/extract_svg_assets.py <layered_svg_dir> \
342 --icons-dir <icons_dir> --icon-namespace imported \
343 --inplace --id-prefix layered
344 python3 scripts/extract_svg_assets.py <flat_svg_dir> \
345 --icons-dir <icons_dir> --icon-namespace imported \
346 --reuse-inventory <layered_inventory.json> \
347 --inplace --id-prefix flat
348 ```
349
350 The first pass records a source fingerprint before namespacing each extracted
351 asset's internal ids. The second pass reuses a fingerprint-matched asset and
352 writes no duplicate SVG file. Unmatched flat-only subtrees still extract
353 normally. Use `--clean-stale` on both import-workspace passes to remove stale
354 generated files for their respective prefixes. In create-template workspaces,
355 `imported` is the fixed namespace: assets live once under `icons/imported/`, and
356 the working SVGs reference them as `data-icon="imported/<name>"`. Inventory
357 entries retain source refs from each extracted subtree, allowing expansion to
358 reconnect the authoring-manifest mapping. A rerun on an
359 already rewritten namespaced projection inventories those references and does
360 not progressively extract their remaining parent or sibling geometry. An
361 in-place pass over an authoring bundle refreshes `authoring_summary.json`
362 automatically.
363
364 ## `mirror_template_materialize.py`
365
366 Compile one Type A PPTX import workspace into a deterministic structured mirror
367 template after the layered authoring IR has been reviewed and edited:
368
369 ```bash
370 python3 scripts/mirror_template_materialize.py \
371 <import_workspace> <template_workspace>
372 ```
373
374 The command treats `<import_workspace>/authoring-svg/` as the sole editable
375 source. It reads the tool-only layered authoring manifest internally and
376 validates it against immutable lossless SVG
377 hashes, source PPTX hash, complete Master/Layout/Slide graph, inheritance
378 visibility facts, source-ref closure, and extracted-vector inventory before it
379 writes anything. It accepts an absent/empty destination or a project
380 `templates/` containing unique qualified Brand/Style specs plus, for a
381 Layout-over-Deck transition, one qualified Deck spec with no staged roster. A
382 bare spec, active structural roster, SVG, Layout spec, or other template payload blocks direct
383 materialization; Create Template uses its isolated transition workspace when a
384 new Layout or Deck must be composed with the other structural kind. It stages the whole
385 result before atomic publication, so a failed preflight cannot leave a partial
386 template.
387
388 Materialization preserves source page order and emits one definition-only
389 `layout_<layout_key>.svg` for every source Layout unused by all source Slides.
390 It mechanically expands fixed Master/Layout group wrappers into direct atoms,
391 rehydrates only unchanged converter-supported Slide-local/slot refs, keeps the
392 current SVG fallback for edited refs, preserves explicit text hard breaks, and
393 removes every IR-only source ref. Imported axis-flipped groups retain their
394 geometry reflection while descendant SVG text receives a matching
395 counter-reflection, preserving PowerPoint's upright glyph appearance in browser
396 previews. Supported opaque `p:txBody`,
397 relationship-free `p:style`, and `a:custGeom` payloads are deduplicated into
398 `templates/native_payloads.json.gz`. Repeated native restoration attributes
399 are stored there as short `data-pptx-native-ref` records; page and
400 imported-vector SVGs retain only those record ids and content-hash payload
401 references. The native record referenced by an imported text placeholder
402 carrier owns its authoritative source frame, so the Slide-local frame can
403 differ from reusable Layout bounds without restoring long exact coordinates
404 inline. Structural Master/Layout, placeholder, layer, and editable-object
405 fields remain inline. Source `p:sldLayout@showMasterSp` and
406 `p:sld@showMasterSp` facts become canonical root
407 `data-pptx-show-master-shapes` and
408 `data-pptx-show-inherited-shapes` booleans.
409
410 Checker, template-structure validation, and export hydrate both store layers in
411 memory; legacy inline payload and v1 payload-only stores remain readable.
412
413 The published `ppt-master.template-execution-manifest.v1` roster points to one
414 compact `ppt-master.template-text-slots.v2-min` sidecar per prototype. Each text
415 slot contains only `selector`, `role`, `current_text`, `text_segments`, and
416 `tspan_count`; a top-level tool hash covers its selectors and immutable
417 text/tspan topology and attributes. These records are deterministic tool
418 diagnostics, not page-authoring inputs. Page-context emits only the complete
419 prototype's path and SHA for that reference, so the model reads the SVG once
420 per execution context and reuses it until the SHA changes. The model chooses
421 semantics and edits only existing visible text values, while checker and
422 structured export validate output attributes, text/tspan topology, and
423 referenced-resource hashes against
424 the prototype.
425
426 The output routes reusable vectors once to `icons/imported/`, bitmaps to
427 `images/`, and other referenced files to `templates/assets/`. The JSON report
428 reports payload occurrence, native-record, unique-byte, and compressed-store
429 counts and is written to stdout only. The command intentionally does not create
430 `templates/design_spec.md`; Template_Designer writes the package-specific rules
431 and page roster after materialization. This compiler is for Type A mirror materialization,
432 not `standard` / `fidelity`, loose Type B SVGs, ordinary generation, finalize,
433 or export.
434
435 ## `extract_svg_pictures.py`
436
437 Normalize one deliberately selected complex SVG object into one PowerPoint
438 picture. The command accepts exact `<g id>` values only, writes each group as a
439 tight standalone SVG asset, embeds its local image/CSS dependencies, and
440 replaces the source group at the same parent index with one `<image>`. Native
441 export therefore emits one `p:pic` backed by SVG media.
442
443 ```bash
444 python3 scripts/extract_svg_pictures.py \
445 "<workspace>/authoring-svg/<layered_svg_file>.svg" \
446 --select "<group_id>" \
447 --resource-root "<workspace>" \
448 --images-dir "<workspace>/picture-assets" \
449 --inplace
450 ```
451
452 Imported PowerPoint groups normally provide `data-pptx-frame`, which is used
453 as the picture bounds. For a large standalone SVG without frame metadata, the
454 tool measures the selected group with Playwright; use repeated
455 `--bounds ID=x,y,width,height` values when browser measurement is unavailable
456 or when effect overflow needs an explicit frame. `--padding` expands the
457 chosen bounds. The generated `*_picture_asset_inventory.json` records the
458 bounds source, asset hash, copied definition ids, and embedded local resources.
459 Nested selections are accepted only through metadata-only `<g>` ancestors.
460 When an ancestor carries a transform, style, clip, opacity, or other visual
461 attribute, select that outer group instead; this prevents applying the ancestor
462 effect once inside the SVG asset and again to the replacement `<image>`.
463 Scripts, `foreignObject`, SVG animation, remote resources, and external SVG
464 fragment references fail closed; local image/CSS resources must stay inside
465 the declared `--resource-root` and are embedded into the asset.
466 An in-place rewrite inside an authoring bundle refreshes
467 `authoring_summary.json` automatically.
468
469 This operation belongs only to an explicit `create-template` normalization
470 decision in `standard` or `fidelity` mode. It does not choose groups, detect
471 repetition, infer a Master/Layout, or run during ordinary import, free
472 generation, mirror materialization, finalize, or export. Placeholder, native
473 single-shape, table/chart, icon-placeholder, and authored-preset groups are
474 rejected because they already own a different semantic route.
475
476 Do not confuse this tool with `extract_svg_assets.py`:
477
478 - `extract_svg_assets.py` is a model-readability optimization. It replaces
479 heuristic vector runs with `<use data-icon>`, then re-inlines them before
480 export so the PPTX still contains native shapes.
481 - `extract_svg_pictures.py` is an explicit representation change. It replaces
482 only named groups with `<image>`, so each result intentionally remains one
483 editable PowerPoint picture rather than individually editable paths.
484
485 ## Recommended Pipeline
486
487 Run these steps one at a time. Wait for each command to exit successfully before
488 starting the next command.
489
490 When the effective Speaker Notes outcome in `design_spec.md §I` is enabled, run:
491
492 ```bash
493 python3 scripts/total_md_split.py <project_path>
494 ```
495
496 After `total_md_split.py` exits successfully, run:
497
498 ```bash
499 python3 scripts/finalize_svg.py <project_path>
500 ```
501
502 After `finalize_svg.py` exits successfully, run:
503
504 ```bash
505 python3 scripts/svg_to_pptx.py <project_path>
506 ```
507
508 When Speaker Notes is disabled, skip `total_md_split.py` and use
509 `python3 scripts/svg_to_pptx.py <project_path> --no-notes` for the final
510 command. This prevents stale files under `notes/` from being embedded.
511
512 Do not start another post-processing command while the current command is still
513 running. The canonical gates and success criteria are owned by
514 [`generate-pptx.md`](../../workflows/generate-pptx.md) Step 7.
515
516 ## `finalize_svg.py`
517
518 Unified post-processing entry point. This is the preferred way to run SVG cleanup.
519
520 It aggregates:
521 - `embed_icons.py`
522 - static same-document `<use>` expansion from `svg_to_pptx/use_expander.py`
523 - `align_embed_images.py` (`crop-images` / `fix-aspect` / `embed-images` aliases route here)
524 - `flatten_tspan.py`
525
526 `svg_final/` remains a required Step 7.2 artifact even though the native exporter reads `svg_output/`. It is the self-contained visual reference and may be manually inserted as an SVG picture.
527
528 ## `svg_to_pptx.py`
529
530 Convert project SVGs into PPTX.
531
532 Native formulas use the two markers owned by
533 [`native-formula.md`](../../references/native-formula.md). A standalone block
534 stores delimiter-free LaTeX in the JSON metadata of
535 `<g data-pptx-replace-with="formula">` and exports `m:oMathPara`. A leaf
536 `<tspan data-pptx-inline-formula="...">preview</tspan>` inside ordinary text
537 exports `m:oMath` in the same DrawingML paragraph as its surrounding runs; it
538 inherits computed size and visible solid fill, then uses the project text
539 language and Cambria Math.
540 Matrices, multiline derivations, and other high-structure expressions remain
541 blocks. Formula replacement is always active, independent of
542 `--native-charts-and-tables`: export replaces only the registered SVG preview
543 and writes editable PowerPoint 2010+ Office Math. It emits no formula PNG, media
544 relationship, or compatibility fallback, and makes no rendering/editability
545 promise for Keynote, WPS, LibreOffice, or another non-PowerPoint client.
546
547 ```bash
548 python3 scripts/svg_to_pptx.py <project_path>
549 # Explicit compact image export:
550 python3 scripts/svg_to_pptx.py <project_path> --image-sizing display --image-scale 2 --image-quality 85
551 # Force original image bytes:
552 python3 scripts/svg_to_pptx.py <project_path> --no-image-optimize
553 python3 scripts/svg_to_pptx.py <project_path> --native-charts-and-tables
554 python3 scripts/svg_to_pptx.py <project_path> --pptx-structure structured # deck/layout template override
555 python3 scripts/svg_to_pptx.py <project_path> --pptx-structure flat # free-design/brand-only override
556 # Template-import visual round-trip diagnostic only:
557 python3 scripts/svg_to_pptx.py <template_import_output> -s svg-flat
558 # Post-processed-source comparison diagnostic only (never a release export):
559 python3 scripts/svg_to_pptx.py <project_path> -s final
560 python3 scripts/svg_to_pptx.py <project_path> --no-notes
561 python3 scripts/svg_to_pptx.py <project_path> -t none
562 python3 scripts/svg_to_pptx.py <project_path> --auto-advance 3
563 python3 scripts/svg_to_pptx.py <project_path> --animation mixed --animation-duration 0.8
564 python3 scripts/svg_to_pptx.py <project_path> --reflow-text # opt-in PowerPoint reflow
565 python3 scripts/svg_to_pptx.py <project_path> --no-merge # one text frame per visual line
566 python3 scripts/svg_to_pptx.py <project_path> --recorded-narration audio
567 python3 scripts/svg_to_pptx.py <project_path> --recorded-narration audio --animation-config animations.json
568 python3 scripts/svg_to_pptx.py <project_path> --recorded-narration audio --no-animations
569 ```
570
571 Native image export defaults to `--image-sizing cap`: it preserves source bytes
572 when no resize or EXIF geometry normalization is required, and re-encodes only
573 images that require one of those transformations. The `display` command above
574 is an explicit compact export; `--no-image-optimize` disables all native image
575 optimization and forces original bytes.
576
577 The normal command reads `pptx_structure.mode` from `spec_lock.md`. For legacy
578 projects whose lock exists but predates that field, export emits one compatibility
579 warning and uses `flat`; no SVG regeneration is required. A missing `spec_lock.md`,
580 an explicit legacy/unknown mode, or a requested `structured` export without an
581 explicit current structured contract remains blocking.
582
583 Explicit direct generation may use the
584 [`quick-generate`](../../workflows/profiles/quick-generate.md) profile after the
585 current agent has converted/read sources, researched identified factual gaps,
586 prepared the required images, icons, and resource manifests as needed, and
587 retained any source LaTeX for direct native-marker authoring. That profile skips Strategist, Confirm UI, `design_spec.md`, and
588 `spec_lock.md`; it does not skip the resources required by the authored pages.
589 After the complete SVG roster exists, run its lockless final checker, then
590 export:
591
592 ```bash
593 python3 scripts/svg_quality_checker.py <project_path> \
594 --quick-generate --stage final --json
595 python3 scripts/svg_to_pptx.py <project_path> --quick-generate
596 ```
597
598 This direct-export flag takes `svg_output/` as its authored page source, resolves
599 valid project-local resources referenced by those pages, infers one consistent
600 canvas, uses flat converter-default package scaffolding, and does not read or
601 require `spec_lock.md`. Notes, motion, narration, native objects, conversion
602 trace, and other ordinary exporter capabilities remain available; notes,
603 custom object animation, and narration start off in Quick and may be enabled
604 when needed. The exporter refuses a missing, blocking, non-final, or stale
605 Quick final report before PPTX creation. Default-path output retains the normal
606 postflight report and `backup/` snapshot; explicit `-o` retains the ordinary
607 no-backup behavior. Existing source, analysis, image/icon, and resource-manifest
608 artifacts remain untouched; formula source stays inside its authored SVG marker.
609
610 For generated-project narration, follow the
611 [`generate-audio`](../../workflows/stages/generate-audio.md) stage. It owns voice
612 selection, audio generation, and the narrated re-export workflow.
613
614 Behavior:
615 - Default output (either Generate profile, no `-o`):
616 - `exports/<project_name>_<timestamp>.pptx` — native editable pptx (canonical output)
617 - `validation/<project_name>_<timestamp>.report.json` — package postflight, quality-gate linkage, unresolved resource audit, and published part counts
618 - `backup/<timestamp>/svg_output/` — copy of authored SVG source for re-export without re-running the LLM
619 - `exports/` contains only final PPTX deliverables; machine-readable quality and postflight reports belong in `validation/`.
620 - The default Generate flow always runs `finalize_svg.py` before export. This directory is the self-contained SVG visual preview; it is not packaged as a second PPTX. Quick-generate deliberately skips it.
621 - In both Generate profiles, explicit `-o/--output` changes the native PPTX destination and skips `backup/`; the postflight report still uses the output stem under the project `validation/` directory.
622 - Postflight reruns ZIP integrity and published Slide count. Internal relationships,
623 structured-package validation, transitions, and animations are enforced before the
624 builder publishes the PPTX and are reported as `enforced-at-build`, not as repeated
625 postflight checks.
626 - `font_portability` warns when a complete font stack has no concrete family or when
627 the converter resolves its Latin / East Asian role to a typeface that normally
628 requires a custom installation. A recommended stack such as
629 `"Microsoft YaHei", Arial, sans-serif` does not warn merely because it ends with a
630 generic fallback.
631 - Multiline text export modes:
632 - Default: one editable frame retains authored breaks and disables PowerPoint wrapping. An ordinary generated frame uses PowerPoint's native resize-shape-to-fit-text behavior, so deleting a retained break expands the frame instead of leaving text outside it; imported exact frames and structured multiline placeholder carriers retain fixed-size behavior.
633 - `--reflow-text`: eligible same-size lines become flowing prose that PowerPoint may rewrap; a font-size change, list marker, or accepted larger gap remains a paragraph boundary. Legacy `--merge-paragraphs` aliases this mode.
634 - `--no-merge`: each dy-stacked line becomes an independent frame with its own placement.
635 - Detection is conservative: mixed-layout `<text>` falls back to per-line frames. Use `--reflow-text` only for resizable body copy and `--no-merge` only for independent line objects or absolute line positions.
636 - Native release export reads `svg_output/`. `-s final` is an explicit diagnostic override for comparing conversion behavior against post-processed SVGs; it does not change artifact ownership or create a supported release path.
637 - `svg_final/` may be opened directly or inserted into PowerPoint as an SVG picture. PowerPoint's manual Convert-to-Shape operation is outside the compatibility contract.
638 - On every SVG-authoring route, each file in `svg_output/` is the complete visible
639 page-design source. Templates and locks may guide authoring, but finalize/export
640 never use them to overlay visible content missing from the SVG. Notes, animation,
641 narration, transitions, and direct native-PPTX workflows keep their separate
642 inputs and package-level processing.
643 - For PPTX template-import workspaces, use `-s svg-flat` when you need a visual round-trip check. The layered `svg/` tree is the machine-readable template source and intentionally does not inline inherited master / layout decoration into each slide.
644 - Native mode is strict about unsupported visual SVG elements: if a visual element cannot be represented or safely preserved, export fails with the SVG file, element tag, and position instead of silently dropping content.
645 - Omitting `--pptx-structure` reads `spec_lock.md`. Free-design, brand-only, and `template_reuse_scope: style` releases declare `mode: flat`, omit Master/Layout mappings and SVG structure metadata, and materialize one clean project-owned Master plus one Blank Layout from the current lock. Deck/layout templates use `mode: structured` only for `template_reuse_scope: mirror|layout`, with complete unique `pptx_masters` / `pptx_layouts` rosters and one `page_pptx_layouts` assignment per page. A template-backed Layout definition may remain unused by pages and still register in the final package.
646 - On structured template routes, every page root repeats Master/Layout keys and picker names. Master/Layout fixed visuals are direct semantic atoms. Ordinary layer `<g>` elements are invalid; one validated compact authored-preset `<g>` emitted by `preset_shape_svg.py` is the sole group exception because it compiles to one native shape.
647 - Every visible direct root `<g>` except a compact helper-authored preset atom requires root-coordinate `data-pptx-bounds`; nested bounds are ignored. The text-free preset atom remains top-level when standalone, uses `data-pptx-frame`, and never carries bounds. Frame/native metadata never replaces bounds on any other group; placeholder bounds also define the slot frame. Checker compares root bounds with `viewBox`, descendant text with its module using DrawingML wrapping headroom, and every estimable visible text carrier directly with the root `viewBox` before that headroom. Images, shapes, paths, `<use>`, effects, and object frames are excluded from module containment. Per side, ≤`1px` is ignored; module overflow ≤`5%` warns and >`5%` fails, while larger page text overflow always fails. Bounds never clip/reflow; unestimable visible text warns. A wholly off-canvas direct-root Morph endpoint may opt out of page containment with `data-pptx-morph-staging="true"`; it still needs valid module bounds, retained Morph uses an explicit pair, and partial overflow remains blocking.
648 - Missing required root bounds fails on final pages/templates and under `--template-mode`; references warn until adapted.
649 - On structured template routes, each normal slot is a direct root `<g id>` with semantic type, positive design-zone bounds, and exactly one compatible carrier. Composite `object` slots use explicit proxy binding; zero-slot Layouts are valid. Flat pages keep all SVG objects Slide-local.
650 - Flat export maps locked typography/colors into a clean project-owned theme/Master, removes stock content placeholders and unused built-in Layouts, retains only the standard date/footer/slide-number capability hooks, and keeps one Blank Layout without promoting Slide content. Structured export additionally creates one reusable Layout per declared key and reopens the package to verify the full Presentation → Master → Layout → Slide graph, fixed-object order, placeholder identities/bounds, carrier bindings, hidden proxies, and zero-slot Layouts.
651 - Template `page_layouts` remains input provenance. Strict preserves the prototype contract; adaptive retains its Master and may use a new Layout identity only when Strategist declared it in the plan and lock. Construction cannot allocate or mutate Layout identity downstream.
652 - Legacy structured/template contracts using `baseline`, `template`, `preserve`, `layout_strategy`, `data-pptx-layout-kind`, `distilled`/`utility`, direct atomic placeholders, or incomplete Master identity are rejected with a pointer to [`create-template`](../../workflows/create-template.md). Create a new workspace and generate new structured SVG pages; do not upgrade the existing project in place. Explicit flat free-design/brand-only projects intentionally omit Master identity.
653 - Native output uses content-hash media filenames, so identical images are reused and different images cannot overwrite each other by sharing a basename.
654 - `[Content_Types].xml` is generated from the actual media extensions written into the PPTX. Unknown media extensions fail unless Python's `mimetypes` can identify them.
655 - Native export writes to a temporary file first and publishes the requested PPTX only after conversion succeeds. A failed conversion does not replace the main output file.
656 - `--conversion-trace` without a path writes `validation/<output_stem>.trace.json`. `--conversion-trace <path>` respects the explicit destination; relative paths are resolved from the project root, so `exports/<name>.trace.json` remains available when intentionally requested.
657 - Formal default and `--quick-generate` release export compute the exact SVG source fingerprint and refuse a missing, unreadable, unsupported, non-final, blocking, stale, or unverifiable final quality report before PPTX creation. A project without `validation/svg_quality_report.json` exits nonzero with the `not-provided` gate status; run the final checker against its current `svg_output/` first. An explicit non-`output` `--source` remains a diagnostic override and bypasses this release gate; postflight still records any verifiable report linkage.
658 - The final quality report carries an informational `carrier_receipt` aggregate plus each page's `files[].info.carrier_receipt`: actual text/image/icon counts, SVG geometry, native preset names, marker use, native Chart/Table/Formula markers, and largest image-frame share. The terminal prints only the compact aggregate. These facts never affect exit status, create coverage quotas, or score design; the active Generate profile compares them with its retained page decisions before export.
659 - After publication, native export writes `validation/<output_stem>.report.json`. The report distinguishes authored Slides from internal Layout definitions, reruns ZIP integrity and published Slide-count checks, records slide/layout/master/notes part counts, labels relationship/structured/transition/animation validation as enforced at build time, links the final SVG quality report only when its SHA-256 source fingerprint matches the exact export inputs, and surfaces stale/unverified gates, unresolved template tokens, generic-only font stacks, and external image references. A matching final quality report with introduced warnings yields `passed-with-warnings` and a `quality_introduced_warnings=<N>` receipt instead of a clean `passed` claim.
660 - By default, a successful command also prints a compact receipt instead of requiring a report read: `[POSTFLIGHT] status=<...> quality_gate=<...> slides=<N> warning_categories=<N>`, followed by one compact line per warning category and the `[PPTX]` / `[REPORT]` paths. Resource-warning lines carry counts; a non-passing quality gate carries its status. Routine agents use this receipt and do not load either complete validation JSON into model context. Full reports remain cold audit artifacts; failure investigation and explicit audits extract only the required fields. `--quiet` keeps suppressing successful-run output.
661 - Before publishing structured template output, export reopens the temporary PPTX and validates the Slide → Layout → Master graph and registrations, Layout identity, placeholder identity, reusable bounds, and prompt/level-one sizes. A mismatch aborts publication. Flat release instead validates its single referenced Master/Layout shell and exact date/footer/slide-number hook roster before packaging.
662 - Authored SVG clip-path restrictions remain. Crop wrappers use an
663 overflow-hidden viewport; preview-safe shape clips target the inner image in
664 viewBox coordinates, while legacy imported wrapper clips remain compatible.
665 Both map to native picture crop/geometry when possible.
666 - The default Generate flow embeds speaker notes automatically unless `--no-notes` is used; Quick Generate defaults them off and enables them with `--with-notes`
667 - Recorded narration is opt-in:
668 - `notes_to_audio.py` uses `edge-tts` by default, or a configured cloud TTS provider (`elevenlabs`, `minimax`, `qwen`, `cosyvoice`), and generates one audio file per slide into `audio/`
669 - Narration text is read strictly from the matching `notes/*.md` file; the script only skips Markdown heading lines (`# ...`) and does not summarize, rewrite, or filter delivery notes
670 - `--recorded-narration audio` prepares PowerPoint's "recorded timings and narrations": every slide must have matching `m4a` / `mp3` / `wav` audio, `ffprobe` must read every duration, and `--animation-trigger on-click` is rejected
671 - `--recorded-narration audio` keeps speaker notes, embeds each matching audio file, and writes slide auto-advance timings from page-start lead-in + audio duration + page-tail padding. `--narration-start-floor` and `--narration-padding` are independent optional seconds; their defaults are `0.8` and `0.5`, and the post-transition lead-in is `max(0, start floor - transition duration)`
672 - While motion remains enabled, narrated export without an explicit `--animation-config` selects `<project>/narration_animations.json` when either animation sidecar exists; canonical-only cue synchronization therefore blocks until the derived file exists. Narration-independent custom motion explicitly passes `--animation-config animations.json`, even when a derived sidecar also exists
673 - Without animation sidecars, Generate narration may inherit base-report deck motion via `--inherit-motion-from`; direct low-level omission keeps legacy `fade` / no object builds. Use `--no-animations` to remove object/page motion while retaining narration timings
674 - Non-narrated export keeps the existing optional `<project>/animations.json` default
675 - Narration timing merges into the existing slide timing DOM. While motion remains enabled, object-animation rows and the resolved page transition are preserved rather than regenerated; inherited `-a none` suppresses object rows, and `--no-animations` removes both motion layers
676 - `--narration-audio-dir audio` is the lower-level embedding path: it embeds whatever files match and allows partial audio coverage
677 - Either narration flag names the default-flow export `<project_name>_<timestamp>_narrated.pptx`, telling it apart from silent exports in the same directory
678 - This is intended for direct PowerPoint video export with "Use recorded timings and narrations"
679 - Long-audio import and automatic long-audio splitting are not supported; keep narration assets page-level
680 - Voice choices can be listed with `python3 scripts/notes_to_audio.py --list-common-voices`, `python3 scripts/notes_to_audio.py --list-voices --locale zh-CN`, or provider-specific `--provider <name> --list-voices`
681 - Page transitions are controlled by `-t/--transition`; per-element object animations are controlled by `-a/--animation`
682 - Per-element animation applies to ordinary top-level SVG `<g id="...">` groups; each group is a PowerPoint shape-target anchor, not necessarily one Animation Pane row. Use one group per logical Slide-local content unit rather than targeting a group count. Master/Layout atoms and slot groups are structural and excluded; exact id tokens remain a fallback only when explicit structural roles are absent
683 - An explicit `animations.json` group entry may override the marker-free legacy chrome-name heuristic. It cannot override `data-pptx-layer` or an explicit static role/placeholder marker
684 - Start mode is set globally by `--animation-trigger`, mirroring PowerPoint's Start dropdown: `after-previous` (default, cascade with `--animation-stagger` spacing on slide entry), `on-click` (presenter-paced), or `with-previous` (all together on slide entry). A sidecar row may override it with `trigger`; the slide value is only the inherited Start mode
685 - `on-click` is for live presentations only; recorded narration rejects every row that resolves to it, including a row with `trigger_shape`, because the tool does not generate object-level click timings
686 - Flat SVG roots without top-level groups fall back to at most 8 visible primitives; beyond that, animation is skipped on the slide
687 - Per-element animation defaults to `none`. `auto` is opt-in (`-a auto`) and maps
688 generic entrance effects from the group's SVG id: information-dense elements
689 get a stable entrance (chart→wipe, card-/step-/pillar-→fly,
690 title/takeaway→fade); image-like and unmatched ids rotate through bounded
691 entrance pools.
692 - `mixed` (legacy) deterministically rotates through the canonical entrance pool; `random` selects from the same entrance pool with a stable seed from the effective deck input. `auto`, `mixed`, and `random` never choose emphasis, motion-path, or exit effects; select an explicit canonical `entrance_*`, `emphasis_*`, `path_*`, or `exit_*` key for those authored duties. `--conversion-trace` records each resolved effect when enabled
693 - `--animation-duration` controls the inherited per-row schedule length (default
694 `0.4`); scalable native effects preserve internal timing ratios, while
695 instantaneous presets keep their authored duration. `--animation-stagger`
696 supplies the default gap between successive non-trigger-shape rows in
697 `after-previous` mode (default `0.5`)
698 - Optional object-level overrides live in `<project>/animations.json` or a path passed via `--animation-config`; build and validate them with `animation_config.py scaffold|validate`. The scaffold is neutral (`defaults.animation.effect: none`, untouched groups `{}`). A populated group uses either the fully compatible legacy single-effect fields or a non-empty `effects[]`, never both; every `effects[]` row names an explicit effect
699 - Transition/object sound remains off by default. After SVG and visual motion are complete and one row has a concrete auditory job, read the complete [`sound-vocabulary.md`](../../templates/sounds/sound-vocabulary.md), then copy only selected ids with `sound_sync.py <project> <namespace>/<sound_id> [...]`; `list --query <term>` is optional exact filtering after that review. `transition.sound` references a project-relative `.wav`; object-animation `sound` accepts the existing `.m4a`/`.mp3`/`.wav` path contract, while bundled selections use the synced project-relative `.wav`. With no selected cue, do not create `<project>/sounds/`. Export never resolves ids or reads `templates/sounds/` directly
700 - One `effects[]` row becomes one Animation Pane record on the group's shape target. Each row may independently set sequence `order`, `delay`, `duration`, `trigger`, and `trigger_shape`; ordinary rows use page-wide order, while `trigger_shape` rows keep relative order in separate interactive sequences and imply `on-click`
701 - Animation configuration is strict: unknown effects/modes/triggers, invalid finite/range/order values, missing slides/groups, and structural-layer targets fail export without fallback or silent omission
702 - Generated export reads every slide back and verifies animation row order, including repeated rows on one shape target, trigger, shape target, resolved effect tuple and native behavior signature, duration, and offset. Package validation then checks timing placement, `p:cTn` ids, and `p:spTgt` references before publication
703 - The animation writer does not emit paragraph/text-range builds (`p:bldP`), custom freeform motion paths, native Chart/SmartArt build sequences, or media playback commands for grouped SVG content. Direct-PPTX routes preserve source object animation and perform structural package validation only; they do not author effects
704 - The full registry, OOXML rules, and compatibility boundary are documented in [`pptx-animations.md`](./pptx-animations.md)
705
706 Dependency:
707
708 ```bash
709 pip install python-pptx
710 ```
711
712 ## `total_md_split.py`
713
714 Split `total.md` into per-slide note files.
715
716 ```bash
717 python3 scripts/total_md_split.py <project_path>
718 python3 scripts/total_md_split.py <project_path> -o <output_directory>
719 python3 scripts/total_md_split.py <project_path> -q
720 ```
721
722 Requirements:
723 - Each section begins with `# `
724 - Heading text matches the SVG filename
725 - Sections are separated by `---`
726
727 ## `svg_quality_checker.py`
728
729 Validate SVG technical compliance.
730
731 ```bash
732 python3 scripts/svg_quality_checker.py examples/project/svg_output/01_cover.svg
733 python3 scripts/svg_quality_checker.py examples/project/svg_output
734 python3 scripts/svg_quality_checker.py examples/project
735 python3 scripts/svg_quality_checker.py examples/project --stage first-page
736 python3 scripts/svg_quality_checker.py examples/project --stage final --json
737 python3 scripts/svg_quality_checker.py examples/project --format ppt169
738 python3 scripts/svg_quality_checker.py --all examples
739 python3 scripts/svg_quality_checker.py examples/project --export
740 python3 scripts/svg_quality_checker.py path/to/template/templates --template-mode
741 ```
742
743 Checks include:
744 - `viewBox`
745 - banned elements
746 - paint compatibility: unsupported values error; supported non-default spellings such as `rgba()` receive non-blocking recommendations for `#RRGGBB` plus explicit alpha
747 - line-break structure
748 - explicit Master/Layout/slot structure for reusable templates
749 - duplicate empty Layout contracts under different keys
750
751 Warnings are advisory: they require no modification or acknowledgement and do
752 not affect the command's zero exit status. Only errors block the quality gate.
753
754 `--stage first-page` resolves only the first authored SVG and permits an incomplete
755 future page roster. `--stage final` checks the complete project. With `--json`,
756 the final stage writes `validation/svg_quality_report.json`, while the first-page
757 stage writes `validation/svg_quality_first_page_report.json` so it cannot overwrite
758 the release gate (or use `--json-output`). The report separates
759 release failures (`blocking`), changed/new advisories (`introduced`),
760 prototype-identical diagnostics (`inherited`), and source-conversion losses
761 (`source-import`). It also fingerprints every checked SVG so postflight cannot
762 mistake a stale report for the current export gate. On a successful run, use the
763 checker exit status and terminal summary; do not load the complete JSON unless a
764 failure investigation or explicit audit requires targeted fields.
765
766 Template mode accepts the same compact canonical preset groups as generated
767 pages: one atomic `<g data-pptx-authoring="preset">` with direct visible paths.
768 It validates those paths dynamically against the locked registry and does not
769 require an import-style carrier, preview wrapper, fingerprint, or a separate
770 source-payload opt-in marker. Exact syntax remains owned by the linked
771 standards rather than this pipeline overview.
772
773 ## `svg_position_calculator.py`
774
775 Analyze and review supported chart coordinates after SVG generation.
776
777 Use this after `svg_quality_checker.py` passes, and only for chart types supported by this script: `bar`, `pie` / `donut`, `radar`, `line` / `area` / `scatter`, and `grid`. Area charts do not have a separate calculator mode: use `calc line` for the upper boundary points, then close the filled region to the plot area's bottom baseline (`y_max`) in the SVG.
778
779 ### Calculate expected coordinates
780
781 ```bash
782 python3 scripts/svg_position_calculator.py calc bar --data "A:185,B:142" --area "130,155,1200,480" --bar-width 120
783 python3 scripts/svg_position_calculator.py calc line --data "0:50,10:80,20:120" --area "120,120,1200,600" --y-range "0,150"
784 python3 scripts/svg_position_calculator.py calc pie --data "A:35,B:25,C:20" --center "420,400" --radius 200
785 python3 scripts/svg_position_calculator.py calc grid --rows 2 --cols 3 --area "50,150,1230,670"
786 ```
787
788 For an area chart, use the line output as the top boundary:
789
790 ```svg
791 M first_x,first_y ... L last_x,last_y L last_x,y_max L first_x,y_max Z
792 ```
793
794 Manually compare the calculator output with the coordinates already present in the generated SVG. If coordinates differ, update the SVG from the `calc` output, rerun `svg_quality_checker.py`, then repeat the coordinate review. The tool intentionally does not rewrite SVG files automatically.
795
796 ### Analyze (inspect existing SVG)
797
798 ```bash
799 python3 scripts/svg_position_calculator.py analyze <svg_file>
800 ```
801
802 Use this after SVG generation to inspect existing SVG geometry when manual comparison needs more context.
803
804 ## Advanced Standalone Tools
805
806 ### `flatten_tspan.py`
807
808 ```bash
809 python3 scripts/svg_finalize/flatten_tspan.py examples/<project>/svg_output
810 python3 scripts/svg_finalize/flatten_tspan.py path/to/input.svg path/to/output.svg
811 ```
812
813 ### `align_embed_images.py`
814
815 ```bash
816 python3 scripts/svg_finalize/align_embed_images.py path/to/slide.svg
817 python3 scripts/svg_finalize/align_embed_images.py --dry-run path/to/slide.svg
818 ```
819
820 Use for rare single-file diagnostics when image `slice` / `meet` alignment and
821 Base64 embedding must be inspected outside `finalize_svg.py`. In normal project
822 runs, use `python3 scripts/finalize_svg.py <project_path>`; the old
823 `crop-images`, `fix-aspect`, and `embed-images` names remain accepted only as
824 `finalize_svg.py --only` aliases for the merged `align-images` step.
825
826 ### `embed_icons.py`
827
828 ```bash
829 python3 scripts/svg_finalize/embed_icons.py output.svg
830 python3 scripts/svg_finalize/embed_icons.py svg_output/*.svg
831 python3 scripts/svg_finalize/embed_icons.py --dry-run svg_output/*.svg
832 ```
833
834 Replaces `<use data-icon="chunk-filled/name" .../>`, `<use data-icon="tabler-filled/name" .../>` and `<use data-icon="tabler-outline/name" .../>` placeholders with actual SVG path elements. Use for manual icon embedding checks outside `finalize_svg.py`.
835
836 ## SVG Compatibility Contract
837
838 The always-on SVG authoring contract lives in
839 [`shared-standards-core.md`](../../references/shared-standards-core.md), with
840 advanced effects, native data objects, and structured PPTX metadata owned by
841 their conditionally loaded modules. This tool guide does not repeat accepted
842 syntax, rejected constructs, or conditional limits.
843
844 `svg_quality_checker.py` validates source SVG before finalization.
845 `finalize_svg.py` and native export apply the preprocessing required by that
846 contract, while native conversion fails on unsupported visual elements rather
847 than silently dropping them.
848
848 lines MARKDOWN