dsh-office-kit 0.2.0 → 0.3.1
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 +99 -0
- package/assets/skills/office-documents/scripts/a11y_audit.py +201 -0
- package/assets/skills/office-documents/scripts/accept_tracked_changes.py +158 -0
- package/assets/skills/office-documents/scripts/add_tracked_replacements.py +155 -0
- package/assets/skills/office-documents/scripts/append_blocks.py +115 -0
- package/assets/skills/office-documents/scripts/apply_template_styles.py +111 -0
- package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +109 -0
- package/assets/skills/office-documents/scripts/comments_add.py +143 -0
- package/assets/skills/office-documents/scripts/comments_apply_patch.py +121 -0
- package/assets/skills/office-documents/scripts/comments_extract.py +92 -0
- package/assets/skills/office-documents/scripts/comments_strip.py +81 -0
- package/assets/skills/office-documents/scripts/content_controls.py +199 -0
- package/assets/skills/office-documents/scripts/core_properties.py +131 -0
- package/assets/skills/office-documents/scripts/docx_kit.py +594 -0
- package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +232 -0
- package/assets/skills/office-documents/scripts/docx_table_to_csv.py +106 -0
- package/assets/skills/office-documents/scripts/fields_materialize.py +73 -0
- package/assets/skills/office-documents/scripts/fields_report.py +66 -0
- package/assets/skills/office-documents/scripts/flatten_ref_fields.py +69 -0
- package/assets/skills/office-documents/scripts/footnotes_report.py +84 -0
- package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +103 -0
- package/assets/skills/office-documents/scripts/heading_audit.py +88 -0
- package/assets/skills/office-documents/scripts/images_audit.py +87 -0
- package/assets/skills/office-documents/scripts/insert_note.py +133 -0
- package/assets/skills/office-documents/scripts/insert_ref_fields.py +97 -0
- package/assets/skills/office-documents/scripts/insert_toc.py +91 -0
- package/assets/skills/office-documents/scripts/internal_nav.py +153 -0
- package/assets/skills/office-documents/scripts/make_fixtures.py +176 -0
- package/assets/skills/office-documents/scripts/merge_docx_append.py +246 -0
- package/assets/skills/office-documents/scripts/privacy_scrub.py +138 -0
- package/assets/skills/office-documents/scripts/redact_docx.py +120 -0
- package/assets/skills/office-documents/scripts/render_and_diff.py +108 -0
- package/assets/skills/office-documents/scripts/replace_text.py +161 -0
- package/assets/skills/office-documents/scripts/section_audit.py +98 -0
- package/assets/skills/office-documents/scripts/set_protection.py +99 -0
- package/assets/skills/office-documents/scripts/style_lint.py +118 -0
- package/assets/skills/office-documents/scripts/style_normalize.py +110 -0
- package/assets/skills/office-documents/scripts/table_geometry.py +174 -0
- package/assets/skills/office-documents/scripts/watermark_add.py +55 -0
- package/assets/skills/office-documents/scripts/watermark_audit_remove.py +74 -0
- package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +124 -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 +143 -0
- package/assets/skills/office-presentations/container_tools/core_properties.py +127 -0
- package/assets/skills/office-presentations/container_tools/create_montage.py +133 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +292 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +164 -0
- package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +151 -0
- package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +206 -0
- package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +219 -0
- package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +352 -0
- package/assets/skills/office-presentations/container_tools/preview_render.py +382 -0
- package/assets/skills/office-presentations/container_tools/render_slides.py +146 -0
- package/assets/skills/office-presentations/container_tools/replace_text.py +175 -0
- package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +236 -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 +183 -0
- package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +161 -0
- package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +118 -0
- package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +246 -0
- package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +192 -0
- package/assets/skills/office-spreadsheets/SKILL.md +105 -101
- package/assets/skills/office-spreadsheets/container_tools/add_chart.py +243 -0
- package/assets/skills/office-spreadsheets/container_tools/add_image.py +114 -0
- package/assets/skills/office-spreadsheets/container_tools/append_rows.py +129 -0
- package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +313 -0
- package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +405 -0
- package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +287 -0
- package/assets/skills/office-spreadsheets/container_tools/set_cells.py +142 -0
- package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +141 -0
- package/assets/skills/office-spreadsheets/container_tools/style_cells.py +140 -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 +260 -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 +246 -89
- package/package.json +1 -1
- package/postinstall.mjs +92 -82
- package/python/office_cli.py +213 -254
package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
24
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
25
|
+
try:
|
|
26
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
27
|
+
except (AttributeError, ValueError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
PLACEHOLDER_PATTERNS = [
|
|
31
|
+
re.compile(pattern, re.IGNORECASE)
|
|
32
|
+
for pattern in (
|
|
33
|
+
r"^slide number$",
|
|
34
|
+
r"^date$",
|
|
35
|
+
r"^footer$",
|
|
36
|
+
r"^click to add (title|subtitle|text|body)",
|
|
37
|
+
r"^(title|subtitle|name|text|body) goes here$",
|
|
38
|
+
r"^lorem ipsum\b",
|
|
39
|
+
r"\btemplate instruction\b",
|
|
40
|
+
r"^\[.*\]$",
|
|
41
|
+
)
|
|
42
|
+
]
|
|
43
|
+
ART_COVER_MIN_RATIO = 0.9
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _has_solid_fill(shape) -> bool:
|
|
47
|
+
try:
|
|
48
|
+
from pptx.enum.dml import MSO_FILL
|
|
49
|
+
|
|
50
|
+
return shape.fill.type == MSO_FILL.SOLID
|
|
51
|
+
except Exception: # noqa: BLE001 - shapes without a fill API are never opaque panels
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _slide_size_in(slide) -> tuple[float, float]:
|
|
56
|
+
presentation = slide.part.package.presentation_part.presentation
|
|
57
|
+
return presentation.slide_width / 914400, presentation.slide_height / 914400
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def check(template_path: str | Path, final_path: str | Path, plan_path: str | Path | None = None) -> dict:
|
|
61
|
+
from pptx import Presentation
|
|
62
|
+
|
|
63
|
+
template_file = Path(template_path)
|
|
64
|
+
final_file = Path(final_path)
|
|
65
|
+
for candidate in (template_file, final_file):
|
|
66
|
+
if not candidate.is_file():
|
|
67
|
+
return {"gate": "template-authoring-fidelity", "passed": False, "findings": [{"kind": "missing_file", "detail": str(candidate)}]}
|
|
68
|
+
|
|
69
|
+
template = Presentation(str(template_file))
|
|
70
|
+
final = Presentation(str(final_file))
|
|
71
|
+
template_slides = list(template.slides)
|
|
72
|
+
final_slides = list(final.slides)
|
|
73
|
+
plan = {}
|
|
74
|
+
if plan_path and Path(plan_path).is_file():
|
|
75
|
+
plan = json.loads(Path(plan_path).read_text(encoding="utf-8"))
|
|
76
|
+
|
|
77
|
+
findings: list[dict] = []
|
|
78
|
+
mapping: list[tuple[int, int]] = []
|
|
79
|
+
for entry in plan.get("outputSlides") or []:
|
|
80
|
+
mapping.append((int(entry.get("slide", len(mapping) + 1)), int(entry.get("sourceSlide", 0))))
|
|
81
|
+
if not mapping:
|
|
82
|
+
mapping = [(index, index) for index in range(1, min(len(template_slides), len(final_slides)) + 1)]
|
|
83
|
+
|
|
84
|
+
for output_index, source_index in mapping:
|
|
85
|
+
if not (1 <= output_index <= len(final_slides) and 1 <= source_index <= len(template_slides)):
|
|
86
|
+
findings.append({"kind": "mapping_out_of_range", "severity": "fail", "message": f"mapping {output_index}->{source_index} is outside the deck"})
|
|
87
|
+
continue
|
|
88
|
+
output_slide = final_slides[output_index - 1]
|
|
89
|
+
source_slide = template_slides[source_index - 1]
|
|
90
|
+
|
|
91
|
+
if output_slide.slide_layout.name != source_slide.slide_layout.name:
|
|
92
|
+
findings.append(
|
|
93
|
+
{
|
|
94
|
+
"kind": "layout_drift",
|
|
95
|
+
"severity": "review",
|
|
96
|
+
"slide": output_index,
|
|
97
|
+
"message": f"layout is '{output_slide.slide_layout.name}', template slide used '{source_slide.slide_layout.name}'",
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
for shape in output_slide.shapes:
|
|
102
|
+
if not has_visible_text(shape):
|
|
103
|
+
continue
|
|
104
|
+
text = text_of(shape).strip()
|
|
105
|
+
for pattern in PLACEHOLDER_PATTERNS:
|
|
106
|
+
if pattern.search(text):
|
|
107
|
+
findings.append(
|
|
108
|
+
{
|
|
109
|
+
"kind": "leftover_placeholder_text",
|
|
110
|
+
"severity": "fail",
|
|
111
|
+
"slide": output_index,
|
|
112
|
+
"shape": shape.name,
|
|
113
|
+
"text": text[:80],
|
|
114
|
+
"message": "template prompt text is still on the slide",
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
break
|
|
118
|
+
if not text and shape.is_placeholder:
|
|
119
|
+
findings.append({"kind": "empty_placeholder", "severity": "review", "slide": output_index, "shape": shape.name, "message": "placeholder was left empty"})
|
|
120
|
+
|
|
121
|
+
inherited_art = [box(shape) for shape in source_slide.shapes if is_picture(shape) and box(shape)]
|
|
122
|
+
slide_w, slide_h = _slide_size_in(source_slide)
|
|
123
|
+
full_bleed = {
|
|
124
|
+
index
|
|
125
|
+
for index, art in enumerate(inherited_art)
|
|
126
|
+
if art[0] <= 0.02 and art[1] <= 0.02 and art[2] >= slide_w * 0.98 and art[3] >= slide_h * 0.98
|
|
127
|
+
}
|
|
128
|
+
for shape in output_slide.shapes:
|
|
129
|
+
# only an opaque panel is a problem: text over a background image is the template working
|
|
130
|
+
if is_picture(shape) or not _has_solid_fill(shape):
|
|
131
|
+
continue
|
|
132
|
+
geometry = box(shape)
|
|
133
|
+
if geometry is None:
|
|
134
|
+
continue
|
|
135
|
+
for art_index, art in enumerate(inherited_art):
|
|
136
|
+
if art_index in full_bleed:
|
|
137
|
+
continue
|
|
138
|
+
ratio = overlap_ratio(geometry, art)
|
|
139
|
+
if ratio >= ART_COVER_MIN_RATIO:
|
|
140
|
+
findings.append(
|
|
141
|
+
{
|
|
142
|
+
"kind": "template_art_covered",
|
|
143
|
+
"severity": "fail",
|
|
144
|
+
"slide": output_index,
|
|
145
|
+
"shape": shape.name,
|
|
146
|
+
"overlap": round(ratio, 2),
|
|
147
|
+
"message": "a text or table block sits on top of the template artwork",
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
break
|
|
151
|
+
|
|
152
|
+
blocking = [item for item in findings if item["severity"] == "fail"]
|
|
153
|
+
return {
|
|
154
|
+
"gate": "template-authoring-fidelity",
|
|
155
|
+
"template": str(template_file.resolve()),
|
|
156
|
+
"final": str(final_file.resolve()),
|
|
157
|
+
"plan": str(plan_path) if plan_path else None,
|
|
158
|
+
"mappedSlides": len(mapping),
|
|
159
|
+
"findings": findings,
|
|
160
|
+
"passed": not blocking,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def main(argv: list[str] | None = None) -> int:
|
|
165
|
+
import argparse
|
|
166
|
+
|
|
167
|
+
parser = argparse.ArgumentParser(description="Find leftover template prompts, layout drift and covered artwork.")
|
|
168
|
+
parser.add_argument("--template", required=True, help="the template .pptx")
|
|
169
|
+
parser.add_argument("--final", required=True, help="the finished .pptx")
|
|
170
|
+
parser.add_argument("--plan", help="optional template-frame-map.json for the slide mapping")
|
|
171
|
+
parser.add_argument("--json-out", help="also write the report here")
|
|
172
|
+
args = parser.parse_args(argv)
|
|
173
|
+
|
|
174
|
+
report = check(args.template, args.final, args.plan)
|
|
175
|
+
if args.json_out:
|
|
176
|
+
Path(args.json_out).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
177
|
+
json.dump(report, sys.stdout, ensure_ascii=False, indent=2)
|
|
178
|
+
sys.stdout.write("\n")
|
|
179
|
+
return 0 if report["passed"] else 1
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__":
|
|
183
|
+
raise SystemExit(main())
|
package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
27
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
28
|
+
try:
|
|
29
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
30
|
+
except (AttributeError, ValueError):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _theme(presentation) -> dict:
|
|
35
|
+
fonts: set[str] = set()
|
|
36
|
+
fills: set[str] = set()
|
|
37
|
+
for master in presentation.slide_masters:
|
|
38
|
+
for layout in master.slide_layouts:
|
|
39
|
+
for shape in layout.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
|
+
for slide in presentation.slides:
|
|
46
|
+
for shape in slide.shapes:
|
|
47
|
+
if has_visible_text(shape):
|
|
48
|
+
for paragraph in shape.text_frame.paragraphs:
|
|
49
|
+
for run in paragraph.runs:
|
|
50
|
+
if run.font.name:
|
|
51
|
+
fonts.add(run.font.name)
|
|
52
|
+
return {"fontsSeen": sorted(fonts), "colourSourcesNote": "check layouts and masters in PowerPoint for exact theme colours"}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def inspect_template(template_path: str | Path, out_dir: str | Path) -> dict:
|
|
56
|
+
from pptx import Presentation
|
|
57
|
+
|
|
58
|
+
source = Path(template_path)
|
|
59
|
+
if not source.is_file():
|
|
60
|
+
return {"ok": False, "error": {"code": "input_missing", "message": f"no such file: {source}"}}
|
|
61
|
+
presentation = Presentation(str(source))
|
|
62
|
+
slide_w, slide_h = slide_size_in(presentation)
|
|
63
|
+
destination = Path(out_dir)
|
|
64
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
records: list[dict] = []
|
|
67
|
+
for index, slide in enumerate(presentation.slides, start=1):
|
|
68
|
+
layout = slide.slide_layout
|
|
69
|
+
placeholders = []
|
|
70
|
+
for shape in slide.shapes:
|
|
71
|
+
if not shape.is_placeholder:
|
|
72
|
+
continue
|
|
73
|
+
placeholders.append(
|
|
74
|
+
{
|
|
75
|
+
"idx": shape.placeholder_format.idx,
|
|
76
|
+
"kind": placeholder_kind(shape) or "",
|
|
77
|
+
"type": int(shape.placeholder_format.type) if shape.placeholder_format.type is not None else None,
|
|
78
|
+
"bboxIn": [round(value, 3) for value in (box(shape) or (0, 0, 0, 0))],
|
|
79
|
+
"text": text_of(shape)[:120],
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
layout_placeholders = [
|
|
83
|
+
{
|
|
84
|
+
"idx": shape.placeholder_format.idx,
|
|
85
|
+
"kind": placeholder_kind(shape) or "",
|
|
86
|
+
"bboxIn": [round(value, 3) for value in (box(shape) or (0, 0, 0, 0))],
|
|
87
|
+
}
|
|
88
|
+
for shape in layout.shapes
|
|
89
|
+
if shape.is_placeholder
|
|
90
|
+
]
|
|
91
|
+
records.append(
|
|
92
|
+
{
|
|
93
|
+
"slide": index,
|
|
94
|
+
"layout": layout.name,
|
|
95
|
+
"layoutIndex": list(presentation.slide_layouts).index(layout) if layout in list(presentation.slide_layouts) else None,
|
|
96
|
+
"layoutPlaceholders": layout_placeholders,
|
|
97
|
+
"placeholders": placeholders,
|
|
98
|
+
"shapes": len(slide.shapes),
|
|
99
|
+
"pictures": sum(1 for shape in slide.shapes if is_picture(shape)),
|
|
100
|
+
"charts": sum(1 for shape in slide.shapes if is_chart(shape)),
|
|
101
|
+
"tables": sum(1 for shape in slide.shapes if is_table(shape)),
|
|
102
|
+
"textExcerpt": " | ".join(text_of(shape)[:80] for shape in slide.shapes if has_visible_text(shape))[:240],
|
|
103
|
+
"backgroundPicture": any(
|
|
104
|
+
is_picture(shape) and box(shape) and box(shape)[2] >= slide_w * 0.95 and box(shape)[3] >= slide_h * 0.95
|
|
105
|
+
for shape in slide.shapes
|
|
106
|
+
),
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
manifest = {
|
|
111
|
+
"template": str(source.resolve()),
|
|
112
|
+
"slideSizeIn": [round(slide_w, 3), round(slide_h, 3)],
|
|
113
|
+
"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",
|
|
114
|
+
"slideCount": len(presentation.slides),
|
|
115
|
+
"layouts": [layout.name for layout in presentation.slide_layouts],
|
|
116
|
+
"masters": len(presentation.slide_masters),
|
|
117
|
+
"slides": [{key: record[key] for key in ("slide", "layout", "shapes", "backgroundPicture")} for record in records],
|
|
118
|
+
"theme": _theme(presentation),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
ndjson = destination / "template-inspect.ndjson"
|
|
122
|
+
ndjson.write_text("\n".join(json.dumps(record, ensure_ascii=False) for record in records) + "\n", encoding="utf-8")
|
|
123
|
+
manifest_path = destination / "template-manifest.json"
|
|
124
|
+
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
"ok": True,
|
|
128
|
+
"template": str(source.resolve()),
|
|
129
|
+
"records": len(records),
|
|
130
|
+
"ndjson": str(ndjson.resolve()),
|
|
131
|
+
"manifest": str(manifest_path.resolve()),
|
|
132
|
+
"summary": {
|
|
133
|
+
"slideCount": manifest["slideCount"],
|
|
134
|
+
"slideSizeIn": manifest["slideSizeIn"],
|
|
135
|
+
"aspect": manifest["aspect"],
|
|
136
|
+
"layouts": manifest["layouts"],
|
|
137
|
+
"backgroundArtSlides": [record["slide"] for record in records if record["backgroundPicture"]],
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main(argv: list[str] | None = None) -> int:
|
|
143
|
+
import argparse
|
|
144
|
+
|
|
145
|
+
parser = argparse.ArgumentParser(description="Summarise a template deck: layouts, placeholders, geometry, theme.")
|
|
146
|
+
parser.add_argument("template", nargs="?", help="path to the template .pptx")
|
|
147
|
+
parser.add_argument("--template", dest="template_opt", help="path to the template .pptx (same as the positional)")
|
|
148
|
+
parser.add_argument("--out-dir", required=True, help="directory for template-inspect.ndjson and template-manifest.json")
|
|
149
|
+
args = parser.parse_args(argv)
|
|
150
|
+
template = args.template_opt or args.template
|
|
151
|
+
if not template:
|
|
152
|
+
parser.error("a template .pptx path is required (positional or --template)")
|
|
153
|
+
|
|
154
|
+
report = inspect_template(template, args.out_dir)
|
|
155
|
+
json.dump(report, sys.stdout, ensure_ascii=False, indent=2)
|
|
156
|
+
sys.stdout.write("\n")
|
|
157
|
+
return 0 if report.get("ok") else 2
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Tile slide previews into one image so the whole deck can be judged at once.
|
|
2
|
+
|
|
3
|
+
Purpose: reading ten previews one by one costs ten looks and hides pace,
|
|
4
|
+
repetition and colour drift between slides. One contact sheet shows whether the
|
|
5
|
+
deck has a rhythm: which slides are dense, which are empty, whether the cover
|
|
6
|
+
carries the same accent as the close.
|
|
7
|
+
|
|
8
|
+
Exit codes: 0 sheet written, 2 nothing readable.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import math
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
19
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
20
|
+
try:
|
|
21
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
22
|
+
except (AttributeError, ValueError):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "container_tools"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _natural_key(path: Path) -> list:
|
|
29
|
+
import re
|
|
30
|
+
|
|
31
|
+
return [int(part) if part.isdigit() else part.lower() for part in re.split(r"(\d+)", path.stem)]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_sheet(images: list[Path], out_path: Path, columns: int = 3, label_height: int = 48, padding: int = 18) -> dict:
|
|
35
|
+
from PIL import Image, ImageDraw
|
|
36
|
+
|
|
37
|
+
columns = max(1, columns)
|
|
38
|
+
rows = max(1, math.ceil(len(images) / columns))
|
|
39
|
+
thumbs = []
|
|
40
|
+
cell_w = 0
|
|
41
|
+
cell_h = 0
|
|
42
|
+
for path in images:
|
|
43
|
+
try:
|
|
44
|
+
with Image.open(path) as opened:
|
|
45
|
+
thumb = opened.convert("RGB")
|
|
46
|
+
thumbs.append((path, thumb.copy(), None))
|
|
47
|
+
cell_w = max(cell_w, thumb.width)
|
|
48
|
+
cell_h = max(cell_h, thumb.height)
|
|
49
|
+
except Exception as exc: # noqa: BLE001 - unreadable previews become placeholder cells
|
|
50
|
+
thumbs.append((path, None, str(exc)))
|
|
51
|
+
if not cell_w or not cell_h:
|
|
52
|
+
return {"ok": False, "error": {"code": "no_readable_images", "message": "none of the inputs could be opened"}}
|
|
53
|
+
|
|
54
|
+
width = columns * cell_w + (columns + 1) * padding
|
|
55
|
+
height = rows * (cell_h + label_height) + (rows + 1) * padding
|
|
56
|
+
sheet = Image.new("RGB", (width, height), (246, 246, 246))
|
|
57
|
+
draw = ImageDraw.Draw(sheet)
|
|
58
|
+
|
|
59
|
+
for index, (path, thumb, error) in enumerate(thumbs):
|
|
60
|
+
row, column = divmod(index, columns)
|
|
61
|
+
x = padding + column * (cell_w + padding)
|
|
62
|
+
y = padding + row * (cell_h + label_height + padding)
|
|
63
|
+
if thumb is None:
|
|
64
|
+
draw.rectangle([x, y, x + cell_w, y + cell_h], fill=(232, 232, 232), outline=(180, 60, 60), width=3)
|
|
65
|
+
draw.line([x, y, x + cell_w, y + cell_h], fill=(180, 60, 60), width=3)
|
|
66
|
+
draw.line([x + cell_w, y, x, y + cell_h], fill=(180, 60, 60), width=3)
|
|
67
|
+
else:
|
|
68
|
+
sheet.paste(thumb, (x, y))
|
|
69
|
+
draw.rectangle([x, y, x + cell_w, y + cell_h], outline=(190, 190, 190), width=1)
|
|
70
|
+
draw.text((x + 4, y + cell_h + 8), path.stem, fill=(40, 40, 40))
|
|
71
|
+
|
|
72
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
sheet.save(out_path)
|
|
74
|
+
return {
|
|
75
|
+
"ok": True,
|
|
76
|
+
"output": str(out_path.resolve()),
|
|
77
|
+
"cells": len(thumbs),
|
|
78
|
+
"grid": {"columns": columns, "rows": rows},
|
|
79
|
+
"size": [width, height],
|
|
80
|
+
"unreadable": [path.name for path, thumb, _ in thumbs if thumb is None],
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _collect(args) -> list[Path]:
|
|
85
|
+
found: list[Path] = []
|
|
86
|
+
for value in [*args.images, *([args.dir] if args.dir else [])]:
|
|
87
|
+
path = Path(value)
|
|
88
|
+
if path.is_dir():
|
|
89
|
+
found.extend(sorted((item for item in path.iterdir() if item.suffix.lower() in {".png", ".jpg", ".jpeg", ".webp"}), key=_natural_key))
|
|
90
|
+
else:
|
|
91
|
+
found.append(path)
|
|
92
|
+
return found
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def main(argv: list[str] | None = None) -> int:
|
|
96
|
+
import argparse
|
|
97
|
+
|
|
98
|
+
parser = argparse.ArgumentParser(description="Build one contact sheet from slide previews.")
|
|
99
|
+
parser.add_argument("images", nargs="*", help="preview images and/or directories of them")
|
|
100
|
+
parser.add_argument("--dir", help="a directory of preview images to include (same as passing it positionally)")
|
|
101
|
+
parser.add_argument("--out", required=True, help="path of the contact sheet to write")
|
|
102
|
+
parser.add_argument("--columns", type=int, default=3, help="grid width (default 3)")
|
|
103
|
+
parser.add_argument("--json-out", help="also write the report here")
|
|
104
|
+
args = parser.parse_args(argv)
|
|
105
|
+
if not args.images and not args.dir:
|
|
106
|
+
parser.error("give at least one image, one directory, or --dir")
|
|
107
|
+
|
|
108
|
+
images = _collect(args)
|
|
109
|
+
report = build_sheet(images, Path(args.out), args.columns) if images else {"ok": False, "error": {"code": "no_images", "message": "no preview images found"}}
|
|
110
|
+
if args.json_out:
|
|
111
|
+
Path(args.json_out).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
112
|
+
json.dump(report, sys.stdout, ensure_ascii=False, indent=2)
|
|
113
|
+
sys.stdout.write("\n")
|
|
114
|
+
return 0 if report.get("ok") else 2
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
raise SystemExit(main())
|