dsh-office-kit 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CAPABILITY.md +147 -68
- package/README.md +3 -1
- package/assets/skills/excel-live-control/SKILL.md +234 -0
- package/assets/skills/excel-live-control/container_tools/live_excel.py +1589 -0
- package/assets/skills/excel-live-control/domain_guidance/financial_models.md +86 -0
- package/assets/skills/excel-live-control/domain_guidance/healthcare.md +58 -0
- package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +53 -0
- package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +53 -0
- package/assets/skills/excel-live-control/references/office-automation.md +207 -0
- package/assets/skills/excel-live-control/references/read_only_qna.md +59 -0
- package/assets/skills/excel-live-control/style_guidelines.md +87 -0
- package/assets/skills/excel-live-control/workflows/create_workflows.md +78 -0
- package/assets/skills/excel-live-control/workflows/edit_workflows.md +72 -0
- package/assets/skills/office-documents/SKILL.md +90 -77
- package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +86 -0
- package/assets/skills/office-documents/ooxml/comments.md +40 -0
- package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +66 -0
- package/assets/skills/office-documents/ooxml/rels_and_content_types.md +62 -0
- package/assets/skills/office-documents/ooxml/tracked_changes.md +53 -0
- package/assets/skills/office-documents/references/docx-structure.md +83 -0
- package/assets/skills/office-documents/references/template-elicitation.md +49 -0
- package/assets/skills/office-documents/references/writing-quality.md +49 -0
- package/assets/skills/office-documents/render_docx.py +92 -0
- package/assets/skills/office-documents/scripts/a11y_audit.py +194 -0
- package/assets/skills/office-documents/scripts/accept_tracked_changes.py +151 -0
- package/assets/skills/office-documents/scripts/add_tracked_replacements.py +148 -0
- package/assets/skills/office-documents/scripts/append_blocks.py +108 -0
- package/assets/skills/office-documents/scripts/apply_template_styles.py +104 -0
- package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +102 -0
- package/assets/skills/office-documents/scripts/comments_add.py +136 -0
- package/assets/skills/office-documents/scripts/comments_apply_patch.py +114 -0
- package/assets/skills/office-documents/scripts/comments_extract.py +85 -0
- package/assets/skills/office-documents/scripts/comments_strip.py +74 -0
- package/assets/skills/office-documents/scripts/content_controls.py +192 -0
- package/assets/skills/office-documents/scripts/core_properties.py +124 -0
- package/assets/skills/office-documents/scripts/docx_kit.py +587 -0
- package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +225 -0
- package/assets/skills/office-documents/scripts/docx_table_to_csv.py +99 -0
- package/assets/skills/office-documents/scripts/fields_materialize.py +66 -0
- package/assets/skills/office-documents/scripts/fields_report.py +59 -0
- package/assets/skills/office-documents/scripts/flatten_ref_fields.py +62 -0
- package/assets/skills/office-documents/scripts/footnotes_report.py +77 -0
- package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +96 -0
- package/assets/skills/office-documents/scripts/heading_audit.py +81 -0
- package/assets/skills/office-documents/scripts/images_audit.py +80 -0
- package/assets/skills/office-documents/scripts/insert_note.py +126 -0
- package/assets/skills/office-documents/scripts/insert_ref_fields.py +90 -0
- package/assets/skills/office-documents/scripts/insert_toc.py +84 -0
- package/assets/skills/office-documents/scripts/internal_nav.py +146 -0
- package/assets/skills/office-documents/scripts/make_fixtures.py +169 -0
- package/assets/skills/office-documents/scripts/merge_docx_append.py +239 -0
- package/assets/skills/office-documents/scripts/privacy_scrub.py +131 -0
- package/assets/skills/office-documents/scripts/redact_docx.py +113 -0
- package/assets/skills/office-documents/scripts/render_and_diff.py +101 -0
- package/assets/skills/office-documents/scripts/replace_text.py +154 -0
- package/assets/skills/office-documents/scripts/section_audit.py +91 -0
- package/assets/skills/office-documents/scripts/set_protection.py +92 -0
- package/assets/skills/office-documents/scripts/style_lint.py +111 -0
- package/assets/skills/office-documents/scripts/style_normalize.py +103 -0
- package/assets/skills/office-documents/scripts/table_geometry.py +167 -0
- package/assets/skills/office-documents/scripts/watermark_add.py +48 -0
- package/assets/skills/office-documents/scripts/watermark_audit_remove.py +67 -0
- package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +117 -0
- package/assets/skills/office-documents/tasks/accessibility_a11y.md +45 -0
- package/assets/skills/office-documents/tasks/captions_crossrefs.md +49 -0
- package/assets/skills/office-documents/tasks/clean_tracked_changes.md +49 -0
- package/assets/skills/office-documents/tasks/comments_manage.md +59 -0
- package/assets/skills/office-documents/tasks/compare_diff.md +40 -0
- package/assets/skills/office-documents/tasks/create_edit.md +107 -0
- package/assets/skills/office-documents/tasks/equations.md +59 -0
- package/assets/skills/office-documents/tasks/fields_update.md +44 -0
- package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +50 -0
- package/assets/skills/office-documents/tasks/footnotes_endnotes.md +38 -0
- package/assets/skills/office-documents/tasks/forms_content_controls.md +48 -0
- package/assets/skills/office-documents/tasks/headings_numbering.md +55 -0
- package/assets/skills/office-documents/tasks/images_figures.md +63 -0
- package/assets/skills/office-documents/tasks/multi_doc_merge.md +43 -0
- package/assets/skills/office-documents/tasks/navigation_internal_links.md +38 -0
- package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +45 -0
- package/assets/skills/office-documents/tasks/protection_restrict_editing.md +41 -0
- package/assets/skills/office-documents/tasks/read_review.md +50 -0
- package/assets/skills/office-documents/tasks/redaction_anonymization.md +42 -0
- package/assets/skills/office-documents/tasks/sections_layout.md +54 -0
- package/assets/skills/office-documents/tasks/style_lint_normalize.md +44 -0
- package/assets/skills/office-documents/tasks/tables_spreadsheets.md +64 -0
- package/assets/skills/office-documents/tasks/templates_style_packs.md +53 -0
- package/assets/skills/office-documents/tasks/toc_workflow.md +39 -0
- package/assets/skills/office-documents/tasks/verify_render.md +50 -0
- package/assets/skills/office-documents/tasks/watermarks_background.md +35 -0
- package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +47 -0
- package/assets/skills/office-documents/troubleshooting/run_splitting.md +36 -0
- package/assets/skills/office-pdf/SKILL.md +73 -56
- package/assets/skills/office-pdf/references/form-fields.md +56 -0
- package/assets/skills/office-pdf/references/geometry.md +44 -0
- package/assets/skills/office-pdf/references/layout-quality.md +41 -0
- package/assets/skills/office-pdf/references/limits.md +58 -0
- package/assets/skills/office-pdf/scripts/extract_text.py +172 -0
- package/assets/skills/office-pdf/scripts/forms.py +556 -0
- package/assets/skills/office-pdf/scripts/metadata.py +161 -0
- package/assets/skills/office-pdf/scripts/page_ops.py +198 -0
- package/assets/skills/office-pdf/scripts/redact_region.py +347 -0
- package/assets/skills/office-pdf/scripts/render_pages.py +207 -0
- package/assets/skills/office-pdf/scripts/split_pages.py +155 -0
- package/assets/skills/office-pdf/scripts/stamp_overlay.py +199 -0
- package/assets/skills/office-pdf/tasks/annotations.md +98 -0
- package/assets/skills/office-pdf/tasks/create.md +75 -0
- package/assets/skills/office-pdf/tasks/edit-pages.md +71 -0
- package/assets/skills/office-pdf/tasks/forms.md +67 -0
- package/assets/skills/office-pdf/tasks/read.md +62 -0
- package/assets/skills/office-pdf/tasks/redact.md +72 -0
- package/assets/skills/office-pdf/tasks/stamp.md +60 -0
- package/assets/skills/office-presentations/SKILL.md +141 -87
- package/assets/skills/office-presentations/container_tools/add_slides.py +136 -0
- package/assets/skills/office-presentations/container_tools/core_properties.py +120 -0
- package/assets/skills/office-presentations/container_tools/create_montage.py +126 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +285 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +157 -0
- package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +144 -0
- package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +199 -0
- package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +212 -0
- package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +345 -0
- package/assets/skills/office-presentations/container_tools/preview_render.py +375 -0
- package/assets/skills/office-presentations/container_tools/render_slides.py +139 -0
- package/assets/skills/office-presentations/container_tools/replace_text.py +168 -0
- package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +229 -0
- package/assets/skills/office-presentations/domain_guidance/finance.md +66 -0
- package/assets/skills/office-presentations/recipes/README.md +71 -0
- package/assets/skills/office-presentations/recipes/autolayout.md +88 -0
- package/assets/skills/office-presentations/recipes/connectors.md +83 -0
- package/assets/skills/office-presentations/recipes/images.md +80 -0
- package/assets/skills/office-presentations/recipes/master-and-layouts.md +127 -0
- package/assets/skills/office-presentations/recipes/native-charts.md +87 -0
- package/assets/skills/office-presentations/recipes/rich-text-runs.md +86 -0
- package/assets/skills/office-presentations/recipes/speaker-notes.md +65 -0
- package/assets/skills/office-presentations/recipes/tables.md +88 -0
- package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +71 -0
- package/assets/skills/office-presentations/recipes/theme-tokens.md +87 -0
- package/assets/skills/office-presentations/references/authoring-workflow.md +65 -0
- package/assets/skills/office-presentations/references/finalization.md +63 -0
- package/assets/skills/office-presentations/references/native-bullets.md +78 -0
- package/assets/skills/office-presentations/references/native-evidence.md +67 -0
- package/assets/skills/office-presentations/references/style-guidelines.md +66 -0
- package/assets/skills/office-presentations/references/templates.md +76 -0
- package/assets/skills/office-presentations/routing/google_slides.md +45 -0
- package/assets/skills/office-presentations/tasks/apply-annotations.md +83 -0
- package/assets/skills/office-presentations/tasks/charts-tables-notes.md +79 -0
- package/assets/skills/office-presentations/tasks/create-deck.md +106 -0
- package/assets/skills/office-presentations/tasks/edit-deck.md +93 -0
- package/assets/skills/office-presentations/tasks/inspect-and-verify.md +107 -0
- package/assets/skills/office-presentations/tasks/template-following.md +132 -0
- package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py +176 -0
- package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +154 -0
- package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +111 -0
- package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +239 -0
- package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +185 -0
- package/assets/skills/office-spreadsheets/SKILL.md +105 -101
- package/assets/skills/office-spreadsheets/container_tools/add_chart.py +236 -0
- package/assets/skills/office-spreadsheets/container_tools/add_image.py +107 -0
- package/assets/skills/office-spreadsheets/container_tools/append_rows.py +122 -0
- package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +306 -0
- package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +398 -0
- package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +275 -0
- package/assets/skills/office-spreadsheets/container_tools/set_cells.py +135 -0
- package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +134 -0
- package/assets/skills/office-spreadsheets/container_tools/style_cells.py +133 -0
- package/assets/skills/office-spreadsheets/recipes/charts.md +66 -0
- package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +69 -0
- package/assets/skills/office-spreadsheets/recipes/data-tables.md +58 -0
- package/assets/skills/office-spreadsheets/recipes/named-ranges.md +41 -0
- package/assets/skills/office-spreadsheets/recipes/number-formats.md +72 -0
- package/assets/skills/office-spreadsheets/recipes/sparklines.md +43 -0
- package/assets/skills/office-spreadsheets/references/charts.md +63 -0
- package/assets/skills/office-spreadsheets/references/domain/financial-models.md +40 -0
- package/assets/skills/office-spreadsheets/references/domain/healthcare.md +22 -0
- package/assets/skills/office-spreadsheets/references/domain/marketing.md +28 -0
- package/assets/skills/office-spreadsheets/references/domain/research.md +22 -0
- package/assets/skills/office-spreadsheets/references/formula-patterns.md +78 -0
- package/assets/skills/office-spreadsheets/references/style-guidelines.md +69 -0
- package/assets/skills/office-spreadsheets/references/workbook-structure.md +60 -0
- package/assets/skills/office-spreadsheets/routing/google-sheets.md +36 -0
- package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +37 -0
- package/assets/skills/office-spreadsheets/tasks/annotations.md +51 -0
- package/assets/skills/office-spreadsheets/tasks/create-workbook.md +66 -0
- package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +38 -0
- package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +69 -0
- package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +38 -0
- package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +57 -0
- package/assets/skills/shared/annotation-recipes.md +252 -0
- package/assets/skills/shared/check_runtime.py +253 -0
- package/assets/skills/shared/common-workflow.md +139 -0
- package/assets/skills/shared/scripts/ensure_raster_image.py +187 -0
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/index.js +239 -89
- package/package.json +1 -1
- package/postinstall.mjs +92 -82
- package/python/office_cli.py +171 -246
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Task: edit an existing deck
|
|
2
|
+
|
|
3
|
+
Editing is mutation, and mutation lives in this pack's tools, not in the helper
|
|
4
|
+
CLI: `container_tools/replace_text.py`, `add_slides.py`, `core_properties.py`,
|
|
5
|
+
plus the recipes for everything structural.
|
|
6
|
+
|
|
7
|
+
**Never write over the original.** Every tool defaults to
|
|
8
|
+
`<original>-edited.pptx`; keep that default unless the user names a path.
|
|
9
|
+
|
|
10
|
+
## 1. Understand the file first
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
"<python>" "<cli>" pptx read --input deck.pptx
|
|
14
|
+
"<python>" "<skill>/container_tools/inspect_presentation_layout_geometry.py" --input deck.pptx
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`pptx read` gives titles, text, tables and chart data per slide. The geometry
|
|
18
|
+
gate gives every shape's rect in inches, which is what you need to decide
|
|
19
|
+
whether a new block fits.
|
|
20
|
+
|
|
21
|
+
## 2. Text changes: run-level replace
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
"<python>" "<skill>/container_tools/replace_text.py" --input deck.pptx \
|
|
25
|
+
--find "Q4 2025" --replace "Q4 2026" --out deck-edited.pptx
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Batch form:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"replacements": [
|
|
33
|
+
{ "find": "Q4 2025", "replace": "Q4 2026" },
|
|
34
|
+
{ "find": "€", "replace": "EUR ", "match_case": true },
|
|
35
|
+
{ "find": "draft", "replace": "final", "slide": 7 }
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
"<python>" "<skill>/container_tools/replace_text.py" --input deck.pptx --spec replacements.json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Semantics that matter: a pattern only matches **inside a single run**. Text that
|
|
45
|
+
PowerPoint split for formatting ("**Q4** 2025") will not match "Q4 2025" and the
|
|
46
|
+
report lists it under `unmatched`. When a pattern cannot match, that is the
|
|
47
|
+
signal to edit that paragraph with a run-level snippet
|
|
48
|
+
([rich text runs](../recipes/rich-text-runs.md)) rather than widening the match.
|
|
49
|
+
|
|
50
|
+
Exit code 1 means a pattern matched nothing; the file is still written, so read
|
|
51
|
+
`unmatched` before delivering.
|
|
52
|
+
|
|
53
|
+
## 3. Adding slides
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
"<python>" "<skill>/container_tools/add_slides.py" --input deck.pptx --spec extra-slides.json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The spec is the same slide object as `pptx create` (`layout`, `title`,
|
|
60
|
+
`subtitle`, `bullets`, `table`, `chart`, `image`). For anything outside that
|
|
61
|
+
vocabulary, add the slide with `add_slides.py` and then place content with a
|
|
62
|
+
recipe.
|
|
63
|
+
|
|
64
|
+
## 4. Metadata
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
"<python>" "<skill>/container_tools/core_properties.py" --input deck.pptx \
|
|
68
|
+
--title "Automation Review" --author "Warehouse Ops" --subject "FY2026 capital plan"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 5. Structural changes
|
|
72
|
+
|
|
73
|
+
| Change | Route |
|
|
74
|
+
|---|---|
|
|
75
|
+
| Move or resize a shape | recipe: set `.left/.top/.width/.height` on the shape from `slide.shapes` |
|
|
76
|
+
| Restyle a family | [masters and layouts](../recipes/master-and-layouts.md) |
|
|
77
|
+
| Add notes | [speaker notes](../recipes/speaker-notes.md) |
|
|
78
|
+
| Replace a chart's data | [native charts](../recipes/native-charts.md) — edit the series, do not paste a picture |
|
|
79
|
+
| Fix a table's total | [tables](../recipes/tables.md), then the arithmetic gate |
|
|
80
|
+
| Delete a slide | `prs.slides._sldIdLst.remove(...)` after `prs.part.drop_rel(rId)` |
|
|
81
|
+
|
|
82
|
+
## 6. Verify before delivering
|
|
83
|
+
|
|
84
|
+
Deleting or inserting a slide commonly breaks nothing visible and everything
|
|
85
|
+
relational:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
"<python>" "<skill>/container_tools/inspect_presentation_package_integrity.py" --input deck-edited.pptx
|
|
89
|
+
"<python>" "<skill>/container_tools/render_slides.py" --input deck-edited.pptx --out-dir preview
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Then look at the changed pages. A text replacement that pushes a title onto a
|
|
93
|
+
third line is a layout change, not a text change.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Task: inspect and verify a deck
|
|
2
|
+
|
|
3
|
+
Verification is reading the artefact back and looking at it. The tools below
|
|
4
|
+
print one JSON object each; `passed` is the decision.
|
|
5
|
+
|
|
6
|
+
## 1. What is in the file
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
"<python>" "<cli>" pptx read --input deck.pptx
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Per slide: title, all text, tables as rows, charts with type/title/categories/
|
|
13
|
+
series, image count. This is the cheapest sanity check that the content you
|
|
14
|
+
meant to write is the content that was written.
|
|
15
|
+
|
|
16
|
+
## 2. Is the package coherent
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
"<python>" "<skill>/container_tools/inspect_presentation_package_integrity.py" --input deck.pptx
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Checks required parts, slide/master presence, dangling relationships, XML that
|
|
23
|
+
does not parse, slide count drift and duplicate parts, then reports part counts
|
|
24
|
+
(`slides`, `layouts`, `masters`, `charts`, `notesSlides`, `media`,
|
|
25
|
+
`embeddings`). A chart with an `embeddings` count of zero has lost its data.
|
|
26
|
+
|
|
27
|
+
## 3. Does the layout hold
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
"<python>" "<skill>/container_tools/inspect_presentation_layout_geometry.py" --input deck.pptx --json-out geometry.json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`fail` findings (block a delivery): shape outside the slide, degenerate size,
|
|
34
|
+
text that cannot fit its frame, table outside the slide, chart or table covered
|
|
35
|
+
by another shape. `review` findings (read them, then decide): text overlap,
|
|
36
|
+
empty placeholder, title under 28 pt, body under 14 pt, heading ending in a full
|
|
37
|
+
stop, connector across text, a chart duplicating a table's labels.
|
|
38
|
+
|
|
39
|
+
Add `--strict` to make review findings blocking for a formal deck.
|
|
40
|
+
|
|
41
|
+
## 4. Are the numbers real
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
"<python>" "<skill>/container_tools/native_chart_title_gate.py" --input deck.pptx
|
|
45
|
+
"<python>" "<skill>/container_tools/native_quantitative_chart_gate.py" --input deck.pptx --all-slides
|
|
46
|
+
"<python>" "<skill>/container_tools/native_table_arithmetic_gate.py" --input deck.pptx --contracts totals.json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- Chart titles: fails on leftover "Chart Title" or an empty title, unless the
|
|
50
|
+
chart is explicitly suppressed.
|
|
51
|
+
- Quantitative charts: every declared chart must be a real chart part with a
|
|
52
|
+
readable series, and in the default `portable` mode must carry its embedded
|
|
53
|
+
workbook.
|
|
54
|
+
- Table arithmetic: only declared totals are enforced, compared against the
|
|
55
|
+
displayed rounding; columns whose header is not additive (unit price, ratio,
|
|
56
|
+
percent) are skipped rather than summed wrongly.
|
|
57
|
+
|
|
58
|
+
`totals.json` names the contract explicitly:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{ "contracts": [{ "slide": 4, "table": 1, "total_row": 6, "columns": [1, 2] }] }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 5. Rendering and looking
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
"<python>" "<skill>/container_tools/render_slides.py" --input deck.pptx --out-dir preview
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Writes `slide-NN.png` plus `render.json`. Then `read_image` every PNG — an
|
|
71
|
+
unlooked-at render is not verification.
|
|
72
|
+
|
|
73
|
+
To see the whole deck at once:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
"<python>" "<skill>/template_following_scripts/make_contact_sheet.py" --dir preview --out sheet.png --columns 3
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
or `"<skill>/container_tools/create_montage.py` with an explicit file list.
|
|
80
|
+
|
|
81
|
+
Look for: clipped text, overlapping blocks, a title that reflowed, a chart whose
|
|
82
|
+
point is invisible at thumbnail size, a photograph stretched, a slide with no
|
|
83
|
+
title.
|
|
84
|
+
|
|
85
|
+
## 6. When nothing can render
|
|
86
|
+
|
|
87
|
+
With no Microsoft Office, WPS Office, or LibreOffice installed
|
|
88
|
+
(`office_status` → `renderers.kingsoft_wps` / `microsoft_office` / `libreoffice`),
|
|
89
|
+
`render_slides.py` falls back to `--engine draft`, a PyMuPDF geometry preview. It
|
|
90
|
+
shows placement, overflow and overlap — not typography or theme chrome. Say so
|
|
91
|
+
when reporting: "verified as a geometry preview" is honest, "looks great" from an
|
|
92
|
+
approximate render is not.
|
|
93
|
+
|
|
94
|
+
If even the preview cannot run, the delivery is unverified: report the failing
|
|
95
|
+
command, the file's slide count from `pptx read`, and the gate results, and tell
|
|
96
|
+
the user which renderer to install for a visual check.
|
|
97
|
+
|
|
98
|
+
## Gate summary
|
|
99
|
+
|
|
100
|
+
| Gate | Command | Blocking |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| Package integrity | `inspect_presentation_package_integrity.py` | yes |
|
|
103
|
+
| Layout geometry | `inspect_presentation_layout_geometry.py` | `fail` always, `review` with `--strict` |
|
|
104
|
+
| Chart titles | `native_chart_title_gate.py` | yes |
|
|
105
|
+
| Chart data | `native_quantitative_chart_gate.py` | yes for declared slides |
|
|
106
|
+
| Table arithmetic | `native_table_arithmetic_gate.py` | yes for declared contracts |
|
|
107
|
+
| Template fidelity | `template_source_fidelity_gate.py` | yes when requirements are supplied |
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Task: follow a supplied template
|
|
2
|
+
|
|
3
|
+
Use this when the user hands over a `.pptx` and says "use our template" or "match
|
|
4
|
+
this deck". The design is already decided; your job is to put content into its
|
|
5
|
+
own slides.
|
|
6
|
+
|
|
7
|
+
## 1. Inspect
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/template_following_scripts/inspect_template_deck.py" --template brand.pptx --out-dir template-inspect
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Outputs:
|
|
14
|
+
|
|
15
|
+
- `template-inspect.ndjson` — one JSON line per slide: layout name, layout
|
|
16
|
+
placeholders, slide placeholders (index, kind, rect in inches, text), shape
|
|
17
|
+
counts, text excerpt, whether the slide has full-bleed background art.
|
|
18
|
+
- `template-manifest.json` — slide size, aspect ratio, layout names, theme font
|
|
19
|
+
names, and `backgroundArtSlides`.
|
|
20
|
+
|
|
21
|
+
Read `backgroundArtSlides` before adding anything: those slides already carry
|
|
22
|
+
artwork, so a new opaque panel will cover it.
|
|
23
|
+
|
|
24
|
+
## 2. Plan
|
|
25
|
+
|
|
26
|
+
Write `frame-map.json`:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"template": "brand.pptx",
|
|
31
|
+
"newPrimitives": { "allowed": false, "justification": "" },
|
|
32
|
+
"outputSlides": [
|
|
33
|
+
{
|
|
34
|
+
"slide": 1,
|
|
35
|
+
"mode": "duplicate-slide",
|
|
36
|
+
"sourceSlide": 1,
|
|
37
|
+
"elements": [
|
|
38
|
+
{
|
|
39
|
+
"elementId": "title",
|
|
40
|
+
"action": "fill-placeholder",
|
|
41
|
+
"target": { "placeholder": "title", "text": "Automation Review" }
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"elementId": "subtitle",
|
|
45
|
+
"action": "fill-placeholder",
|
|
46
|
+
"target": { "placeholder": "subtitle", "text": "Warehouse network · FY2026" }
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"slide": 2,
|
|
52
|
+
"mode": "duplicate-slide",
|
|
53
|
+
"sourceSlide": 3,
|
|
54
|
+
"elements": [
|
|
55
|
+
{
|
|
56
|
+
"elementId": "title",
|
|
57
|
+
"action": "rewrite",
|
|
58
|
+
"target": { "placeholder": "title", "text": "Throughput rose while touches fell" }
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Allowed actions: `rewrite`, `rewrite-and-reposition`, `replace`, `delete`,
|
|
67
|
+
`keep`, `fill-placeholder`. `target.zone` is `[x, y, w, h]` in 0–1 slide
|
|
68
|
+
fractions, used with `rewrite-and-reposition`. Elements whose `role` is
|
|
69
|
+
`brand`, `logo`, `bumper`, `divider`, `section`, `chrome` or `footer` must stay
|
|
70
|
+
`keep` — they are the template.
|
|
71
|
+
|
|
72
|
+
Adding new shapes is opt-in: set `newPrimitives.allowed` and write a
|
|
73
|
+
justification. The validator rejects shapes added without it, because "one extra
|
|
74
|
+
box" is how a branded deck stops matching brand.
|
|
75
|
+
|
|
76
|
+
## 3. Validate
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
"<python>" "<skill>/template_following_scripts/validate_template_plan.py" --plan frame-map.json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Rejects: unknown action, preserved role edited, zone outside 0–1, non-sequential
|
|
83
|
+
slide numbers, missing justification for new primitives, and duplicate source
|
|
84
|
+
slides (a warning — it means two output slides share artwork, usually fine for a
|
|
85
|
+
repeated section divider, usually a copy-paste mistake otherwise).
|
|
86
|
+
|
|
87
|
+
## 4. Build
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
"<python>" "<skill>/template_following_scripts/prepare_template_starter_deck.py" --template brand.pptx --plan frame-map.json --out starter.pptx
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The starter deck keeps only the mapped source slides, in plan order, and applies
|
|
94
|
+
the actions. It refuses duplicate source slides (it reuses slides rather than
|
|
95
|
+
cloning parts). The report lists applied actions and any placeholder left empty.
|
|
96
|
+
|
|
97
|
+
From here, extend with `container_tools/add_slides.py` or the recipes when the
|
|
98
|
+
template has no family for a slide you need.
|
|
99
|
+
|
|
100
|
+
## 5. Check fidelity
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
"<python>" "<skill>/template_following_scripts/check_template_fidelity.py" --template brand.pptx --final starter.pptx --plan frame-map.json
|
|
104
|
+
"<python>" "<skill>/container_tools/template_source_fidelity_gate.py" --template brand.pptx --final starter.pptx --requirements fidelity.json
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`fidelity.json`:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"reference_slide_indices": [1, 3],
|
|
112
|
+
"mapping": { "1": 1, "2": 3 },
|
|
113
|
+
"minimum_coverage_ratio": 1.0,
|
|
114
|
+
"require_exact_dimensions": true,
|
|
115
|
+
"require_placeholder_geometry": true,
|
|
116
|
+
"require_photographic_background": false,
|
|
117
|
+
"geometry_tolerance_emu": 9144
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Both print `passed`. With no requirements, the fidelity gate is a deliberate
|
|
122
|
+
no-op pass — pass a requirements file or you have not checked anything.
|
|
123
|
+
|
|
124
|
+
## 6. Look at it
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
"<python>" "<skill>/container_tools/render_slides.py" --input starter.pptx --out-dir starter-preview
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Compare against the source slides. A filled placeholder that overflows, a title
|
|
131
|
+
in the wrong family, or a panel over the corner logo is exactly what this step
|
|
132
|
+
exists to catch.
|
package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Catch the traces an authoring pass leaves in a template-based deck.
|
|
2
|
+
|
|
3
|
+
Purpose: a deck can keep the template's layouts and still be wrong — a
|
|
4
|
+
"Click to add title" prompt left in place, the template's instruction text
|
|
5
|
+
still sitting in a body, or an opaque panel dropped exactly over the artwork
|
|
6
|
+
the design relies on. None of those are visible in a plan; all of them are
|
|
7
|
+
cheap to detect afterwards.
|
|
8
|
+
|
|
9
|
+
Exit codes: 0 clean, 1 findings, 2 unreadable input.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "container_tools"))
|
|
20
|
+
|
|
21
|
+
from ooxml_helpers import box, has_visible_text, is_picture, overlap_ratio, text_of # noqa: E402
|
|
22
|
+
|
|
23
|
+
PLACEHOLDER_PATTERNS = [
|
|
24
|
+
re.compile(pattern, re.IGNORECASE)
|
|
25
|
+
for pattern in (
|
|
26
|
+
r"^slide number$",
|
|
27
|
+
r"^date$",
|
|
28
|
+
r"^footer$",
|
|
29
|
+
r"^click to add (title|subtitle|text|body)",
|
|
30
|
+
r"^(title|subtitle|name|text|body) goes here$",
|
|
31
|
+
r"^lorem ipsum\b",
|
|
32
|
+
r"\btemplate instruction\b",
|
|
33
|
+
r"^\[.*\]$",
|
|
34
|
+
)
|
|
35
|
+
]
|
|
36
|
+
ART_COVER_MIN_RATIO = 0.9
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _has_solid_fill(shape) -> bool:
|
|
40
|
+
try:
|
|
41
|
+
from pptx.enum.dml import MSO_FILL
|
|
42
|
+
|
|
43
|
+
return shape.fill.type == MSO_FILL.SOLID
|
|
44
|
+
except Exception: # noqa: BLE001 - shapes without a fill API are never opaque panels
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _slide_size_in(slide) -> tuple[float, float]:
|
|
49
|
+
presentation = slide.part.package.presentation_part.presentation
|
|
50
|
+
return presentation.slide_width / 914400, presentation.slide_height / 914400
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check(template_path: str | Path, final_path: str | Path, plan_path: str | Path | None = None) -> dict:
|
|
54
|
+
from pptx import Presentation
|
|
55
|
+
|
|
56
|
+
template_file = Path(template_path)
|
|
57
|
+
final_file = Path(final_path)
|
|
58
|
+
for candidate in (template_file, final_file):
|
|
59
|
+
if not candidate.is_file():
|
|
60
|
+
return {"gate": "template-authoring-fidelity", "passed": False, "findings": [{"kind": "missing_file", "detail": str(candidate)}]}
|
|
61
|
+
|
|
62
|
+
template = Presentation(str(template_file))
|
|
63
|
+
final = Presentation(str(final_file))
|
|
64
|
+
template_slides = list(template.slides)
|
|
65
|
+
final_slides = list(final.slides)
|
|
66
|
+
plan = {}
|
|
67
|
+
if plan_path and Path(plan_path).is_file():
|
|
68
|
+
plan = json.loads(Path(plan_path).read_text(encoding="utf-8"))
|
|
69
|
+
|
|
70
|
+
findings: list[dict] = []
|
|
71
|
+
mapping: list[tuple[int, int]] = []
|
|
72
|
+
for entry in plan.get("outputSlides") or []:
|
|
73
|
+
mapping.append((int(entry.get("slide", len(mapping) + 1)), int(entry.get("sourceSlide", 0))))
|
|
74
|
+
if not mapping:
|
|
75
|
+
mapping = [(index, index) for index in range(1, min(len(template_slides), len(final_slides)) + 1)]
|
|
76
|
+
|
|
77
|
+
for output_index, source_index in mapping:
|
|
78
|
+
if not (1 <= output_index <= len(final_slides) and 1 <= source_index <= len(template_slides)):
|
|
79
|
+
findings.append({"kind": "mapping_out_of_range", "severity": "fail", "message": f"mapping {output_index}->{source_index} is outside the deck"})
|
|
80
|
+
continue
|
|
81
|
+
output_slide = final_slides[output_index - 1]
|
|
82
|
+
source_slide = template_slides[source_index - 1]
|
|
83
|
+
|
|
84
|
+
if output_slide.slide_layout.name != source_slide.slide_layout.name:
|
|
85
|
+
findings.append(
|
|
86
|
+
{
|
|
87
|
+
"kind": "layout_drift",
|
|
88
|
+
"severity": "review",
|
|
89
|
+
"slide": output_index,
|
|
90
|
+
"message": f"layout is '{output_slide.slide_layout.name}', template slide used '{source_slide.slide_layout.name}'",
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
for shape in output_slide.shapes:
|
|
95
|
+
if not has_visible_text(shape):
|
|
96
|
+
continue
|
|
97
|
+
text = text_of(shape).strip()
|
|
98
|
+
for pattern in PLACEHOLDER_PATTERNS:
|
|
99
|
+
if pattern.search(text):
|
|
100
|
+
findings.append(
|
|
101
|
+
{
|
|
102
|
+
"kind": "leftover_placeholder_text",
|
|
103
|
+
"severity": "fail",
|
|
104
|
+
"slide": output_index,
|
|
105
|
+
"shape": shape.name,
|
|
106
|
+
"text": text[:80],
|
|
107
|
+
"message": "template prompt text is still on the slide",
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
break
|
|
111
|
+
if not text and shape.is_placeholder:
|
|
112
|
+
findings.append({"kind": "empty_placeholder", "severity": "review", "slide": output_index, "shape": shape.name, "message": "placeholder was left empty"})
|
|
113
|
+
|
|
114
|
+
inherited_art = [box(shape) for shape in source_slide.shapes if is_picture(shape) and box(shape)]
|
|
115
|
+
slide_w, slide_h = _slide_size_in(source_slide)
|
|
116
|
+
full_bleed = {
|
|
117
|
+
index
|
|
118
|
+
for index, art in enumerate(inherited_art)
|
|
119
|
+
if art[0] <= 0.02 and art[1] <= 0.02 and art[2] >= slide_w * 0.98 and art[3] >= slide_h * 0.98
|
|
120
|
+
}
|
|
121
|
+
for shape in output_slide.shapes:
|
|
122
|
+
# only an opaque panel is a problem: text over a background image is the template working
|
|
123
|
+
if is_picture(shape) or not _has_solid_fill(shape):
|
|
124
|
+
continue
|
|
125
|
+
geometry = box(shape)
|
|
126
|
+
if geometry is None:
|
|
127
|
+
continue
|
|
128
|
+
for art_index, art in enumerate(inherited_art):
|
|
129
|
+
if art_index in full_bleed:
|
|
130
|
+
continue
|
|
131
|
+
ratio = overlap_ratio(geometry, art)
|
|
132
|
+
if ratio >= ART_COVER_MIN_RATIO:
|
|
133
|
+
findings.append(
|
|
134
|
+
{
|
|
135
|
+
"kind": "template_art_covered",
|
|
136
|
+
"severity": "fail",
|
|
137
|
+
"slide": output_index,
|
|
138
|
+
"shape": shape.name,
|
|
139
|
+
"overlap": round(ratio, 2),
|
|
140
|
+
"message": "a text or table block sits on top of the template artwork",
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
blocking = [item for item in findings if item["severity"] == "fail"]
|
|
146
|
+
return {
|
|
147
|
+
"gate": "template-authoring-fidelity",
|
|
148
|
+
"template": str(template_file.resolve()),
|
|
149
|
+
"final": str(final_file.resolve()),
|
|
150
|
+
"plan": str(plan_path) if plan_path else None,
|
|
151
|
+
"mappedSlides": len(mapping),
|
|
152
|
+
"findings": findings,
|
|
153
|
+
"passed": not blocking,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main(argv: list[str] | None = None) -> int:
|
|
158
|
+
import argparse
|
|
159
|
+
|
|
160
|
+
parser = argparse.ArgumentParser(description="Find leftover template prompts, layout drift and covered artwork.")
|
|
161
|
+
parser.add_argument("--template", required=True, help="the template .pptx")
|
|
162
|
+
parser.add_argument("--final", required=True, help="the finished .pptx")
|
|
163
|
+
parser.add_argument("--plan", help="optional template-frame-map.json for the slide mapping")
|
|
164
|
+
parser.add_argument("--json-out", help="also write the report here")
|
|
165
|
+
args = parser.parse_args(argv)
|
|
166
|
+
|
|
167
|
+
report = check(args.template, args.final, args.plan)
|
|
168
|
+
if args.json_out:
|
|
169
|
+
Path(args.json_out).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
170
|
+
json.dump(report, sys.stdout, ensure_ascii=False, indent=2)
|
|
171
|
+
sys.stdout.write("\n")
|
|
172
|
+
return 0 if report["passed"] else 1
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
raise SystemExit(main())
|
package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Read a template deck into records an author can plan against.
|
|
2
|
+
|
|
3
|
+
Purpose: matching a supplied deck starts with knowing what it actually is —
|
|
4
|
+
slide size, which layouts exist, what each slide inherits, where the title and
|
|
5
|
+
body sit, which elements repeat on every slide, what type and colours the theme
|
|
6
|
+
carries. Guessing from memory of "a normal 16:9 deck" is how a template gets
|
|
7
|
+
flattened.
|
|
8
|
+
|
|
9
|
+
Writes:
|
|
10
|
+
<out-dir>/template-inspect.ndjson one JSON record per slide
|
|
11
|
+
<out-dir>/template-manifest.json deck level summary
|
|
12
|
+
|
|
13
|
+
Exit codes: 0 written, 2 unreadable input.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "container_tools"))
|
|
23
|
+
|
|
24
|
+
from ooxml_helpers import box, has_visible_text, is_chart, is_picture, is_table, placeholder_kind, slide_size_in, text_of # noqa: E402
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _theme(presentation) -> dict:
|
|
28
|
+
fonts: set[str] = set()
|
|
29
|
+
fills: set[str] = set()
|
|
30
|
+
for master in presentation.slide_masters:
|
|
31
|
+
for layout in master.slide_layouts:
|
|
32
|
+
for shape in layout.shapes:
|
|
33
|
+
if has_visible_text(shape):
|
|
34
|
+
for paragraph in shape.text_frame.paragraphs:
|
|
35
|
+
for run in paragraph.runs:
|
|
36
|
+
if run.font.name:
|
|
37
|
+
fonts.add(run.font.name)
|
|
38
|
+
for slide in presentation.slides:
|
|
39
|
+
for shape in slide.shapes:
|
|
40
|
+
if has_visible_text(shape):
|
|
41
|
+
for paragraph in shape.text_frame.paragraphs:
|
|
42
|
+
for run in paragraph.runs:
|
|
43
|
+
if run.font.name:
|
|
44
|
+
fonts.add(run.font.name)
|
|
45
|
+
return {"fontsSeen": sorted(fonts), "colourSourcesNote": "check layouts and masters in PowerPoint for exact theme colours"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def inspect_template(template_path: str | Path, out_dir: str | Path) -> dict:
|
|
49
|
+
from pptx import Presentation
|
|
50
|
+
|
|
51
|
+
source = Path(template_path)
|
|
52
|
+
if not source.is_file():
|
|
53
|
+
return {"ok": False, "error": {"code": "input_missing", "message": f"no such file: {source}"}}
|
|
54
|
+
presentation = Presentation(str(source))
|
|
55
|
+
slide_w, slide_h = slide_size_in(presentation)
|
|
56
|
+
destination = Path(out_dir)
|
|
57
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
|
|
59
|
+
records: list[dict] = []
|
|
60
|
+
for index, slide in enumerate(presentation.slides, start=1):
|
|
61
|
+
layout = slide.slide_layout
|
|
62
|
+
placeholders = []
|
|
63
|
+
for shape in slide.shapes:
|
|
64
|
+
if not shape.is_placeholder:
|
|
65
|
+
continue
|
|
66
|
+
placeholders.append(
|
|
67
|
+
{
|
|
68
|
+
"idx": shape.placeholder_format.idx,
|
|
69
|
+
"kind": placeholder_kind(shape) or "",
|
|
70
|
+
"type": int(shape.placeholder_format.type) if shape.placeholder_format.type is not None else None,
|
|
71
|
+
"bboxIn": [round(value, 3) for value in (box(shape) or (0, 0, 0, 0))],
|
|
72
|
+
"text": text_of(shape)[:120],
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
layout_placeholders = [
|
|
76
|
+
{
|
|
77
|
+
"idx": shape.placeholder_format.idx,
|
|
78
|
+
"kind": placeholder_kind(shape) or "",
|
|
79
|
+
"bboxIn": [round(value, 3) for value in (box(shape) or (0, 0, 0, 0))],
|
|
80
|
+
}
|
|
81
|
+
for shape in layout.shapes
|
|
82
|
+
if shape.is_placeholder
|
|
83
|
+
]
|
|
84
|
+
records.append(
|
|
85
|
+
{
|
|
86
|
+
"slide": index,
|
|
87
|
+
"layout": layout.name,
|
|
88
|
+
"layoutIndex": list(presentation.slide_layouts).index(layout) if layout in list(presentation.slide_layouts) else None,
|
|
89
|
+
"layoutPlaceholders": layout_placeholders,
|
|
90
|
+
"placeholders": placeholders,
|
|
91
|
+
"shapes": len(slide.shapes),
|
|
92
|
+
"pictures": sum(1 for shape in slide.shapes if is_picture(shape)),
|
|
93
|
+
"charts": sum(1 for shape in slide.shapes if is_chart(shape)),
|
|
94
|
+
"tables": sum(1 for shape in slide.shapes if is_table(shape)),
|
|
95
|
+
"textExcerpt": " | ".join(text_of(shape)[:80] for shape in slide.shapes if has_visible_text(shape))[:240],
|
|
96
|
+
"backgroundPicture": any(
|
|
97
|
+
is_picture(shape) and box(shape) and box(shape)[2] >= slide_w * 0.95 and box(shape)[3] >= slide_h * 0.95
|
|
98
|
+
for shape in slide.shapes
|
|
99
|
+
),
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
manifest = {
|
|
104
|
+
"template": str(source.resolve()),
|
|
105
|
+
"slideSizeIn": [round(slide_w, 3), round(slide_h, 3)],
|
|
106
|
+
"aspect": "16:9" if abs(slide_w / slide_h - 16 / 9) < 0.02 else "4:3" if abs(slide_w / slide_h - 4 / 3) < 0.02 else "custom",
|
|
107
|
+
"slideCount": len(presentation.slides),
|
|
108
|
+
"layouts": [layout.name for layout in presentation.slide_layouts],
|
|
109
|
+
"masters": len(presentation.slide_masters),
|
|
110
|
+
"slides": [{key: record[key] for key in ("slide", "layout", "shapes", "backgroundPicture")} for record in records],
|
|
111
|
+
"theme": _theme(presentation),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ndjson = destination / "template-inspect.ndjson"
|
|
115
|
+
ndjson.write_text("\n".join(json.dumps(record, ensure_ascii=False) for record in records) + "\n", encoding="utf-8")
|
|
116
|
+
manifest_path = destination / "template-manifest.json"
|
|
117
|
+
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"ok": True,
|
|
121
|
+
"template": str(source.resolve()),
|
|
122
|
+
"records": len(records),
|
|
123
|
+
"ndjson": str(ndjson.resolve()),
|
|
124
|
+
"manifest": str(manifest_path.resolve()),
|
|
125
|
+
"summary": {
|
|
126
|
+
"slideCount": manifest["slideCount"],
|
|
127
|
+
"slideSizeIn": manifest["slideSizeIn"],
|
|
128
|
+
"aspect": manifest["aspect"],
|
|
129
|
+
"layouts": manifest["layouts"],
|
|
130
|
+
"backgroundArtSlides": [record["slide"] for record in records if record["backgroundPicture"]],
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main(argv: list[str] | None = None) -> int:
|
|
136
|
+
import argparse
|
|
137
|
+
|
|
138
|
+
parser = argparse.ArgumentParser(description="Summarise a template deck: layouts, placeholders, geometry, theme.")
|
|
139
|
+
parser.add_argument("template", nargs="?", help="path to the template .pptx")
|
|
140
|
+
parser.add_argument("--template", dest="template_opt", help="path to the template .pptx (same as the positional)")
|
|
141
|
+
parser.add_argument("--out-dir", required=True, help="directory for template-inspect.ndjson and template-manifest.json")
|
|
142
|
+
args = parser.parse_args(argv)
|
|
143
|
+
template = args.template_opt or args.template
|
|
144
|
+
if not template:
|
|
145
|
+
parser.error("a template .pptx path is required (positional or --template)")
|
|
146
|
+
|
|
147
|
+
report = inspect_template(template, args.out_dir)
|
|
148
|
+
json.dump(report, sys.stdout, ensure_ascii=False, indent=2)
|
|
149
|
+
sys.stdout.write("\n")
|
|
150
|
+
return 0 if report.get("ok") else 2
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
raise SystemExit(main())
|