| 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> <empty_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 refuses a non-empty destination and stages the whole result |
| 380 | before atomic publication, so a failed preflight cannot leave a partial |
| 381 | template. |
| 382 | |
| 383 | Materialization preserves source page order and emits one definition-only |
| 384 | `layout_<layout_key>.svg` for every source Layout unused by all source Slides. |
| 385 | It mechanically expands fixed Master/Layout group wrappers into direct atoms, |
| 386 | rehydrates only unchanged converter-supported Slide-local/slot refs, keeps the |
| 387 | current SVG fallback for edited refs, preserves explicit text hard breaks, and |
| 388 | removes every IR-only source ref. Imported axis-flipped groups retain their |
| 389 | geometry reflection while descendant SVG text receives a matching |
| 390 | counter-reflection, preserving PowerPoint's upright glyph appearance in browser |
| 391 | previews. Supported opaque `p:txBody`, |
| 392 | relationship-free `p:style`, and `a:custGeom` payloads are deduplicated into |
| 393 | `templates/native_payloads.json.gz`. Repeated native restoration attributes |
| 394 | are stored there as short `data-pptx-native-ref` records; page and |
| 395 | imported-vector SVGs retain only those record ids and content-hash payload |
| 396 | references. The native record referenced by an imported text placeholder |
| 397 | carrier owns its authoritative source frame, so the Slide-local frame can |
| 398 | differ from reusable Layout bounds without restoring long exact coordinates |
| 399 | inline. Structural Master/Layout, placeholder, layer, and editable-object |
| 400 | fields remain inline. Source `p:sldLayout@showMasterSp` and |
| 401 | `p:sld@showMasterSp` facts become canonical root |
| 402 | `data-pptx-show-master-shapes` and |
| 403 | `data-pptx-show-inherited-shapes` booleans. |
| 404 | |
| 405 | Checker, template-structure validation, and export hydrate both store layers in |
| 406 | memory; legacy inline payload and v1 payload-only stores remain readable. |
| 407 | |
| 408 | The published `ppt-master.template-execution-manifest.v1` roster points to one |
| 409 | compact `ppt-master.template-text-slots.v2-min` sidecar per prototype. Each text |
| 410 | slot contains only `selector`, `role`, `current_text`, `text_segments`, and |
| 411 | `tspan_count`; a top-level tool hash covers its selectors and immutable |
| 412 | text/tspan topology and attributes. These records are deterministic tool |
| 413 | diagnostics, not page-authoring inputs. Page-context emits only the complete |
| 414 | prototype's path and SHA for that reference, so the model reads the SVG once |
| 415 | per execution context and reuses it until the SHA changes. The model chooses |
| 416 | semantics and edits only existing visible text values, while checker and |
| 417 | structured export validate output attributes, text/tspan topology, and |
| 418 | referenced-resource hashes against |
| 419 | the prototype. |
| 420 | |
| 421 | The output routes reusable vectors once to `icons/imported/`, bitmaps to |
| 422 | `images/`, and other referenced files to `templates/assets/`. The JSON report |
| 423 | reports payload occurrence, native-record, unique-byte, and compressed-store |
| 424 | counts and is written to stdout only. The command intentionally does not create |
| 425 | `templates/design_spec.md`; Template_Designer writes the package-specific rules |
| 426 | and page roster after materialization. This compiler is for Type A mirror materialization, |
| 427 | not `standard` / `fidelity`, loose Type B SVGs, ordinary generation, finalize, |
| 428 | or export. |
| 429 | |
| 430 | ## `extract_svg_pictures.py` |
| 431 | |
| 432 | Normalize one deliberately selected complex SVG object into one PowerPoint |
| 433 | picture. The command accepts exact `<g id>` values only, writes each group as a |
| 434 | tight standalone SVG asset, embeds its local image/CSS dependencies, and |
| 435 | replaces the source group at the same parent index with one `<image>`. Native |
| 436 | export therefore emits one `p:pic` backed by SVG media. |
| 437 | |
| 438 | ```bash |
| 439 | python3 scripts/extract_svg_pictures.py \ |
| 440 | "<workspace>/authoring-svg/<layered_svg_file>.svg" \ |
| 441 | --select "<group_id>" \ |
| 442 | --resource-root "<workspace>" \ |
| 443 | --images-dir "<workspace>/picture-assets" \ |
| 444 | --inplace |
| 445 | ``` |
| 446 | |
| 447 | Imported PowerPoint groups normally provide `data-pptx-frame`, which is used |
| 448 | as the picture bounds. For a large standalone SVG without frame metadata, the |
| 449 | tool measures the selected group with Playwright; use repeated |
| 450 | `--bounds ID=x,y,width,height` values when browser measurement is unavailable |
| 451 | or when effect overflow needs an explicit frame. `--padding` expands the |
| 452 | chosen bounds. The generated `*_picture_asset_inventory.json` records the |
| 453 | bounds source, asset hash, copied definition ids, and embedded local resources. |
| 454 | Nested selections are accepted only through metadata-only `<g>` ancestors. |
| 455 | When an ancestor carries a transform, style, clip, opacity, or other visual |
| 456 | attribute, select that outer group instead; this prevents applying the ancestor |
| 457 | effect once inside the SVG asset and again to the replacement `<image>`. |
| 458 | Scripts, `foreignObject`, SVG animation, remote resources, and external SVG |
| 459 | fragment references fail closed; local image/CSS resources must stay inside |
| 460 | the declared `--resource-root` and are embedded into the asset. |
| 461 | An in-place rewrite inside an authoring bundle refreshes |
| 462 | `authoring_summary.json` automatically. |
| 463 | |
| 464 | This operation belongs only to an explicit `create-template` normalization |
| 465 | decision in `standard` or `fidelity` mode. It does not choose groups, detect |
| 466 | repetition, infer a Master/Layout, or run during ordinary import, free |
| 467 | generation, mirror materialization, finalize, or export. Placeholder, native |
| 468 | single-shape, table/chart, icon-placeholder, and authored-preset groups are |
| 469 | rejected because they already own a different semantic route. |
| 470 | |
| 471 | Do not confuse this tool with `extract_svg_assets.py`: |
| 472 | |
| 473 | - `extract_svg_assets.py` is a model-readability optimization. It replaces |
| 474 | heuristic vector runs with `<use data-icon>`, then re-inlines them before |
| 475 | export so the PPTX still contains native shapes. |
| 476 | - `extract_svg_pictures.py` is an explicit representation change. It replaces |
| 477 | only named groups with `<image>`, so each result intentionally remains one |
| 478 | editable PowerPoint picture rather than individually editable paths. |
| 479 | |
| 480 | ## Recommended Pipeline |
| 481 | |
| 482 | Run these steps one at a time. Wait for each command to exit successfully before |
| 483 | starting the next command. |
| 484 | |
| 485 | When the effective Speaker Notes outcome in `design_spec.md §I` is enabled, run: |
| 486 | |
| 487 | ```bash |
| 488 | python3 scripts/total_md_split.py <project_path> |
| 489 | ``` |
| 490 | |
| 491 | After `total_md_split.py` exits successfully, run: |
| 492 | |
| 493 | ```bash |
| 494 | python3 scripts/finalize_svg.py <project_path> |
| 495 | ``` |
| 496 | |
| 497 | After `finalize_svg.py` exits successfully, run: |
| 498 | |
| 499 | ```bash |
| 500 | python3 scripts/svg_to_pptx.py <project_path> |
| 501 | ``` |
| 502 | |
| 503 | When Speaker Notes is disabled, skip `total_md_split.py` and use |
| 504 | `python3 scripts/svg_to_pptx.py <project_path> --no-notes` for the final |
| 505 | command. This prevents stale files under `notes/` from being embedded. |
| 506 | |
| 507 | Do not start another post-processing command while the current command is still |
| 508 | running. The canonical gates and success criteria are owned by |
| 509 | [`generate-pptx.md`](../../workflows/generate-pptx.md) Step 7. |
| 510 | |
| 511 | ## `finalize_svg.py` |
| 512 | |
| 513 | Unified post-processing entry point. This is the preferred way to run SVG cleanup. |
| 514 | |
| 515 | It aggregates: |
| 516 | - `embed_icons.py` |
| 517 | - static same-document `<use>` expansion from `svg_to_pptx/use_expander.py` |
| 518 | - `align_embed_images.py` (`crop-images` / `fix-aspect` / `embed-images` aliases route here) |
| 519 | - `flatten_tspan.py` |
| 520 | |
| 521 | `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. |
| 522 | |
| 523 | ## `svg_to_pptx.py` |
| 524 | |
| 525 | Convert project SVGs into PPTX. |
| 526 | |
| 527 | ```bash |
| 528 | python3 scripts/svg_to_pptx.py <project_path> |
| 529 | # Explicit compact image export: |
| 530 | python3 scripts/svg_to_pptx.py <project_path> --image-sizing display --image-scale 2 --image-quality 85 |
| 531 | # Force original image bytes: |
| 532 | python3 scripts/svg_to_pptx.py <project_path> --no-image-optimize |
| 533 | python3 scripts/svg_to_pptx.py <project_path> --native-charts-and-tables |
| 534 | python3 scripts/svg_to_pptx.py <project_path> --pptx-structure structured # deck/layout template override |
| 535 | python3 scripts/svg_to_pptx.py <project_path> --pptx-structure flat # free-design/brand-only override |
| 536 | # Template-import visual round-trip diagnostic only: |
| 537 | python3 scripts/svg_to_pptx.py <template_import_output> -s svg-flat |
| 538 | # Post-processed-source comparison diagnostic only (never a release export): |
| 539 | python3 scripts/svg_to_pptx.py <project_path> -s final |
| 540 | python3 scripts/svg_to_pptx.py <project_path> --no-notes |
| 541 | python3 scripts/svg_to_pptx.py <project_path> -t none |
| 542 | python3 scripts/svg_to_pptx.py <project_path> --auto-advance 3 |
| 543 | python3 scripts/svg_to_pptx.py <project_path> --animation mixed --animation-duration 0.8 |
| 544 | python3 scripts/svg_to_pptx.py <project_path> --reflow-text # opt-in PowerPoint reflow |
| 545 | python3 scripts/svg_to_pptx.py <project_path> --no-merge # one text frame per visual line |
| 546 | python3 scripts/svg_to_pptx.py <project_path> --recorded-narration audio |
| 547 | python3 scripts/svg_to_pptx.py <project_path> --recorded-narration audio --animation-config animations.json |
| 548 | python3 scripts/svg_to_pptx.py <project_path> --recorded-narration audio --no-animations |
| 549 | ``` |
| 550 | |
| 551 | Native image export defaults to `--image-sizing cap`: it preserves source bytes |
| 552 | when no resize or EXIF geometry normalization is required, and re-encodes only |
| 553 | images that require one of those transformations. The `display` command above |
| 554 | is an explicit compact export; `--no-image-optimize` disables all native image |
| 555 | optimization and forces original bytes. |
| 556 | |
| 557 | The normal command reads `pptx_structure.mode` from `spec_lock.md`. For legacy |
| 558 | projects whose lock exists but predates that field, export emits one compatibility |
| 559 | warning and uses `flat`; no SVG regeneration is required. A missing `spec_lock.md`, |
| 560 | an explicit legacy/unknown mode, or a requested `structured` export without an |
| 561 | explicit current structured contract remains blocking. |
| 562 | |
| 563 | Explicit direct generation may use the |
| 564 | [`quick-generate`](../../workflows/profiles/quick-generate.md) profile after the |
| 565 | current agent has converted/read sources, researched identified factual gaps, |
| 566 | and prepared the required images, icons, formulas, and resource manifests as |
| 567 | needed. That profile skips Strategist, Confirm UI, `design_spec.md`, and |
| 568 | `spec_lock.md`; it does not skip the resources required by the authored pages. |
| 569 | After the complete SVG roster exists, run its lockless final checker, then |
| 570 | export: |
| 571 | |
| 572 | ```bash |
| 573 | python3 scripts/svg_quality_checker.py <project_path> \ |
| 574 | --quick-generate --stage final --json |
| 575 | python3 scripts/svg_to_pptx.py <project_path> --quick-generate |
| 576 | ``` |
| 577 | |
| 578 | This direct-export flag takes `svg_output/` as its authored page source, resolves |
| 579 | valid project-local resources referenced by those pages, infers one consistent |
| 580 | canvas, uses flat converter-default package scaffolding, and does not read or |
| 581 | require `spec_lock.md`. Notes, motion, narration, native objects, conversion |
| 582 | trace, and other ordinary exporter capabilities remain available; notes, |
| 583 | custom object animation, and narration start off in Quick and may be enabled |
| 584 | when needed. The exporter refuses a missing, blocking, non-final, or stale |
| 585 | Quick final report before PPTX creation. Default-path output retains the normal |
| 586 | postflight report and `backup/` snapshot; explicit `-o` retains the ordinary |
| 587 | no-backup behavior. Existing source, analysis, image/icon/formula, and |
| 588 | resource-manifest artifacts remain untouched. |
| 589 | |
| 590 | For generated-project narration, follow the |
| 591 | [`generate-audio`](../../workflows/stages/generate-audio.md) stage. It owns voice |
| 592 | selection, audio generation, and the narrated re-export workflow. |
| 593 | |
| 594 | Behavior: |
| 595 | - Default output (either Generate profile, no `-o`): |
| 596 | - `exports/<project_name>_<timestamp>.pptx` — native editable pptx (canonical output) |
| 597 | - `validation/<project_name>_<timestamp>.report.json` — package postflight, quality-gate linkage, unresolved resource audit, and published part counts |
| 598 | - `backup/<timestamp>/svg_output/` — copy of authored SVG source for re-export without re-running the LLM |
| 599 | - `exports/` contains only final PPTX deliverables; machine-readable quality and postflight reports belong in `validation/`. |
| 600 | - 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. |
| 601 | - 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. |
| 602 | - Postflight reruns ZIP integrity and published Slide count. Internal relationships, |
| 603 | structured-package validation, transitions, and animations are enforced before the |
| 604 | builder publishes the PPTX and are reported as `enforced-at-build`, not as repeated |
| 605 | postflight checks. |
| 606 | - `font_portability` warns when a complete font stack has no concrete family or when |
| 607 | the converter resolves its Latin / East Asian role to a typeface that normally |
| 608 | requires a custom installation. A recommended stack such as |
| 609 | `"Microsoft YaHei", Arial, sans-serif` does not warn merely because it ends with a |
| 610 | generic fallback. |
| 611 | - Multiline text export modes: |
| 612 | - 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. |
| 613 | - `--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. |
| 614 | - `--no-merge`: each dy-stacked line becomes an independent frame with its own placement. |
| 615 | - 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. |
| 616 | - 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. |
| 617 | - `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. |
| 618 | - On every SVG-authoring route, each file in `svg_output/` is the complete visible |
| 619 | page-design source. Templates and locks may guide authoring, but finalize/export |
| 620 | never use them to overlay visible content missing from the SVG. Notes, animation, |
| 621 | narration, transitions, and direct native-PPTX workflows keep their separate |
| 622 | inputs and package-level processing. |
| 623 | - 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. |
| 624 | - 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. |
| 625 | - 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. |
| 626 | - 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. |
| 627 | - Every visible direct root `<g>` requires root-coordinate `data-pptx-bounds`; nested bounds are ignored. Frame/native metadata never replaces it; 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. |
| 628 | - Missing root bounds fails on final pages/templates and under `--template-mode`; references warn until adapted. |
| 629 | - 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. |
| 630 | - 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. |
| 631 | - 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. |
| 632 | - 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. |
| 633 | - Native output uses content-hash media filenames, so identical images are reused and different images cannot overwrite each other by sharing a basename. |
| 634 | - `[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. |
| 635 | - 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. |
| 636 | - `--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. |
| 637 | - 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. |
| 638 | - 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. |
| 639 | - 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. |
| 640 | - 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. |
| 641 | - Authored SVG clip-path restrictions remain. Crop wrappers use an |
| 642 | overflow-hidden viewport; preview-safe shape clips target the inner image in |
| 643 | viewBox coordinates, while legacy imported wrapper clips remain compatible. |
| 644 | Both map to native picture crop/geometry when possible. |
| 645 | - The default Generate flow embeds speaker notes automatically unless `--no-notes` is used; Quick Generate defaults them off and enables them with `--with-notes` |
| 646 | - Recorded narration is opt-in: |
| 647 | - `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/` |
| 648 | - 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 |
| 649 | - `--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 |
| 650 | - `--recorded-narration audio` keeps speaker notes, embeds each matching audio file, and writes slide auto-advance timings from audio duration |
| 651 | - When either animation sidecar exists, narrated export defaults to `<project>/narration_animations.json`; a canonical `animations.json` without that derived file remains a blocking synchronization error |
| 652 | - Without animation sidecars, Generate narration reads base-report deck motion via `--inherit-motion-from`; direct low-level omission keeps legacy `fade` / no object builds. Use `--animation-config animations.json` for canonical animation, or `--no-animations` to remove object/page motion while retaining narration timings |
| 653 | - Non-narrated export keeps the existing optional `<project>/animations.json` default |
| 654 | - 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 |
| 655 | - `--narration-audio-dir audio` is the lower-level embedding path: it embeds whatever files match and allows partial audio coverage |
| 656 | - Either narration flag names the default-flow export `<project_name>_<timestamp>_narrated.pptx`, telling it apart from silent exports in the same directory |
| 657 | - This is intended for direct PowerPoint video export with "Use recorded timings and narrations" |
| 658 | - Long-audio import and automatic long-audio splitting are not supported; keep narration assets page-level |
| 659 | - 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` |
| 660 | - Page transitions are controlled by `-t/--transition`; per-element object animations are controlled by `-a/--animation` |
| 661 | - 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 |
| 662 | - 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 |
| 663 | - 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 |
| 664 | - `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 |
| 665 | - Flat SVG roots without top-level groups fall back to at most 8 visible primitives; beyond that, animation is skipped on the slide |
| 666 | - Per-element animation defaults to `none`. `auto` is opt-in (`-a auto`) and maps |
| 667 | generic entrance effects from the group's SVG id: information-dense elements |
| 668 | get a stable entrance (chart→wipe, card-/step-/pillar-→fly, |
| 669 | title/takeaway→fade); image-like and unmatched ids rotate through bounded |
| 670 | entrance pools. |
| 671 | - `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 |
| 672 | - `--animation-duration` controls the inherited per-row schedule length (default |
| 673 | `0.4`); scalable native effects preserve internal timing ratios, while |
| 674 | instantaneous presets keep their authored duration. `--animation-stagger` |
| 675 | supplies the default gap between successive non-trigger-shape rows in |
| 676 | `after-previous` mode (default `0.5`) |
| 677 | - 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 |
| 678 | - 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` |
| 679 | - 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 |
| 680 | - 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 |
| 681 | - 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 |
| 682 | - The full registry, OOXML rules, and compatibility boundary are documented in [`pptx-animations.md`](./pptx-animations.md) |
| 683 | |
| 684 | Dependency: |
| 685 | |
| 686 | ```bash |
| 687 | pip install python-pptx |
| 688 | ``` |
| 689 | |
| 690 | ## `total_md_split.py` |
| 691 | |
| 692 | Split `total.md` into per-slide note files. |
| 693 | |
| 694 | ```bash |
| 695 | python3 scripts/total_md_split.py <project_path> |
| 696 | python3 scripts/total_md_split.py <project_path> -o <output_directory> |
| 697 | python3 scripts/total_md_split.py <project_path> -q |
| 698 | ``` |
| 699 | |
| 700 | Requirements: |
| 701 | - Each section begins with `# ` |
| 702 | - Heading text matches the SVG filename |
| 703 | - Sections are separated by `---` |
| 704 | |
| 705 | ## `svg_quality_checker.py` |
| 706 | |
| 707 | Validate SVG technical compliance. |
| 708 | |
| 709 | ```bash |
| 710 | python3 scripts/svg_quality_checker.py examples/project/svg_output/01_cover.svg |
| 711 | python3 scripts/svg_quality_checker.py examples/project/svg_output |
| 712 | python3 scripts/svg_quality_checker.py examples/project |
| 713 | python3 scripts/svg_quality_checker.py examples/project --stage first-page |
| 714 | python3 scripts/svg_quality_checker.py examples/project --stage final --json |
| 715 | python3 scripts/svg_quality_checker.py examples/project --format ppt169 |
| 716 | python3 scripts/svg_quality_checker.py --all examples |
| 717 | python3 scripts/svg_quality_checker.py examples/project --export |
| 718 | python3 scripts/svg_quality_checker.py path/to/template/templates --template-mode |
| 719 | ``` |
| 720 | |
| 721 | Checks include: |
| 722 | - `viewBox` |
| 723 | - banned elements |
| 724 | - paint compatibility: unsupported values error; supported non-default spellings such as `rgba()` receive non-blocking recommendations for `#RRGGBB` plus explicit alpha |
| 725 | - line-break structure |
| 726 | - explicit Master/Layout/slot structure for reusable templates |
| 727 | - duplicate empty Layout contracts under different keys |
| 728 | |
| 729 | Warnings are advisory: they require no modification or acknowledgement and do |
| 730 | not affect the command's zero exit status. Only errors block the quality gate. |
| 731 | |
| 732 | `--stage first-page` resolves only the first authored SVG and permits an incomplete |
| 733 | future page roster. `--stage final` checks the complete project. With `--json`, |
| 734 | the final stage writes `validation/svg_quality_report.json`, while the first-page |
| 735 | stage writes `validation/svg_quality_first_page_report.json` so it cannot overwrite |
| 736 | the release gate (or use `--json-output`). The report separates |
| 737 | release failures (`blocking`), changed/new advisories (`introduced`), |
| 738 | prototype-identical diagnostics (`inherited`), and source-conversion losses |
| 739 | (`source-import`). It also fingerprints every checked SVG so postflight cannot |
| 740 | mistake a stale report for the current export gate. On a successful run, use the |
| 741 | checker exit status and terminal summary; do not load the complete JSON unless a |
| 742 | failure investigation or explicit audit requires targeted fields. |
| 743 | |
| 744 | Template mode accepts the same compact canonical preset groups as generated |
| 745 | pages: one atomic `<g data-pptx-authoring="preset">` with direct visible paths. |
| 746 | It validates those paths dynamically against the locked registry and does not |
| 747 | require an import-style carrier, preview wrapper, fingerprint, or a separate |
| 748 | source-payload opt-in marker. Exact syntax remains owned by the linked |
| 749 | standards rather than this pipeline overview. |
| 750 | |
| 751 | ## `svg_position_calculator.py` |
| 752 | |
| 753 | Analyze and review supported chart coordinates after SVG generation. |
| 754 | |
| 755 | 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. |
| 756 | |
| 757 | ### Calculate expected coordinates |
| 758 | |
| 759 | ```bash |
| 760 | python3 scripts/svg_position_calculator.py calc bar --data "A:185,B:142" --area "130,155,1200,480" --bar-width 120 |
| 761 | python3 scripts/svg_position_calculator.py calc line --data "0:50,10:80,20:120" --area "120,120,1200,600" --y-range "0,150" |
| 762 | python3 scripts/svg_position_calculator.py calc pie --data "A:35,B:25,C:20" --center "420,400" --radius 200 |
| 763 | python3 scripts/svg_position_calculator.py calc grid --rows 2 --cols 3 --area "50,150,1230,670" |
| 764 | ``` |
| 765 | |
| 766 | For an area chart, use the line output as the top boundary: |
| 767 | |
| 768 | ```svg |
| 769 | M first_x,first_y ... L last_x,last_y L last_x,y_max L first_x,y_max Z |
| 770 | ``` |
| 771 | |
| 772 | 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. |
| 773 | |
| 774 | ### Analyze (inspect existing SVG) |
| 775 | |
| 776 | ```bash |
| 777 | python3 scripts/svg_position_calculator.py analyze <svg_file> |
| 778 | ``` |
| 779 | |
| 780 | Use this after SVG generation to inspect existing SVG geometry when manual comparison needs more context. |
| 781 | |
| 782 | ## Advanced Standalone Tools |
| 783 | |
| 784 | ### `flatten_tspan.py` |
| 785 | |
| 786 | ```bash |
| 787 | python3 scripts/svg_finalize/flatten_tspan.py examples/<project>/svg_output |
| 788 | python3 scripts/svg_finalize/flatten_tspan.py path/to/input.svg path/to/output.svg |
| 789 | ``` |
| 790 | |
| 791 | ### `align_embed_images.py` |
| 792 | |
| 793 | ```bash |
| 794 | python3 scripts/svg_finalize/align_embed_images.py path/to/slide.svg |
| 795 | python3 scripts/svg_finalize/align_embed_images.py --dry-run path/to/slide.svg |
| 796 | ``` |
| 797 | |
| 798 | Use for rare single-file diagnostics when image `slice` / `meet` alignment and |
| 799 | Base64 embedding must be inspected outside `finalize_svg.py`. In normal project |
| 800 | runs, use `python3 scripts/finalize_svg.py <project_path>`; the old |
| 801 | `crop-images`, `fix-aspect`, and `embed-images` names remain accepted only as |
| 802 | `finalize_svg.py --only` aliases for the merged `align-images` step. |
| 803 | |
| 804 | ### `embed_icons.py` |
| 805 | |
| 806 | ```bash |
| 807 | python3 scripts/svg_finalize/embed_icons.py output.svg |
| 808 | python3 scripts/svg_finalize/embed_icons.py svg_output/*.svg |
| 809 | python3 scripts/svg_finalize/embed_icons.py --dry-run svg_output/*.svg |
| 810 | ``` |
| 811 | |
| 812 | 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`. |
| 813 | |
| 814 | ## SVG Compatibility Contract |
| 815 | |
| 816 | The always-on SVG authoring contract lives in |
| 817 | [`shared-standards-core.md`](../../references/shared-standards-core.md), with |
| 818 | advanced effects, native data objects, and structured PPTX metadata owned by |
| 819 | their conditionally loaded modules. This tool guide does not repeat accepted |
| 820 | syntax, rejected constructs, or conditional limits. |
| 821 | |
| 822 | `svg_quality_checker.py` validates source SVG before finalization. |
| 823 | `finalize_svg.py` and native export apply the preprocessing required by that |
| 824 | contract, while native conversion fails on unsupported visual elements rather |
| 825 | than silently dropping them. |
| 826 |